Coverage for src/pycse/beginner.py: 95.71%
70 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-23 16:23 -0400
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-23 16:23 -0400
1"""Beginner module.
3This module contains function definitions designed to minimize the need for
4Python syntax. They are for beginners just learning, and they try to give
5helpful error messages.
7There are a series of functions to access parts of a list, e.g. the first-fifth
8and nth elements, the last element, all but the first, and all but the last
9elements. There is also a cut function to avoid list slicing syntax. The point
10of these is to delay introducing indexing syntax.
12"""
14import collections.abc
15from scipy.optimize import fsolve as _fsolve
16from scipy.integrate import quad
19def first(x):
20 """Return the first element of x if it is iterable, else return x."""
21 if not isinstance(x, collections.abc.Iterable):
22 raise Exception("{} is not iterable.".format(x))
24 return x[0]
27def second(x):
28 """Return the second element of x."""
29 if not isinstance(x, collections.abc.Iterable):
30 raise Exception("{} is not iterable.".format(x))
32 if not len(x) >= 2:
33 raise Exception("{} does not have a second element.".format(x))
35 return x[1]
38def third(x):
39 """Return the third element of x."""
40 if not isinstance(x, collections.abc.Iterable):
41 raise Exception("{} is not iterable.".format(x))
43 if not len(x) >= 3:
44 raise Exception("{} does not have a third element.".format(x))
46 return x[2]
49def fourth(x):
50 """Return the fourth element of x."""
51 if not isinstance(x, collections.abc.Iterable):
52 raise Exception("{} is not iterable.".format(x))
54 if not len(x) >= 4:
55 raise Exception("{} does not have a fourth element.".format(x))
57 return x[3]
60def fifth(x):
61 """Return the fifth element of x."""
62 if not isinstance(x, collections.abc.Iterable):
63 raise Exception("{} is not iterable.".format(x))
65 if not len(x) >= 5:
66 raise Exception("{} does not have a fifth element.".format(x))
68 return x[4]
71def nth(x, n=0):
72 """Return the nth value of x.
73 Note that `n` starts at 0."""
74 if not isinstance(x, collections.abc.Iterable):
75 raise Exception("{} is not iterable.".format(x))
77 if not len(x) >= n:
78 raise Exception("{} does not have an n={} element.".format(x, n))
80 return x[n]
83def cut(x, start=0, stop=None, step=None):
84 """Alias for x[start:stop:step].
86 This is to avoid having to introduce the slicing syntax.
87 """
88 if not isinstance(x, collections.abc.Iterable):
89 raise Exception("{} is not iterable.".format(x))
91 return x[slice(start, stop, step)]
94def last(x):
95 """Return the last element of x if it is iterable."""
96 if not isinstance(x, collections.abc.Iterable):
97 raise Exception("{} is not iterable.".format(x))
99 return x[-1]
102def rest(x):
103 """Return everything after the first element of x."""
104 if not isinstance(x, collections.abc.Iterable):
105 raise Exception("{} is not iterable.".format(x))
107 return x[1:]
110def butlast(x):
111 """Return everything but the last element of x."""
112 if not isinstance(x, collections.abc.Iterable):
113 raise Exception("{} is not iterable.".format(x))
115 return x[0:-1]
118# * Wrapped functions
120# These functions are wrapped to provide a simpler use for new students. Usually
121# that means there are fewer confusing outputs. For example fsolve returns an
122# array even for a single number which leads to the need to unpack it to get a
123# simple number. The nsolve function does not do that. It returns a float if the
124# result is a 1d array. It also is more explicit about checking for convergence.
127def nsolve(objective, x0, *args, **kwargs):
128 """Solve an objective function.
130 A Wrapped version of scipy.optimize.fsolve.
132 objective: a callable function f(x) = 0
133 x0: the initial guess for the solution.
135 This version warns you if the call did not finish cleanly and prints the
136 message.
138 Returns: If there is only one result it returns a float, otherwise it
139 returns an array.
141 """
142 if "full_output" not in kwargs:
143 kwargs["full_output"] = 1
145 ans, _, flag, msg = _fsolve(objective, x0, *args, **kwargs)
147 if flag != 1:
148 raise Exception("nsolve did not finish cleanly: {}".format(msg))
150 if len(ans) == 1:
151 return float(ans)
152 else:
153 return ans
156# The quad function returns the integral and error estimate. We rarely use the
157# error estimate, so here we eliminate it from the output.
160def integrate(f, a, b, *args, **kwargs):
161 """Integrate the function f(x) from a to b.
163 This wraps scipy.integrate.quad to eliminate the error estimate and provide
164 better debugging information.
166 If the error estimate is greater than the tolerance argument, an exception
167 is raised.
169 """
170 if "full_output" not in kwargs:
171 kwargs["full_output"] = 1
172 results = quad(f, a, b, *args, **kwargs)
174 tolerance = kwargs.get("tolerance", 1e-6)
176 if second(results) > tolerance:
177 raise Exception(
178 "Your integral error {} is too large. ".format(second(results))
179 + "{} ".format(fourth(results))
180 + "See your instructor for help"
181 )
182 return first(results)