|
3 | 3 | import numpy as np |
4 | 4 | from itertools import product |
5 | 5 | from functools import partial |
6 | | -from warnings import filterwarnings |
| 6 | +from warnings import filterwarnings, warn |
7 | 7 | from multiprocessing import Pool |
| 8 | +from tqdm import tqdm |
8 | 9 |
|
9 | 10 | from ..utils import evaluate |
10 | 11 | from ..finite_difference import first_order, second_order, fourth_order |
|
68 | 69 | smooth_acceleration: ({'gamma': [1e-2, 1e-1, 1, 10, 100, 1000], |
69 | 70 | 'window_size': [3, 10, 30, 50, 90, 130]}, |
70 | 71 | {'gamma': (1e-4, 1e7), |
71 | | - 'window_size': (1, 1000)}), |
| 72 | + 'window_size': (3, 1000)}), |
72 | 73 | constant_velocity: ({'forwardbackward': [True, False], |
73 | 74 | 'q': [1e-8, 1e-4, 1e-1, 1e1, 1e4, 1e8], |
74 | 75 | 'r': [1e-8, 1e-4, 1e-1, 1e1, 1e4, 1e8]}, |
|
86 | 87 | method_params_and_bounds[method] = method_params_and_bounds[constant_velocity] |
87 | 88 |
|
88 | 89 |
|
89 | | -# This function has to be at the top level for multiprocessing |
| 90 | +# This function has to be at the top level for multiprocessing but is only used by optimize. |
90 | 91 | def _objective_function(point, func, x, dt, singleton_params, search_space_types, dxdt_truth, metric, |
91 | 92 | tvgamma, padding): |
92 | 93 | """Function minimized by scipy.optimize.minimize, needs to have the form: (point, *args) -> float |
@@ -123,7 +124,7 @@ def optimize(func, x, dt, search_space={}, dxdt_truth=None, tvgamma=1e-2, paddin |
123 | 124 | """Find the optimal parameters for a given differentiation method. |
124 | 125 |
|
125 | 126 | :param function func: differentiation method to optimize parameters for, e.g. linear_model.savgoldiff |
126 | | - :param np.array[float]: data to differentiate |
| 127 | + :param np.array[float] x: data to differentiate |
127 | 128 | :param float dt: step size |
128 | 129 | :param dict search_space: function parameter settings to use as initial starting points in optimization, |
129 | 130 | structured as :code:`{param1:[values], param2:[values], param3:value, ...}`. The search space |
@@ -155,7 +156,7 @@ def optimize(func, x, dt, search_space={}, dxdt_truth=None, tvgamma=1e-2, paddin |
155 | 156 | singleton_params = {k:v for k,v in params.items() if not isinstance(v, list)} |
156 | 157 |
|
157 | 158 | # The search space is the Cartesian product of all dimensions where multiple options are given |
158 | | - search_space_types = {k:type(v[0]) for k,v in params.items() if isinstance(v, list)} # map param name -> type for converting to and from point |
| 159 | + search_space_types = {k:type(v[0]) for k,v in params.items() if isinstance(v, list)} # map param name -> type, for converting to and from point |
159 | 160 | if any(v not in [float, int, bool] for v in search_space_types.values()): |
160 | 161 | raise ValueError("Optimization over categorical strings currently not supported") |
161 | 162 | # If excluding string type, I can just cast ints and bools to floats, and we're good to go |
@@ -183,3 +184,58 @@ def optimize(func, x, dt, search_space={}, dxdt_truth=None, tvgamma=1e-2, paddin |
183 | 184 | opt_params.update(singleton_params) |
184 | 185 |
|
185 | 186 | return opt_params, results[opt_idx].fun |
| 187 | + |
| 188 | + |
| 189 | +def suggest_method(x, dt, dxdt_truth=None, cutoff_frequency=None): |
| 190 | + """This is meant as an easy-to-use, automatic way for users with some time on their hands to determine |
| 191 | + a good method and settings for their data. It calls the optimizer over (almost) all methods in the repo |
| 192 | + using default search spaces defined at the top of the :code:`pynumdiff/optimize/_optimize.py` file. |
| 193 | + This routine will take a few minutes to run. |
| 194 | + |
| 195 | + Excluded: |
| 196 | + - ``first_order``, because iterating causes drift |
| 197 | + - ``lineardiff``, ``iterative_velocity``, and ``jerk_sliding``, because they either take too long, |
| 198 | + can be fragile, or tend not to do best |
| 199 | + - all ``cvxpy``-based methods if it is not installed |
| 200 | + - ``velocity`` because it tends to not be best but dominates the optimization process by directly |
| 201 | + optimizing the second term of the metric :math:`L = \\text{RMSE} \\Big( \\text{trapz}(\\mathbf{ |
| 202 | + \\hat{\\dot{x}}}(\\Phi)) + \\mu, \\mathbf{y} \\Big) + \\gamma \\Big({TV}\\big(\\mathbf{\\hat{ |
| 203 | + \\dot{x}}}(\\Phi)\\big)\\Big)` |
| 204 | +
|
| 205 | + :param np.array[float] x: data to differentiate |
| 206 | + :param float dt: step size, because most methods are not designed to work with variable step sizes |
| 207 | + :param np.array[float] dxdt_truth: if known, you can pass true derivative values; otherwise you must use |
| 208 | + :code: `cutoff_frequency` |
| 209 | + :param float cutoff_frequency: in Hz, the highest dominant frequency of interest in the signal, |
| 210 | + used to find parameter :math:`\\gamma` for regularization of the optimization process |
| 211 | + in the absence of ground truth. See https://ieeexplore.ieee.org/document/9241009. |
| 212 | + Estimate by (a) counting real number of peaks per second in the data, (b) looking at |
| 213 | + power spectrum and choosing a cutoff, or (c) making an educated guess. |
| 214 | +
|
| 215 | + :return: tuple[callable, dict, np.array, np.array] of\n |
| 216 | + - **method** -- a reference to the function handle of the differentiation method that worked best |
| 217 | + - **opt_params** -- optimal parameter settings for the differentation method |
| 218 | + """ |
| 219 | + tvgamma = None |
| 220 | + if dxdt_truth is None: # parameter checking |
| 221 | + if cutoff_frequency is None: |
| 222 | + raise ValueError('Either dxdt_truth or cutoff_frequency must be provided.') |
| 223 | + tvgamma = np.exp(-1.6*np.log(cutoff_frequency) -0.71*np.log(dt) - 5.1) # See https://ieeexplore.ieee.org/document/9241009 |
| 224 | + |
| 225 | + methods = [second_order, fourth_order, mediandiff, meandiff, gaussiandiff, friedrichsdiff, butterdiff, |
| 226 | + splinediff, spectraldiff, polydiff, savgoldiff, constant_velocity, constant_acceleration, constant_jerk] |
| 227 | + try: # optionally skip some methods |
| 228 | + import cvxpy |
| 229 | + methods += [acceleration, jerk, smooth_acceleration] |
| 230 | + except ImportError: |
| 231 | + warn("CVXPY not installed, skipping acceleration, jerk, and smooth_acceleration") |
| 232 | + |
| 233 | + best_value = float('inf') # core loop |
| 234 | + for func in tqdm(methods): |
| 235 | + p, v = optimize(func, x, dt, dxdt_truth=dxdt_truth, tvgamma=tvgamma) |
| 236 | + if v < best_value: |
| 237 | + method = func |
| 238 | + best_value = v |
| 239 | + opt_params = p |
| 240 | + |
| 241 | + return method, opt_params |
0 commit comments