|
8 | 8 |
|
9 | 9 | import matplotlib.pyplot as plt
|
10 | 10 | import numpy as np
|
11 |
| -from scipy import integrate, linalg |
| 11 | +from scipy import integrate, linalg, optimize |
12 | 12 |
|
13 | 13 |
|
14 | 14 | class Function:
|
@@ -2024,9 +2024,259 @@ def integral(self, a, b, numerical=False):
|
2024 | 2024 | ans, _ = integrate.quad(self, a, b, epsabs=0.1, limit=10000)
|
2025 | 2025 | return ans
|
2026 | 2026 |
|
2027 |
| - # Not implemented |
2028 | 2027 | def differentiate(self, x, dx=1e-6):
|
| 2028 | + """Evaluates the derivative of a 1-D Function at x with a step size of dx. |
| 2029 | +
|
| 2030 | + Parameters |
| 2031 | + ---------- |
| 2032 | + x : float |
| 2033 | + Point at which to evaluate the derivative. |
| 2034 | + dx : float |
| 2035 | + Step size to use in the derivative calculation. Default is 1e-6. |
| 2036 | +
|
| 2037 | +
|
| 2038 | + Returns |
| 2039 | + ------- |
| 2040 | + ans : float |
| 2041 | + Evaluated derivative. |
| 2042 | + """ |
2029 | 2043 | return (self.getValue(x + dx) - self.getValue(x - dx)) / (2 * dx)
|
2030 |
| - # h = (10)**-300 |
2031 |
| - # z = x + h*1j |
2032 |
| - # return self(z).imag/h |
| 2044 | + |
| 2045 | + def derivativeFunction(self): |
| 2046 | + """Returns a Function object which gives the derivative of the Function object. |
| 2047 | +
|
| 2048 | + Returns |
| 2049 | + ------- |
| 2050 | + result : Function |
| 2051 | + A Function object which gives the derivative of self. |
| 2052 | + """ |
| 2053 | + # Check if Function object source is array |
| 2054 | + if isinstance(self.source, np.ndarray): |
| 2055 | + # Operate on grid values |
| 2056 | + Ys = np.diff(self.source[:, 1]) / np.diff(self.source[:, 0]) |
| 2057 | + Xs = self.source[:-1, 0] + np.diff(self.source[:, 0]) / 2 |
| 2058 | + source = np.concatenate(([Xs], [Ys])).transpose() |
| 2059 | + |
| 2060 | + # Retrieve inputs, outputs and interpolation |
| 2061 | + inputs = self.__inputs__[:] |
| 2062 | + outputs = "d(" + self.__outputs__[0] + ")/d(" + inputs[0] + ")" |
| 2063 | + outputs = "(" + outputs + ")" |
| 2064 | + interpolation = "linear" |
| 2065 | + |
| 2066 | + # Create new Function object |
| 2067 | + return Function(source, inputs, outputs, interpolation) |
| 2068 | + else: |
| 2069 | + return Function(lambda x: self.differentiate(x)) |
| 2070 | + |
| 2071 | + def integralFunction(self, lower=None, upper=None, datapoints=100): |
| 2072 | + """Returns a Function object representing the integral of the Function object. |
| 2073 | +
|
| 2074 | + Parameters |
| 2075 | + ---------- |
| 2076 | + lower : scalar, optional |
| 2077 | + The lower limit of the interval in which the function is to be |
| 2078 | + plotted. If the Function is given by a dataset, the default |
| 2079 | + value is the start of the dataset. |
| 2080 | + upper : scalar, optional |
| 2081 | + The upper limit of the interval in which the function is to be |
| 2082 | + plotted. If the Function is given by a dataset, the default |
| 2083 | + value is the end of the dataset. |
| 2084 | + datapoints : int, optional |
| 2085 | + The number of points in which the integral will be evaluated for |
| 2086 | + plotting it, which draws lines between each evaluated point. |
| 2087 | + The default value is 100. |
| 2088 | +
|
| 2089 | + Returns |
| 2090 | + ------- |
| 2091 | + result : Function |
| 2092 | + The integral of the Function object. |
| 2093 | + """ |
| 2094 | + # Check if lower and upper are given |
| 2095 | + if lower is None: |
| 2096 | + if isinstance(self.source, np.ndarray): |
| 2097 | + lower = self.source[0, 0] |
| 2098 | + else: |
| 2099 | + raise ValueError("Lower limit must be given if source is a function.") |
| 2100 | + if upper is None: |
| 2101 | + if isinstance(self.source, np.ndarray): |
| 2102 | + upper = self.source[-1, 0] |
| 2103 | + else: |
| 2104 | + raise ValueError("Upper limit must be given if source is a function.") |
| 2105 | + |
| 2106 | + # Create a new Function object |
| 2107 | + xData = np.linspace(lower, upper, datapoints) |
| 2108 | + yData = np.zeros(datapoints) |
| 2109 | + for i in range(datapoints): |
| 2110 | + yData[i] = self.integral(lower, xData[i]) |
| 2111 | + return Function(np.concatenate(([xData], [yData])).transpose(), |
| 2112 | + inputs=self.__inputs__, |
| 2113 | + outputs=[o + " Integral" for o in self.__outputs__]) |
| 2114 | + |
| 2115 | + def inverseFunction(self, lower=None, upper=None, datapoints=100): |
| 2116 | + """ |
| 2117 | + Returns the inverse of the Function. The inverse function of F is a function that undoes the operation of F. The |
| 2118 | + inverse of F exists if and only if F is bijective. Makes the domain the range and the range the domain. |
| 2119 | +
|
| 2120 | + Parameters |
| 2121 | + ---------- |
| 2122 | + lower : float |
| 2123 | + Lower limit of the new domain. Only required if the Function's source is a callable instead of a list of points. |
| 2124 | + upper : float |
| 2125 | + Upper limit of the new domain. Only required if the Function's source is a callable instead of a list of points. |
| 2126 | +
|
| 2127 | + Returns |
| 2128 | + ------- |
| 2129 | + result : Function |
| 2130 | + A Function whose domain and range have been inverted. |
| 2131 | + """ |
| 2132 | + if isinstance(self.source, np.ndarray): |
| 2133 | + # Swap the columns |
| 2134 | + source = np.concatenate(([self.source[:, 1]], [self.source[:, 0]])).transpose() |
| 2135 | + |
| 2136 | + return Function( |
| 2137 | + source, |
| 2138 | + inputs=self.__outputs__, |
| 2139 | + outputs=self.__inputs__, |
| 2140 | + interpolation=self.__interpolation__ |
| 2141 | + ) |
| 2142 | + else: |
| 2143 | + return Function( |
| 2144 | + lambda x: self.findOptimalInput(x), |
| 2145 | + inputs=self.__outputs__, |
| 2146 | + outputs=self.__inputs__, |
| 2147 | + interpolation=self.__interpolation__ |
| 2148 | + ) |
| 2149 | + |
| 2150 | + def findOptimalInput(self, val): |
| 2151 | + """ |
| 2152 | + Finds the optimal input for a given output. |
| 2153 | +
|
| 2154 | + Parameters |
| 2155 | + ---------- |
| 2156 | + val : float |
| 2157 | + The value of the output. |
| 2158 | +
|
| 2159 | + Returns |
| 2160 | + ------- |
| 2161 | + result : ndarray |
| 2162 | + The value of the input which gives the output closest to val. |
| 2163 | + """ |
| 2164 | + return optimize.fmin(lambda x: np.abs(self.getValue(x) - val), 0, ftol=1e-6, disp=False) |
| 2165 | + |
| 2166 | + def compose(self, func, lower=None, upper=None, datapoints=100): |
| 2167 | + """ |
| 2168 | + Returns a Function object which is the result of inputing a function into a function |
| 2169 | + (i.e. f(g(x))). The domain will become the domain of the input function and the range |
| 2170 | + will become the range of the original function. |
| 2171 | +
|
| 2172 | + Parameters |
| 2173 | + ---------- |
| 2174 | + func : Function |
| 2175 | + The function to be inputed into the function. |
| 2176 | + lower : float |
| 2177 | + Lower limit of the new domain. |
| 2178 | + upper : float |
| 2179 | + Upper limit of the new domain. |
| 2180 | +
|
| 2181 | + Returns |
| 2182 | + ------- |
| 2183 | + result : Function |
| 2184 | + The result of inputing the function into the function. |
| 2185 | + """ |
| 2186 | + # Check if the input is a function |
| 2187 | + if not isinstance(func, Function): |
| 2188 | + raise TypeError("Input must be a Function object.") |
| 2189 | + |
| 2190 | + # Checks to make sure lower bound is given |
| 2191 | + # If not it will start at the higher of the two lower bounds |
| 2192 | + if lower is None: |
| 2193 | + if isinstance(self.source, np.ndarray): |
| 2194 | + lower = self.source[0, 0] |
| 2195 | + if isinstance(func.source, np.ndarray): |
| 2196 | + lower = func.source[0, 0] if lower is None else max(lower, func.source[0, 0]) |
| 2197 | + if lower is None: |
| 2198 | + raise ValueError("If Functions.source is a <class 'function'>, must provide bounds") |
| 2199 | + |
| 2200 | + # Checks to make sure upper bound is given |
| 2201 | + # If not it will end at the lower of the two upper bounds |
| 2202 | + if upper is None: |
| 2203 | + if isinstance(self.source, np.ndarray): |
| 2204 | + upper = self.source[-1, 0] |
| 2205 | + if isinstance(func.source, np.ndarray): |
| 2206 | + upper = func.source[-1, 0] if upper is None else min(upper, func.source[-1, 0]) |
| 2207 | + if upper is None: |
| 2208 | + raise ValueError("If Functions.source is a <class 'function'>, must provide bounds") |
| 2209 | + |
| 2210 | + # Create a new Function object |
| 2211 | + xData = np.linspace(lower, upper, datapoints) |
| 2212 | + yData = np.zeros(datapoints) |
| 2213 | + for i in range(datapoints): |
| 2214 | + yData[i] = self.getValue(func.getValue(xData[i])) |
| 2215 | + return Function(np.concatenate(([xData], [yData])).T, |
| 2216 | + inputs=func.__inputs__, |
| 2217 | + outputs=self.__outputs__, |
| 2218 | + interpolation=self.__interpolation__, |
| 2219 | + extrapolation=self.__extrapolation__) |
| 2220 | + |
| 2221 | + |
| 2222 | +class PiecewiseFunction(Function): |
| 2223 | + def __new__(cls, source, inputs=["Scalar"], outputs=["Scalar"], interpolation="akima", extrapolation=None, datapoints=50): |
| 2224 | + """ |
| 2225 | + Creates a piecewise function from a dictionary of functions. The keys of the dictionary |
| 2226 | + must be tuples that represent the domain of the function. The domains must be disjoint. |
| 2227 | + The piecewise function will be evaluated at datapoints points to create Function object. |
| 2228 | +
|
| 2229 | + Parameters |
| 2230 | + ---------- |
| 2231 | + source: dictionary |
| 2232 | + A dictionary of Function objects, where the keys are the domains. |
| 2233 | + inputs : list |
| 2234 | + A list of strings that represent the inputs of the function. |
| 2235 | + outputs: list |
| 2236 | + A list of strings that represent the outputs of the function. |
| 2237 | + interpolation: str |
| 2238 | + The type of interpolation to use. The default value is 'akima'. |
| 2239 | + extrapolation: str |
| 2240 | + The type of extrapolation to use. The default value is None. |
| 2241 | + datapoints: int |
| 2242 | + The number of points in which the piecewise function will be |
| 2243 | + evaluated to create a base function. The default value is 100. |
| 2244 | + """ |
| 2245 | + # Check if source is a dictionary |
| 2246 | + if not isinstance(source, dict): |
| 2247 | + raise TypeError("source must be a dictionary") |
| 2248 | + |
| 2249 | + # Check if all keys are tuples |
| 2250 | + for key in source.keys(): |
| 2251 | + if not isinstance(key, tuple): |
| 2252 | + raise TypeError("keys of source must be tuples") |
| 2253 | + |
| 2254 | + # Check if all domains are disjoint |
| 2255 | + for key1 in source.keys(): |
| 2256 | + for key2 in source.keys(): |
| 2257 | + if key1 != key2: |
| 2258 | + if key1[0] < key2[1] and key1[1] > key2[0]: |
| 2259 | + raise ValueError("domains must be disjoint") |
| 2260 | + |
| 2261 | + # Crate Function |
| 2262 | + def calcOutput(func, inputs): |
| 2263 | + o = np.zeros(len(inputs)) |
| 2264 | + for j in range(len(inputs)): |
| 2265 | + o[j] = func.getValue(inputs[j]) |
| 2266 | + return o |
| 2267 | + |
| 2268 | + inputData = [] |
| 2269 | + outputData = [] |
| 2270 | + for key in sorted(source.keys()): |
| 2271 | + i = np.linspace(key[0], key[1], datapoints) |
| 2272 | + i = i[~np.in1d(i, inputData)] |
| 2273 | + inputData = np.concatenate((inputData, i)) |
| 2274 | + |
| 2275 | + f = Function(source[key]) |
| 2276 | + outputData = np.concatenate((outputData, calcOutput(f, i))) |
| 2277 | + |
| 2278 | + return Function(np.concatenate(([inputData], [outputData])).T, |
| 2279 | + inputs=inputs, |
| 2280 | + outputs=outputs, |
| 2281 | + interpolation=interpolation, |
| 2282 | + extrapolation=extrapolation) |
0 commit comments