Skip to content

Commit aa5f4b7

Browse files
committed
big changes! Basically fully rewrote the optimizer code. Getting same answers for spectraldiff, going to follow on with moves of some of the other modules, deleting now-unnecessary code as I go.
1 parent 15c8870 commit aa5f4b7

9 files changed

Lines changed: 272 additions & 351 deletions

File tree

pynumdiff/kalman_smooth/_kalman_smooth.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ def constant_velocity(x, dt, params=None, options=None, r=None, q=None, forwardb
113113
:param list[float] params: (**deprecated**, prefer :code:`r` and :code:`q`)
114114
:param options: (**deprecated**, prefer :code:`forwardbackward`)
115115
a dictionary consisting of {'forwardbackward': (bool)}
116-
param float r: variance of the signal noise
117-
param float q: variance of the constant velocity model
116+
:param float r: variance of the signal noise
117+
:param float q: variance of the constant velocity model
118118
:param bool forwardbackward: indicates whether to run smoother forwards and backwards
119119
(usually achieves better estimate at end points)
120120
@@ -148,8 +148,8 @@ def constant_acceleration(x, dt, params=None, options=None, r=None, q=None, forw
148148
:param list[float] params: (**deprecated**, prefer :code:`r` and :code:`q`)
149149
:param options: (**deprecated**, prefer :code:`forwardbackward`)
150150
a dictionary consisting of {'forwardbackward': (bool)}
151-
param float r: variance of the signal noise
152-
param float q: variance of the constant acceleration model
151+
:param float r: variance of the signal noise
152+
:param float q: variance of the constant acceleration model
153153
:param bool forwardbackward: indicates whether to run smoother forwards and backwards
154154
(usually achieves better estimate at end points)
155155
@@ -187,8 +187,8 @@ def constant_jerk(x, dt, params=None, options=None, r=None, q=None, forwardbackw
187187
:param list[float] params: (**deprecated**, prefer :code:`r` and :code:`q`)
188188
:param options: (**deprecated**, prefer :code:`forwardbackward`)
189189
a dictionary consisting of {'forwardbackward': (bool)}
190-
param float r: variance of the signal noise
191-
param float q: variance of the constant jerk model
190+
:param float r: variance of the signal noise
191+
:param float q: variance of the constant jerk model
192192
:param bool forwardbackward: indicates whether to run smoother forwards and backwards
193193
(usually achieves better estimate at end points)
194194

pynumdiff/optimize/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22
"""
33
try:
44
import cvxpy
5-
from . import total_variation_regularization
65
except ImportError:
76
from warnings import warn
87
warn("Limited support for total variation regularization and linear model detected! " +
98
"Some functions in the `total_variation_regularization` and `linear_model` modules require " +
109
"CVXPY to be installed. You can still pynumdiff.optimize for other functions.")
1110

12-
from . import finite_difference, smooth_finite_difference, linear_model, kalman_smooth
11+
from ._optimize import optimize
1312

14-
__all__ = ['finite_difference', 'smooth_finite_difference', 'linear_model', 'kalman_smooth', 'total_variation_regularization'] # So these get treated as direct members of the module by sphinx
13+
__all__ = ['optimize'] # So these get treated as direct members of the module by sphinx

pynumdiff/optimize/__optimize__.py

Lines changed: 0 additions & 144 deletions
This file was deleted.

pynumdiff/optimize/_optimize.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Optimization"""
2+
import scipy.optimize
3+
import numpy as np
4+
from itertools import product
5+
from functools import partial
6+
from multiprocessing import Pool
7+
8+
from pynumdiff.utils import evaluate
9+
10+
from ..linear_model import spectraldiff, polydiff
11+
12+
13+
# Map from method -> (init_conds, type_low_hi)
14+
method_params_and_bounds = {
15+
spectraldiff: ({'even_extension': True,
16+
'pad_to_zero_dxdt': True,
17+
'high_freq_cutoff': [1e-3, 5e-2, 1e-2, 5e-2, 1e-1]},
18+
{'high_freq_cutoff': [1e-5, 1-1e-5]}),
19+
polydiff: ({'sliding': True,
20+
'step_size': 1,
21+
'kernel': 'friedrichs',
22+
'order': [2, 3, 5, 7],
23+
'window_size': [10, 30, 50, 90, 130]},
24+
{'order': [1, 8],
25+
'window_size': [10, 1000]})
26+
}
27+
28+
29+
# This function to be at the top level for multiprocessing
30+
def _objective_function(point, func, x, dt, singleton_params, search_space_types, dxdt_truth, metric,
31+
tvgamma, padding):
32+
"""Function minimized by scipy.optimize.minimize, needs to have the form: (point, *args) -> float
33+
This is mildly complicated, because "point" controls the settings of a differentiation function, but
34+
the method may have numerical and non-numerical parameters, and all such parameters are now passed by
35+
keyword arguments. So the encoded `point` has to be decoded to dict.
36+
37+
:param np.array point: a numerical vector scipy chooses to try in the objective function
38+
All other parameters documented in `optimize`
39+
40+
:return: float, cost of this objective at the point
41+
"""
42+
point_params = {k:(v if search_space_types[k] == float else
43+
int(np.round(v)) if search_space_types[k] == int else
44+
v > 0.5) for k,v in zip(search_space_types, point)} # point -> dict
45+
# add back in the singletons we're not searching over
46+
x_hat, dxdt_hat = func(x, dt, **point_params, **singleton_params) # estimate x and dxdt
47+
48+
# evaluate estimate
49+
if dxdt_truth is not None: # then minimize ||dxdt_hat - dxdt_truth||^2
50+
if metric == 'rmse':
51+
rms_rec_x, rms_x, rms_dxdt = evaluate.metrics(x, dt, x_hat, dxdt_hat, dxdt_truth=dxdt_truth, padding=padding)
52+
return rms_dxdt
53+
elif metric == 'error_correlation':
54+
return evaluate.error_correlation(dxdt_hat, dxdt_truth, padding=padding)
55+
else: # then minimize [ || integral(dxdt_hat) - x||^2 + gamma*TV(dxdt_hat) ]
56+
rms_rec_x, rms_x, rms_dxdt = evaluate.metrics(x, dt, x_hat, dxdt_hat, dxdt_truth=None, padding=padding)
57+
return rms_rec_x + tvgamma*evaluate.total_variation(dxdt_hat, padding=padding)
58+
59+
60+
def optimize(func, x, dt, init_conds={}, dxdt_truth=None, tvgamma=1e-2, padding='auto', metric='rmse',
61+
opt_method='Nelder-Mead', opt_kwargs={'maxiter': 10}):
62+
"""Find the optimal parameters for a given differentiation method.
63+
64+
:param function func: differentiation method to optimize parameters for, e.g. linear_model.savgoldiff
65+
:param np.array[float]: data to differentiate
66+
:param float dt: step size
67+
:param dict init_conds: function parameter settings to use as initial starting points in optimization,
68+
structured as :code:`{param1:[values], param2:[values], param3:value, ...}`. If left None,
69+
a default search space of initial values is used.
70+
:param np.array[float] dxdt_truth: actual time series of the derivative of x, if known
71+
:param float tvgamma: regularization value used to select for parameters that yield a smooth derivative.
72+
Larger value results in a smoother derivative
73+
:param int padding: number of time steps to ignore at the beginning and end of the time series in the
74+
optimization. Larger value causes the optimization to emphasize the accuracy of dxdt in the
75+
middle of the time series
76+
:param str metric: either :code:`'rmse'` or :code:`'error_correlation'`, only applies if :code:`dxdt_truth`
77+
is not None, see _objective_function
78+
:param str opt_method: Optimization technique used by :code:`scipy.minimize`, the workhorse
79+
:param dict opt_kwargs: keyword arguments to pass down to :code:`scipy.minimize`
80+
81+
:return: tuple[dict, float] of\n
82+
- **opt_params** -- best parameter settings for the differentation method
83+
- **opt_value** -- lowest value found for objective function
84+
"""
85+
if metric not in ['rmse','error_correlation']:
86+
raise ValueError('`metric` should either be `rmse` or `error_correlation`.')
87+
if metric == 'error_correlation' and dxdt_truth is None:
88+
raise ValueError('`metric` can only be `error_correlation` if `dxdt_truth` is given.')
89+
90+
params, bounds = method_params_and_bounds[func]
91+
params.update(init_conds) # for things not given, use defaults
92+
93+
# No need to optimize over singletons, just pass them through
94+
singleton_params = {k:v for k,v in params.items() if not isinstance(v, list)}
95+
96+
# The search space is the cartesian product of all dimensions where multiple options are given
97+
search_space_types = {k:type(v[0]) for k,v in params.items() if isinstance(v, list)} # for converting back and forth from point
98+
if any(v not in [float, int, bool] for v in search_space_types.values()):
99+
raise ValueError("Optimization over categorical strings currently not supported")
100+
# If excluding string type, I can just cast ints and bools to floats, and we're good to go
101+
search_space = product(*[np.array(params[k]).astype(float) for k in search_space_types]) #
102+
103+
bounds = [bounds[k] if k in bounds else # pass these to minimize(). It should respect them.
104+
(0, 1) if v == bool else
105+
None for k,v in search_space_types.items()]
106+
107+
# wrap the objective and scipy.optimize.minimize because the objective and options are always the same
108+
_obj_fun = partial(_objective_function, func=func, x=x, dt=dt, singleton_params=singleton_params,
109+
search_space_types=search_space_types, dxdt_truth=dxdt_truth, metric=metric, tvgamma=tvgamma,
110+
padding=padding)
111+
_minimize = partial(scipy.optimize.minimize, _obj_fun, method=opt_method, bounds=bounds, options=opt_kwargs)
112+
113+
with Pool() as pool: # The heavy lifting
114+
results = pool.map(_minimize, search_space) # returns a bunch of OptimizeResult objects
115+
116+
opt_idx = np.nanargmin([r.fun for r in results])
117+
opt_point = results[opt_idx].x
118+
# results are going to be floats, but that may not be allowed, so convert back to a dict
119+
opt_params = {k:(v if search_space_types[k] == float else
120+
int(np.round(v)) if search_space_types[k] == int else
121+
v > 0.5) for k,v in zip(search_space_types, opt_point)}
122+
opt_params.update(singleton_params)
123+
124+
return opt_params, results[opt_idx].fun

pynumdiff/optimize/finite_difference/__finite_difference__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
functions for optimizing finite difference
33
"""
44
import pynumdiff.finite_difference
5-
from pynumdiff.optimize.__optimize__ import __optimize__
5+
from pynumdiff import optimize
66

77

88
def first_order(x, dt, params=None, options={'iterate': True}, dxdt_truth=None,
@@ -30,6 +30,6 @@ def first_order(x, dt, params=None, options={'iterate': True}, dxdt_truth=None,
3030

3131
# optimize
3232
args = [func, x, dt, params_types, params_low, params_high, options, dxdt_truth, tvgamma, padding, metric]
33-
opt_params, opt_val = __optimize__(params, args)
33+
opt_params, opt_val = optimize(params, args)
3434

3535
return opt_params, opt_val

0 commit comments

Comments
 (0)