Skip to content

Commit bf6dd04

Browse files
committed
working on #68 for the _linear_model file. In the process I discovered an oddity that has spawned #92
1 parent b171ac5 commit bf6dd04

1 file changed

Lines changed: 117 additions & 161 deletions

File tree

pynumdiff/linear_model/_linear_model.py

Lines changed: 117 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,24 @@
1717
####################
1818
# Helper functions #
1919
####################
20-
def __slide_function__(func, x, dt, params, window_size, step_size, kernel_name, solver=None):
21-
"""
22-
Slide a smoothing derivative function across a timeseries with specified window size.
23-
24-
:param func: (function) name of the function to slide
25-
:param x: (np.array of floats, 1xN) time series to differentiate
26-
:param dt: (float) time step
27-
:param params: (list) see func for requirements
28-
:param window_size: (int) size of the sliding window
29-
:param step_size: (int) step size for slide (e.g. 1 means slide by 1 step)
30-
:param kernel_name: (string) name of the smoothing kernel (e.g. 'friedrichs' or 'gaussian')
31-
:return: x_hat : estimated (smoothed) x
32-
dxdt_hat : estimated derivative of x
20+
def _slide_function(func, x, dt, args, window_size, step_size, kernel_name):
21+
"""Slide a smoothing derivative function across a timeseries with specified window size.
22+
23+
:param callable func: name of the function to slide
24+
:param np.array[float] x: time series to differentiate
25+
:param float dt: time step
26+
:param dict args: see func for requirements
27+
:param int window_size: size of the sliding window
28+
:param int step_size: step size for slide (e.g. 1 means slide by 1 step)
29+
:param str kernel_name: name of the smoothing kernel (e.g. 'friedrichs' or 'gaussian')
30+
31+
:return: tuple[np.array, np.array] of\n
32+
- **x_hat** -- estimated (smoothed) x
33+
- **dxdt_hat** -- estimated derivative of x
3334
"""
3435

3536
# get smoothing kernel
36-
if not window_size % 2: # then make odd
37+
if not window_size % 2: # then make odd
3738
window_size += 1
3839
ker = KERNELS[kernel_name](window_size)
3940

@@ -62,7 +63,7 @@ def __slide_function__(func, x, dt, params, window_size, step_size, kernel_name,
6263

6364
# run the function on the window
6465
_x = x[start:end]
65-
x_hat, dxdt_hat = func(_x, dt, params, options={'weights': w, 'solver': solver})
66+
x_hat, dxdt_hat = func(_x, dt, *args, weights=w)
6667

6768
# stack results
6869
z_x_hat = np.zeros([len(x)])
@@ -150,97 +151,70 @@ def savgoldiff(x, dt, params, options=None):
150151
######################
151152
# Polynomial fitting #
152153
######################
153-
def __polydiff__(x, dt, params, options=None):
154-
"""
155-
Fit polynomials to the timeseries, and differentiate the polynomials.
154+
def _polydiff(x, dt, polynomial_order, weights=None):
155+
"""Fit polynomials to the timeseries, and differentiate the polynomials.
156156
157-
:param x: (np.array of floats, 1xN) time series to differentiate
158-
:param dt: (float) time step
159-
:param params: (list) [N] : (int) order of the polynomial
160-
:param options:(dict) {'weights'} : (np.array, optional) weights applied to each point in calculating the
161-
polynomial fit. Defaults to 1s if missing.
162-
163-
:return: x_hat : estimated (smoothed) x
164-
dxdt_hat : estimated derivative of x
157+
:param np.array[float] x: time series to differentiate
158+
:param float dt: time step
159+
:param int polynomial_order: order of the polynomial
160+
:param np.array[float] weights: weights applied to each point in calculating the polynomial fit.
161+
Defaults to 1s if missing.
165162
163+
:return: tuple[np.array, np.array] of\n
164+
- **x_hat** -- estimated (smoothed) x
165+
- **dxdt_hat** -- estimated derivative of x
166166
"""
167-
if isinstance(options, dict) and 'weights' in options.keys():
168-
w = options['weights']
169-
else:
170-
w = np.ones_like(x)
171-
172-
if isinstance(params, list):
173-
order = params[0]
174-
else:
175-
order = params
167+
if weights is None:
168+
weights = np.ones(x.shape)
176169

177170
t = np.arange(1, len(x)+1)*dt
178171

179-
# polyfit
180-
r = np.polyfit(t, x, order, w=w)[::-1]
172+
r = np.polyfit(t, x, polynomial_order, w=weights) # polyfit returns highest order first
173+
dr = np.polyder(r) # power rule already implemented for us
181174

182-
# derivative coefficients
183-
dr = copy.copy(r[1:])
184-
for i, _ in enumerate(dr):
185-
dr[i] = dr[i]*(i + 1)
186-
187-
# evaluate dxdt_hat
188-
dxdt_hat = 0
189-
for i, _ in enumerate(dr):
190-
dxdt_hat += dr[i]*t**i
191-
192-
# evaluate smooth x
193-
x_hat = 0
194-
for i, _ in enumerate(r):
195-
x_hat += r[i]*t**i
175+
dxdt_hat = np.polyval(dr, t) # evaluate the derivative and original polynomials at points t
176+
x_hat = np.polyval(r, t) # smoothed x
196177

197178
return x_hat, dxdt_hat
198179

199180

200-
def polydiff(x, dt, params, options=None):
201-
"""
202-
Fit polynomials to the time series, and differentiate the polynomials.
181+
def polydiff(x, dt, params=None, options=None, polynomial_order=None, window_size=None,
182+
sliding=True, step_size=1, kernel='friedrichs'):
183+
"""Fit polynomials to the time series, and differentiate the polynomials.
203184
204-
:param x: array of time series to differentiate
205-
:type x: np.array (float)
206-
207-
:param dt: time step size
208-
:type dt: float
209-
210-
:param params: a list of 2 elements:
211-
212-
- N: order of the polynomial
213-
- window_size: size of the sliding window (ignored if not sliding)
214-
215-
:type params: list (int)
216-
217-
:param options: a dictionary consisting of 3 key value pairs:
218-
219-
- 'sliding': whether to use sliding approach
220-
- 'step_size': step size for sliding
221-
- 'kernel_name': kernel to use for weighting and smoothing windows ('gaussian' or 'friedrichs')
222-
223-
:type options: dict {'sliding': (bool), 'step_size': (int), 'kernel_name': (string)}, optional
224-
225-
:return: a tuple consisting of:
226-
227-
- x_hat: estimated (smoothed) x
228-
- dxdt_hat: estimated derivative of x
185+
:param np.array[float] x: array of time series to differentiate
186+
:param float dt: time step size
187+
:param list[int] params: (**deprecated**, prefer :code:`polynomial_order` and :code:`window_size`)
188+
:param dict options: (**deprecated**, prefer :code:`sliding`, :code:`step_size`, and :code:`kernel`)
189+
a dictionary consisting of {'sliding': (bool), 'step_size': (int), 'kernel_name': (str)}
190+
:param int polynomial_order: order of the polynomial
191+
:param int window_size: size of the sliding window (ignored if not sliding)
192+
:param bool sliding: whether to use sliding approach
193+
:param int step_size: step size for sliding
194+
:param str kernel: kernel to use for weighting and smoothing windows ('gaussian' or 'friedrichs')
229195
230-
:rtype: tuple -> (np.array, np.array)
196+
:return: tuple[np.array, np.array] of\n
197+
- **x_hat** -- estimated (smoothed) x
198+
- **dxdt_hat** -- estimated derivative of x
231199
"""
200+
if params != None:
201+
warn("""`params` and `options` parameters will be removed in a future version. Use `polynomial_order`
202+
and `window_size` instead.""", DeprecationWarning)
203+
polynomial_order, window_size = params
204+
if options != None:
205+
if 'sliding' in options: sliding = options['sliding']
206+
if 'step_size' in options: step_size = options['step_size']
207+
if 'kernel_name' in options: kernel = options['kernel_name']
208+
elif polynomial_order == None or window_size == None:
209+
raise ValueError("`polynomial_order` and `window_size` must be given.")
232210

233-
if options is None:
234-
options = {'sliding': True, 'step_size': 1, 'kernel_name': 'friedrichs'}
211+
if window_size < polynomial_order*3:
212+
window_size = polynomial_order*3+1
235213

236-
if 'sliding' in options.keys() and options['sliding'] is True:
237-
window_size = copy.copy(params[-1])
238-
if window_size < params[0]*3:
239-
window_size = params[0]*3+1
240-
params[1] = window_size
241-
return __slide_function__(__polydiff__, x, dt, params, window_size, options['step_size'], options['kernel_name'])
214+
if sliding:
215+
return _slide_function(_polydiff, x, dt, [polynomial_order], window_size, step_size, kernel)
242216

243-
return __polydiff__(x, dt, params, options={})
217+
return _polydiff(x, dt, polynomial_order)
244218

245219

246220
#############
@@ -402,25 +376,23 @@ def __integrate_dxdt_hat_matrix__(dxdt_hat, dt):
402376
return x
403377

404378

405-
def __lineardiff__(x, dt, params, options=None):
406-
"""
407-
Estimate the parameters for a system xdot = Ax, and use that to calculate the derivative
408-
409-
:param x: (np.array of floats, 1xN) time series to differentiate
410-
:param dt: (float) time step
411-
:param params: (list) [N, : (int, >1) order (e.g. 2: velocity; 3: acceleration)
412-
gamma] : (float) regularization term
413-
:return: x_hat : estimated (smoothed) x
414-
dxdt_hat : estimated derivative of x
415-
"""
416-
if options is None:
417-
options = {'solver': 'MOSEK'}
379+
def _lineardiff(x, dt, N, gamma, solver='MOSEK', weights=None):
380+
"""Estimate the parameters for a system xdot = Ax, and use that to calculate the derivative
418381
419-
N, gamma = params
382+
:param np.array[float] x: time series to differentiate
383+
:param float dt: time step
384+
:param int > 1 N: order (e.g. 2: velocity; 3: acceleration)
385+
:param float gamma: regularization term
386+
:param np.array[float] weights: Bug? Currently not used here, although `lineardiff` takes a kernel
387+
:param str solver: which cvxpy solver to use
388+
389+
:return: tuple[np.array, np.array] of\n
390+
- **x_hat** -- estimated (smoothed) x
391+
- **dxdt_hat** -- estimated derivative of x
392+
"""
420393
mean = np.mean(x)
421394
x = x - mean
422395

423-
424396
# Generate the matrix of integrals of x
425397
X = [x]
426398
for n in range(1, N):
@@ -430,7 +402,7 @@ def __lineardiff__(x, dt, params, options=None):
430402
integral_X = __integrate_dxdt_hat_matrix__(X, dt)
431403

432404
# Solve for A and the integration constants
433-
A, C = __solve_for_A_and_C_given_X_and_Xdot__(integral_X, integral_Xdot, N, dt, gamma, solver=options['solver'])
405+
A, C = __solve_for_A_and_C_given_X_and_Xdot__(integral_X, integral_Xdot, N, dt, gamma, solver=solver)
434406

435407
# Add the integration constants
436408
Csum = 0
@@ -453,61 +425,45 @@ def __lineardiff__(x, dt, params, options=None):
453425
return x_hat, dxdt_hat
454426

455427

456-
def lineardiff(x, dt, params, options=None):
457-
"""
458-
Slide a smoothing derivative function across a time series with specified window size.
459-
460-
:param x: array of time series to differentiate
461-
:type x: np.array (float)
462-
463-
:param dt: time step size
464-
:type dt: float
465-
466-
:param params: a list of 3 elements:
428+
def lineardiff(x, dt, params=None, options=None, polynomial_order=None, gamma=None, window_size=None,
429+
sliding=True, step_size=10, kernel='friedrichs', solver='MOSEK'):
430+
"""Slide a smoothing derivative function across a time series with specified window size.
467431
468-
- N: order of the polynomial
469-
- gamma: regularization term
470-
- window_size: size of the sliding window (ignored if not sliding)
471-
472-
:type params: list (int, float, int)
473-
474-
:param options: a dictionary consisting of 3 key value pairs:
475-
476-
- 'sliding': whether to use sliding approach
477-
- 'step_size': step size for sliding
478-
- 'kernel_name': kernel to use for weighting and smoothing windows ('gaussian' or 'friedrichs')
479-
480-
:type options: dict {'sliding': (bool), 'step_size': (int), 'kernel_name': (string)}, optional
481-
482-
:return: a tuple consisting of:
483-
484-
- x_hat: estimated (smoothed) x
485-
- dxdt_hat: estimated derivative of x
432+
:param np.array[float] x: array of time series to differentiate
433+
:param float dt: time step size
434+
:param list[int, float, int] params: (**deprecated**, prefer :code:`polynomial_order`, :code:`gamma`, and :code:`window_size`)
435+
:param dict options: (**deprecated**, prefer :code:`sliding`, :code:`step_size`, :code:`kernel`, and :code:`solver`
436+
a dictionary consisting of {'sliding': (bool), 'step_size': (int), 'kernel_name': (str), 'solver': (str)}
437+
:param int polynomial_order: order of the polynomial
438+
:param float gamma: regularization term
439+
:param int window_size: size of the sliding window (ignored if not sliding)
440+
:param bool sliding: whether to use sliding approach
441+
:param int step_size: step size for sliding
442+
:param str kernel_name: kernel to use for weighting and smoothing windows ('gaussian' or 'friedrichs')
443+
:param str solver: CVXPY solver to use, one of :code:`cvxpy.installed_solvers()`
486444
487-
:rtype: tuple -> (np.array, np.array)
445+
:return: tuple[np.array, np.array] of\n
446+
- **x_hat** -- estimated (smoothed) x
447+
- **dxdt_hat** -- estimated derivative of x
488448
"""
489-
490-
if options is None:
491-
options = {'sliding': True, 'step_size': 10, 'kernel_name': 'friedrichs', 'solver': 'MOSEK'}
492-
493-
if 'sliding' not in options.keys():
494-
options['sliding'] = True
495-
if 'step_size' not in options.keys():
496-
options['step_size'] = 10
497-
if 'kernel_name' not in options.keys():
498-
options['kernel_name'] = 'friedrichs'
499-
if 'solver' not in options.keys():
500-
options['solver'] = 'MOSEK'
501-
502-
if 'sliding' in options.keys() and options['sliding'] is True:
503-
window_size = copy.copy(params[-1])
504-
params = params[0:-1]
505-
449+
if params != None:
450+
warn("""`params` and `options` parameters will be removed in a future version. Use `polynomial_order`,
451+
`gamma`, and `window_size` instead.""", DeprecationWarning)
452+
polynomial_order, gamma, window_size = params
453+
if options != None:
454+
if 'sliding' in options: sliding = options['sliding']
455+
if 'step_size' in options: step_size = options['step_size']
456+
if 'kernel_name' in options: kernel = options['kernel_name']
457+
if 'solver' in options: solver = options['solver']
458+
elif polynomial_order == None or gamma == None or window_size == None:
459+
raise ValueError("`polynomial_order`, `gamma`, and `window_size` must be given.")
460+
461+
if sliding:
506462
# forward and backward
507-
x_hat_forward, _ = __slide_function__(__lineardiff__, x, dt, params, window_size, options['step_size'],
508-
options['kernel_name'], options['solver'])
509-
x_hat_backward, _ = __slide_function__(__lineardiff__, x[::-1], dt, params, window_size, options['step_size'],
510-
options['kernel_name'], options['solver'])
463+
x_hat_forward, _ = _slide_function(_lineardiff, x, dt, [polynomial_order, gamma, solver], window_size, step_size,
464+
kernel)
465+
x_hat_backward, _ = _slide_function(_lineardiff, x[::-1], dt, [polynomial_order, gamma, solver], window_size, step_size,
466+
kernel)
511467

512468
# weights
513469
w = np.arange(1, len(x_hat_forward)+1,1)[::-1]
@@ -525,7 +481,7 @@ def lineardiff(x, dt, params, options=None):
525481

526482
return x_hat, dxdt_hat
527483

528-
return __lineardiff__(x, dt, params, options)
484+
return _lineardiff(x, dt, params, options)
529485

530486
###############################
531487
# Fourier Spectral derivative #
@@ -549,11 +505,11 @@ def spectraldiff(x, dt, params=None, options=None, high_freq_cutoff=None, even_e
549505
"""
550506
if params != None: # Warning to support old interface for a while. Remove these lines along with params in a future release.
551507
warn("""`params` and `options` parameters will be removed in a future version. Use `high_freq_cutoff`,
552-
`even_extension`, and `pad_to_zero_dxdt` instead.""")
553-
if options is None:
554-
even_extension = True
555-
pad_to_zero_dxdt = True
508+
`even_extension`, and `pad_to_zero_dxdt` instead.""", DeprecationWarning)
556509
high_freq_cutoff = params[0] if isinstance(params, list) else params
510+
if options != None:
511+
if 'even_extension' in options: high_freq_cutoff = options['even_extension']
512+
if 'pad_to_zero_dxdt' in options: pad_to_zero_dxdt = options['pad_to_zero_dxdt']
557513
elif high_freq_cutoff == None:
558514
raise ValueError("`high_freq_cutoff` must be given.")
559515

0 commit comments

Comments
 (0)