-
-
Save pukpr/0b7ac85fad1ea36f65a9b50d6c30958b to your computer and use it in GitHub Desktop.
| #!/usr/bin/env python3 | |
| """ | |
| timeseries_modeler.py | |
| Read a two-column CSV (time, value). Read a JSON file with the same filename | |
| appended by ".p" (e.g. data.csv.p) into a data dictionary. Create a clone of the | |
| time-series and run a loop that steps through timestamps, calling a user- | |
| customizable model-step function to produce a modeled series. At the end the | |
| script computes Pearson's correlation coefficient and the variance of the | |
| squared errors (and also MSE), using library routines. | |
| Usage: | |
| python timeseries_modeler.py data.csv [--out fitted.csv] [--plot] | |
| python3 timeseries_modeler.py 11a.csv --plot | |
| Notes: | |
| - The CSV must have at least two columns (time, value). Header is tolerated. | |
| - Model is: | |
| - impulse every Imp_Stride rows | |
| - C_t = Imp_Amp * impulse_mask * sum_k PeriodsAmp[k] * sin(2*pi*time/Period_k + PeriodsPhase_k) | |
| - D_t = (1-Hold)*D_{t-1} + Hold*C_t | |
| - E_t = LTE_Amp * sin(D_t*LTE_Freq + LTE_Phase) | |
| Provide model parameters via --params as a JSON string (see defaults below). | |
| - The implementation loops over timestamps (explicit for-loop) so the model can | |
| be stateful (sample-and-hold needs previous D). | |
| - Pearson correlation computed using scipy.stats.pearsonr if available, | |
| otherwise a fallback via numpy.corrcoef is used. | |
| - Outputs: | |
| - CSV with columns: time, observed, model, residual | |
| - JSON .p file updated with metadata (metrics, parameters used) | |
| Dependencies: | |
| numpy, pandas, scipy (optional but recommended for Pearson), matplotlib (optional for plotting) | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import math | |
| import os | |
| from typing import Any, Dict, List, Tuple, Callable, Optional | |
| import copy | |
| import numpy as np | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| from scipy.stats import pearsonr | |
| import random | |
| from sinusoidal_regression import fit_sinusoidal_regression # use the provided module | |
| TWOPI = 2.0 * math.pi | |
| use_pearson = False | |
| use_random = False | |
| time_series_name = "none" | |
| def read_two_column_csv(path: str) -> Tuple[np.ndarray, np.ndarray]: | |
| """Read CSV-like file with two columns: time, value. Tolerant to header rows.""" | |
| df = pd.read_csv(path, sep=r'[,\s]+', engine='python', header=0) | |
| if df.shape[1] < 2: | |
| raise ValueError("Input CSV must have at least two columns (time, value).") | |
| t = pd.to_numeric(df.iloc[:, 0], errors='coerce') | |
| y = pd.to_numeric(df.iloc[:, 1], errors='coerce') | |
| mask = (~t.isna()) & (~y.isna()) | |
| t = t[mask].to_numpy(dtype=float) | |
| y = y[mask].to_numpy(dtype=float) | |
| return t, y | |
| def read_json_p(path: str) -> Dict[str, Any]: | |
| """Read JSON file at path (if exists), else return empty dict.""" | |
| if not os.path.exists(path): | |
| return {} | |
| with open(path, 'r', encoding='utf-8') as fh: | |
| return json.load(fh) | |
| def write_json_p(path: str, data: Dict[str, Any]) -> None: | |
| """Write JSON dict to file path.""" | |
| with open(path, 'w', encoding='utf-8') as fh: | |
| json.dump(data, fh, indent=2, sort_keys=True) | |
| def clone_series(series: np.ndarray) -> np.ndarray: | |
| """Return a deep copy (clone) of the series.""" | |
| return np.array(series, copy=True) | |
| def normalize_rms_y(ts: np.ndarray) -> np.ndarray: | |
| """ | |
| Return a copy of ts where the y-values (column 1) are scaled so their RMS == 1.0. | |
| No error checking (assumes ts is a 2D numpy array with at least 2 columns). | |
| """ | |
| y = ts.copy() | |
| rms = np.sqrt(np.mean(y * y)) | |
| return y / rms | |
| # ---- Model-step interface --------------------------------------------------- | |
| # A model-step function implements: | |
| # model_value, state = model_step(i, t_i, clone_i, state, params) | |
| # Where: | |
| # i: integer time index (0..N-1) | |
| # t_i: timestamp value (float) | |
| # clone_i: cloned original series value at index i (float) | |
| # state: dict carrying model internal state (mutable or replaced) | |
| # params: dict of user parameters | |
| # | |
| # The runner will set up initial state = {} and call model_step for each i. | |
| # --------------------------------------------------------------------------- | |
| def model_step_algorithm(i: int, t_i: float, clone_i: float, state: Dict[str, Any], | |
| Initial: float, | |
| Year: float, | |
| Imp_Stride: int, | |
| Imp_Amp: float, | |
| Periods: list, | |
| PeriodsAmp: list, | |
| PeriodsPhase: list, | |
| Aliased: list, | |
| AliasedAmp: list, | |
| AliasedPhase: list, | |
| Hold: float, | |
| Delta_Y: float, | |
| Imp_Amp2: float, | |
| Damp: Float): | |
| """ | |
| Implements the algorithm-style model described: | |
| - Impulse mask every Imp_Stride rows (row numbering starts at 1) | |
| - C_t = (Imp_Amp if (row_idx % Imp_Stride == 1) else 0) * SUM_k PeriodsAmp[k]*sin(2*pi * t / Period_k + PeriodsPhase[k]) | |
| - D_t = (1-Hold)*D_{t-1} + Hold*C_t (initialize D_0 = Hold*C_0) | |
| - E_t = LTE_Amp * sin(D_t * LTE_Freq + LTE_Phase) | |
| Required params (defaults provided): | |
| - Imp_Stride: int (default 1) | |
| - Imp_Amp: float (default 1.0) | |
| - Periods: list of floats (default [1.0]) | |
| - PeriodsAmp: list of floats (len match Periods, default [1.0]) | |
| - PeriodsPhase: list of floats (len match Periods, default [0.0]) | |
| - Hold: float in [0,1] (default 0.5) | |
| - LTE_Amp: float (default 1.0) | |
| - LTE_Freq: float (default 1.0) | |
| - LTE_Phase: float (default 0.0) | |
| """ | |
| # compute sum of sinusoids | |
| ssum = 0.0 | |
| for amp, per, ph in zip(PeriodsAmp, Periods, PeriodsPhase): | |
| per = float(per) | |
| if per == 0.0: | |
| continue | |
| ssum += float(amp) * math.sin(TWOPI * float(t_i) * (Year+Delta_Y) / per + float(ph)) | |
| # if os.getenv('FORCING', '') == '1': | |
| # state['D_prev'] = 0.0 | |
| # return ssum, state, 0.0 | |
| # row index as 1..N (i is 0..N-1) | |
| row_idx = i + 2 | |
| impulse = 1.0 if (row_idx % 12 == Imp_Stride) else 0.0 | |
| if os.getenv('SEMI', '') == '1': | |
| impulse2 = 1.0 if (row_idx % 12 == (Imp_Stride+6) % 12) else 0.0 | |
| else: | |
| impulse2 = 1.0 if (row_idx % 24 == Imp_Stride) else 0.0 | |
| C_t = (impulse * Imp_Amp + impulse2 * Imp_Amp2) * ssum * math.exp(-Damp * abs(ssum)) | |
| # stateful D | |
| if 'D_prev' not in state: | |
| # initialize as Hold * C_0 (algorithm initialization) | |
| state['D_prev'] = Initial | |
| D_prev = float(state['D_prev']) | |
| D_t = (1.0 - Hold) * D_prev + Hold * C_t | |
| state['D_prev'] = D_t | |
| # compute sum of aliases | |
| ssum = 0.0 | |
| for amp, freq, ph in zip(AliasedAmp, Aliased, AliasedPhase): | |
| freq = float(freq) | |
| ssum += float(amp) * math.sin(TWOPI * float(t_i) * freq + float(ph)) | |
| E_t = D_t | |
| return float(E_t), state, ssum | |
| def build_mask(time, Time_Low: Float, Time_High: Float, Exclude:bool): | |
| # Build mask for points to INCLUDE in the metric (True => include) | |
| if Time_Low is None and Time_High is None: | |
| mask = np.ones_like(time, dtype=bool) | |
| else: | |
| # If one bound is missing, treat it as open-ended | |
| if Time_Low is None: | |
| # exclude times <= Time_High | |
| mask = time > float(Time_High) | |
| elif Time_High is None: | |
| # exclude times >= Time_Low | |
| mask = time < float(Time_Low) | |
| else: | |
| # ensure bounds are ordered | |
| tl = float(Time_Low) | |
| th = float(Time_High) | |
| if tl > th: | |
| tl, th = th, tl | |
| # include times strictly outside [tl, th] | |
| if Exclude: | |
| mask = (time < tl) | (time > th) | |
| else: | |
| mask = (time > tl) & (time < th) | |
| return mask | |
| # Runner that loops through timestamps and produces modeled series ---------------- | |
| def run_loop_time_series(time: np.ndarray, | |
| observed: np.ndarray, | |
| model_step_fn: Callable, | |
| params: Dict[str, Any], mask) -> Tuple[np.ndarray, Dict[str, Any]]: | |
| """ | |
| Loop over timestamps and compute model values using model_step_fn. | |
| Returns (model_array, final_state). | |
| """ | |
| # Prepare and normalize parameters | |
| DeltaTime = float(params.get('DeltaTime', 0.0)) | |
| Initial = float(params.get('Initial', 0.04294)) | |
| Year = float(params.get('Year', 365.242)) | |
| Imp_Stride = int(params.get('Imp_Stride', 12)) | |
| Imp_Amp = float(params.get('Imp_Amp', 1.0)) | |
| Periods = list(params.get('Periods', [1.0])) | |
| PeriodsAmp = list(params.get('PeriodsAmp', [1.0] * len(Periods))) | |
| PeriodsPhase = list(params.get('PeriodsPhase', [0.0] * len(Periods))) | |
| Aliased = list(params.get('Aliased', [1.0])) | |
| AliasedAmp = list(params.get('AliasedAmp', [1.0] * len(Aliased))) | |
| AliasedPhase = list(params.get('AliasedPhase', [0.0] * len(Aliased))) | |
| Hold = float(params.get('Hold', 0.5)) | |
| LTE_Freq = float(params.get('LTE_Freq', 1.0)) | |
| Delta_Y = float(params.get('Delta_Y', 0.0)) | |
| Imp_Amp2 = float(params.get('Imp_Amp2', 0.0)) | |
| Damp = float(params.get('Damp', 0.0)) | |
| Harmonics = list(params.get('Harmonics', [1])) | |
| DC = float(params.get('DC', 0.0)) | |
| Scale = float(params.get('Scale', 1.0)) | |
| N = time.size | |
| clone = clone_series(observed) | |
| state: Dict[str, Any] = {} | |
| model = np.zeros_like(observed, dtype=float) | |
| model_sup = np.zeros_like(observed, dtype=float) | |
| First_Time = True | |
| Last_Time = 0.0 | |
| for i in range(N): | |
| t_i = float(time[i]+DeltaTime) | |
| clone_i = float(clone[i]) | |
| # should be 1/12 but may be missing values | |
| N_Steps = int(round((t_i - Last_Time) * 12)) | |
| N_Steps = 1 if First_Time else N_Steps | |
| First_Time = False | |
| j = 1 | |
| while j <= N_Steps: | |
| time_i = float(t_i - (N_Steps-j)/12.0) | |
| v, state, sup = model_step_fn(i, time_i, clone_i, state, Initial, Year, Imp_Stride,Imp_Amp, Periods, PeriodsAmp, PeriodsPhase, | |
| Aliased, AliasedAmp, AliasedPhase, Hold, Delta_Y, Imp_Amp2, Damp) | |
| j = j + 1 | |
| model[i] = float(v) | |
| model_sup[i] = float(sup) | |
| Last_Time = t_i | |
| if os.getenv('FORCING', '') == '1': | |
| model1 = Scale*model - DC | |
| return model1, state | |
| clone = clone - model_sup | |
| mask_model = model[mask] | |
| mask_clone = clone[mask] | |
| #if m_model.size != m_model.size: | |
| # print("mask sizes match") | |
| # print(f"{m_model.size:d} {model.size:d}") | |
| # exit() | |
| lte = fit_sinusoidal_regression(mask_model, mask_clone, N_list=Harmonics, k=LTE_Freq, intercept=True, add_linear_x=True, ridge=None) | |
| model1 = lte["predict"](model) + model_sup | |
| return model1, state, model | |
| # Metrics ----------------------------------------------------------------------- | |
| def compute_metrics(time: np.ndarray, observed: np.ndarray, model: np.ndarray, low: float, high:float) -> Dict[str, Any]: | |
| """Compute Pearson correlation, residuals, MSE, and variance of squared errors.""" | |
| residuals = model - observed | |
| mse = float(np.mean(residuals ** 2)) | |
| # variance of squared errors (i.e., variance of residuals**2) | |
| var_sq_err = float(np.var(residuals ** 2)) | |
| # Pearson r (use scipy if available else numpy) | |
| CV = 1.0 - compute_metrics_region(time, observed, model, low, high, False) | |
| try: | |
| r_val, p_val = pearsonr(observed, model) # type: ignore | |
| pearson_r = float(r_val) | |
| pearson_p = float(p_val) | |
| except Exception: | |
| pearson_r = float(np.nan) | |
| pearson_p = float(np.nan) | |
| return { | |
| 'residuals': residuals, | |
| 'mse': mse, | |
| 'var_squared_error': var_sq_err, | |
| 'pearson_r': pearson_r, | |
| 'pearson_p': pearson_p, | |
| 'CV': CV | |
| } | |
| def compute_metrics_scalar(observed: np.ndarray, model: np.ndarray) -> Float: | |
| if False: | |
| """Compute CC""" | |
| r_val, p_val = pearsonr(observed, model) # type: ignore | |
| pearson_r = 1.0-float(r_val) | |
| return pearson_r | |
| else: | |
| """Compute MSE""" | |
| residuals = model - observed | |
| mse = float(np.mean(residuals ** 2)) | |
| return mse | |
| def compute_metrics_region(time: np.ndarray, | |
| observed: np.ndarray, | |
| model: np.ndarray, | |
| Time_Low: Optional[float] = None, | |
| Time_High: Optional[float] = None, | |
| Exclude: Optional[bool] = True) -> float: | |
| """ | |
| Compute a scalar metric between observed and model. By default computes MSE. | |
| If Time_Low and Time_High are provided, values with Time_Low <= time <= Time_High | |
| are excluded from the calculation (useful for cross-validation / holdout). | |
| Minimal behavior, no heavy validation: | |
| - If both Time_Low and Time_High are None the full series is used. | |
| - If only one is provided it is treated as an open bound (i.e. exclude | |
| times >= Time_Low if Time_High is None, or <= Time_High if Time_Low is None). | |
| - If Time_Low > Time_High they are swapped. | |
| - If all points are excluded, returns float('nan'). | |
| Parameters | |
| - time: 1D array of time values (same length as observed/model) | |
| - observed: observed values | |
| - model: model values | |
| - Time_Low: lower bound (inclusive) of the excluded middle region, or None | |
| - Time_High: upper bound (inclusive) of the excluded middle region, or None | |
| Returns | |
| - scalar metric (MSE by default) | |
| """ | |
| # Build mask for points to INCLUDE in the metric (True => include) | |
| mask = build_mask(time, Time_Low, Time_High, Exclude) | |
| """ | |
| if Time_Low is None and Time_High is None: | |
| mask = np.ones_like(time, dtype=bool) | |
| else: | |
| # If one bound is missing, treat it as open-ended | |
| if Time_Low is None: | |
| # exclude times <= Time_High | |
| mask = time > float(Time_High) | |
| elif Time_High is None: | |
| # exclude times >= Time_Low | |
| mask = time < float(Time_Low) | |
| else: | |
| # ensure bounds are ordered | |
| tl = float(Time_Low) | |
| th = float(Time_High) | |
| if tl > th: | |
| tl, th = th, tl | |
| # include times strictly outside [tl, th] | |
| if Exclude: | |
| mask = (time < tl) | (time > th) | |
| else: | |
| mask = (time > tl) & (time < th) | |
| """ | |
| # Select data | |
| t_sel = time[mask] | |
| obs_sel = observed[mask] | |
| mod_sel = model[mask] | |
| # If nothing remains, return NaN | |
| if obs_sel.size == 0: | |
| return float('nan') | |
| # ---- choose metric ---- | |
| # The original snippet had an unused pearson branch; keep MSE as default. | |
| # If you want Pearson-based metric, set use_pearson = True. | |
| global use_pearson | |
| if use_pearson: | |
| from scipy.stats import pearsonr # type: ignore | |
| r_val, _ = pearsonr(obs_sel, mod_sel) | |
| pearson_r = 1.0 - float(r_val) | |
| return pearson_r | |
| else: | |
| residuals = mod_sel - obs_sel | |
| mse = float(np.mean(residuals ** 2)) | |
| return mse | |
| # Opt --------------------------------------------------------------------------- | |
| Monitored = [ | |
| "Imp_Stride", | |
| "DeltaTime", | |
| "Initial", | |
| "Year", | |
| "Imp_Amp", | |
| "Imp_Amp2", | |
| "PeriodsAmp", | |
| "PeriodsPhase", | |
| "AliasedAmp", | |
| "AliasedPhase", | |
| "Hold", | |
| "Delta_Y", | |
| "Damp", | |
| "Harmonics" | |
| ] | |
| Monitored_Slide = [ | |
| "LTE_Freq", | |
| "Imp_Amp", | |
| "Imp_Amp2", | |
| "Imp_Stride", | |
| "DeltaTime", | |
| "Initial", | |
| "PeriodsPhase", | |
| "AliasedPhase", | |
| "Damp", | |
| "DC", | |
| "Scale" | |
| ] | |
| Monitored_Initial = [ | |
| "Imp_Stride", | |
| "DeltaTime", | |
| "Initial" | |
| ] | |
| def _is_list_param(name: str) -> bool: | |
| return name in { | |
| "Periods", | |
| "PeriodsAmp", | |
| "PeriodsPhase", | |
| "Aliased", | |
| "AliasedAmp", | |
| "AliasedPhase" | |
| } | |
| def simple_descent( | |
| time, | |
| observed, | |
| model_step_fn: Callable, | |
| params: Dict[str, Any], | |
| metric_fn: Callable[[Any, Any, Any, float, float], float], | |
| step: float = 0.01, | |
| max_iters: int = 100, | |
| tol: float = 1e-12, | |
| verbose: bool = False, | |
| Time_Low: Float = 1920.0, | |
| Time_High: Float = 1950 | |
| ) -> Dict[str, Any]: | |
| """ | |
| Simple descent optimizer. | |
| Args: | |
| - time, observed, model_step_fn: as in run_loop_time_series | |
| - params: dict with the monitored parameters (scalars and lists of floats) | |
| - metric_fn: function(observed, model_array) -> scalar (smaller is better) | |
| - step: base step size used for +/- changes (applied to scalars and list elements) | |
| - max_iters: max number of outer passes over all parameters | |
| - tol: minimum improvement to consider (metric must decrease by > tol) | |
| - verbose: print progress | |
| Returns dict: | |
| - best_params: dict of accepted parameter values | |
| - best_metric: scalar | |
| - best_model: model array for best params | |
| - best_state: final state from run_loop_time_series for best params | |
| - history: list of evaluated candidates: dicts with keys | |
| ('param', 'index' (or None), 'candidate', 'metric', 'accepted') | |
| - iterations: number of outer iterations performed | |
| """ | |
| if os.getenv('ALIGN', '') == '1': | |
| Monitored_Parameters = Monitored_Slide | |
| Magnify_Scalar = 10.0 | |
| elif os.getenv('ALIGN', '') == '-1': | |
| Monitored_Parameters = Monitored_Initial | |
| Magnify_Scalar = 1.0 | |
| else: | |
| Monitored_Parameters = Monitored | |
| Magnify_Scalar = 1.0 | |
| # start from given params (copy to avoid mutating caller's dict) | |
| best_params = copy.deepcopy(params) | |
| # ensure list parameters are plain python lists of floats (if present) | |
| for name in Monitored_Parameters: | |
| if _is_list_param(name) and name in best_params: | |
| best_params[name] = [float(x) for x in best_params[name]] | |
| mask = build_mask(time, Time_Low, Time_High, False) | |
| # evaluate initial | |
| model_vals, final_state, _ = run_loop_time_series(time, observed, model_step_fn, best_params, mask) | |
| best_metric = float(metric_fn(time, observed, model_vals, Time_Low, Time_High)) | |
| history: List[Dict[str, Any]] = [] | |
| if verbose: | |
| print(f"[{time_series_name}] start metric={best_metric:.6g}") | |
| switched_to_rel = os.getenv('RELATIVE', '') == '1' | |
| for iteration in range(1, max_iters + 1): | |
| any_accepted = False | |
| mode = "rel" if switched_to_rel else "abs" | |
| if verbose: | |
| print(f"[{time_series_name}] iteration {iteration} mode={mode} best_metric={best_metric:.6g}") | |
| # iterate over monitored params in fixed order | |
| for name in Monitored_Parameters: | |
| if name == "Imp_Stride": | |
| max_val = 12 | |
| trials_per_iter = 4 | |
| # sample without replacement when possible | |
| population = list(range(0, max_val + 1)) | |
| k = min(trials_per_iter, len(population)) | |
| sampled = random.sample(population, k) if k > 0 else [] | |
| for cand in sampled: | |
| # skip if same as current | |
| if int(best_params.get(name, 0)) == cand: | |
| continue | |
| candidate_params = copy.deepcopy(best_params) | |
| candidate_params[name] = int(cand) | |
| mvals_cand, _, _ = run_loop_time_series(time, observed, model_step_fn, candidate_params, mask) | |
| metric_cand = float(metric_fn(time, observed, mvals_cand, Time_Low, Time_High)) | |
| accepted = metric_cand + tol < best_metric | |
| history.append({ | |
| "param": name, | |
| "index": None, | |
| "candidate": cand, | |
| "metric": metric_cand, | |
| "accepted": accepted, | |
| "method": "discrete_random", | |
| }) | |
| if accepted: | |
| best_params = candidate_params | |
| best_metric = metric_cand | |
| model_vals = mvals_cand | |
| any_accepted = True | |
| if verbose: | |
| print(f"[stride] accepted {name} = {cand} -> metric {best_metric:.6g}") | |
| # keep sampling further candidates this iteration (could accept more) | |
| continue | |
| if name == "Harmonics": | |
| max_val = 100 | |
| trials_per_iter = 10 | |
| # sample without replacement when possible | |
| population = list(range(0, max_val + 1)) | |
| k = min(trials_per_iter, len(population)) | |
| sampled = random.sample(population, k) if k > 0 else [] | |
| for cand in sampled: | |
| # skip if same as current | |
| lst = best_params.get(name) | |
| if not lst: | |
| continue | |
| # iterate elements | |
| for idx in range(len(lst)): | |
| cur = int(lst[idx]) | |
| if cur == cand: | |
| continue | |
| if cur == 1: # Leave fundamental alone | |
| continue | |
| candidate_params = copy.deepcopy(best_params) | |
| candidate_params[name][idx] = int(cand) | |
| mvals_cand, _, _ = run_loop_time_series(time, observed, model_step_fn, candidate_params, mask) | |
| metric_cand = float(metric_fn(time, observed, mvals_cand, Time_Low, Time_High)) | |
| accepted = metric_cand + tol < best_metric | |
| history.append({ | |
| "param": name, | |
| "index": idx, | |
| "candidate": cand, | |
| "metric": metric_cand, | |
| "accepted": accepted, | |
| "method": "discrete_random", | |
| }) | |
| if accepted: | |
| best_params = candidate_params | |
| best_metric = metric_cand | |
| model_vals = mvals_cand | |
| any_accepted = True | |
| if verbose: | |
| print(f"[harmonic] accepted {name} = {cand} -> metric {best_metric:.6g}") | |
| # keep sampling further candidates this iteration (could accept more) | |
| continue | |
| if _is_list_param(name): | |
| # skip if param not present or empty | |
| lst = best_params.get(name) | |
| if not lst: | |
| continue | |
| # iterate elements | |
| for idx in range(len(lst)): | |
| cur = float(lst[idx]) | |
| accepted_this_element = False | |
| for sign in (1.0, -1.0): | |
| candidate_params = copy.deepcopy(best_params) | |
| # candidate_params[name][idx] = cur + delta*cur | |
| if use_random: | |
| delta = -step * math.log(random.random()) | |
| else: | |
| delta = step | |
| if not switched_to_rel: | |
| # absolute candidate | |
| candidate = cur + sign * delta | |
| else: | |
| # relative candidate (fallback to additive if cur==0) | |
| if abs(cur) > 0.0: | |
| candidate = cur * (1.0 + sign * delta) | |
| else: | |
| candidate = cur + sign * delta | |
| candidate_params[name][idx] = candidate | |
| model_vals_cand, _, _ = run_loop_time_series(time, observed, model_step_fn, candidate_params, mask) | |
| metric_cand = float(metric_fn(time, observed, model_vals_cand, Time_Low, Time_High)) | |
| accepted = metric_cand + tol < best_metric | |
| history.append({ | |
| "param": name, | |
| "index": idx, | |
| "candidate": candidate_params[name][idx], | |
| "metric": metric_cand, | |
| "accepted": accepted, | |
| }) | |
| if accepted: | |
| # keep the candidate | |
| best_params = candidate_params | |
| best_metric = metric_cand | |
| model_vals = model_vals_cand | |
| any_accepted = True | |
| accepted_this_element = True | |
| if verbose: | |
| print(f"[{time_series_name}] accepted {name}[{idx}] = {best_params[name][idx]:.6g} -> metric {best_metric:.6g}") | |
| # continue trying further +/- steps from the new value: | |
| cur = float(best_params[name][idx]) | |
| break # stop trying other sign at this pass; move to try again from updated cur | |
| # end +/- loop for this element | |
| # Optionally attempt additional immediate steps in same direction until no improvement: | |
| # (Keep this simple implementation: we will revisit in next outer iteration) | |
| pass | |
| else: | |
| # scalar parameter | |
| if name not in best_params: | |
| continue | |
| cur = float(best_params[name]) | |
| accepted_this_param = False | |
| for sign in (1.0, -1.0): | |
| candidate_params = copy.deepcopy(best_params) | |
| # candidate_params[name] = cur + delta*cur | |
| if use_random: | |
| delta = -step * math.log(random.random()) * Magnify_Scalar | |
| else: | |
| delta = step | |
| if not switched_to_rel: | |
| # absolute candidate | |
| candidate = cur + sign * delta | |
| else: | |
| # relative candidate (fallback to additive if cur==0) | |
| if abs(cur) > 0.0: | |
| candidate = cur * (1.0 + sign * delta) | |
| else: | |
| candidate = cur + sign * delta | |
| candidate_params[name] = candidate | |
| model_vals_cand, _, _ = run_loop_time_series(time, observed, model_step_fn, candidate_params, mask) | |
| metric_cand = float(metric_fn(time, observed, model_vals_cand, Time_Low, Time_High)) | |
| history.append({ | |
| "param": name, | |
| "index": None, | |
| "candidate": candidate_params[name], | |
| "metric": metric_cand, | |
| "accepted": metric_cand + tol < best_metric, | |
| }) | |
| if metric_cand + tol < best_metric: | |
| best_params = candidate_params | |
| best_metric = metric_cand | |
| model_vals = model_vals_cand | |
| any_accepted = True | |
| accepted_this_param = True | |
| if verbose: | |
| CV = float(metric_fn(time, observed, model_vals_cand, Time_Low, Time_High, False)) | |
| print(f"[{time_series_name}] accepted {name} = {best_params[name]:.6g} -> metric {best_metric:.6g} CV {CV:.6g} ") | |
| # move on from this parameter (we'll potentially refine in the next iteration) | |
| break | |
| # end +/- for scalar | |
| # end loop over parameters | |
| if not any_accepted: | |
| if not switched_to_rel: | |
| # switch strategies and continue searching with relative updates | |
| switched_to_rel = True | |
| if verbose: | |
| print(f"[{time_series_name}] no acceptance with absolute steps; switching to relative steps") | |
| # do not stop yet; continue with next iteration in relative mode | |
| continue | |
| else: | |
| if verbose: | |
| print(f"[{time_series_name}] no acceptance in iteration {iteration}; stopping early") | |
| return { | |
| "best_params": best_params, | |
| "best_metric": best_metric, | |
| "best_model": model_vals, | |
| "best_state": final_state, | |
| "history": history, | |
| "iterations": iteration, | |
| } | |
| # max iters reached | |
| if verbose: | |
| print(f"[{time_series_name}] reached max_iters={max_iters}, best_metric={best_metric:.6g}") | |
| return { | |
| "best_params": best_params, | |
| "best_metric": best_metric, | |
| "best_model": model_vals, | |
| "best_state": final_state, | |
| "history": history, | |
| "iterations": max_iters, | |
| } | |
| # Utilities --------------------------------------------------------------------- | |
| def parse_json_like_arg(s: str | None) -> Dict[str, Any]: | |
| """Parse a JSON-like string from CLI --params. Returns dict.""" | |
| if s is None: | |
| return {} | |
| try: | |
| return json.loads(s) | |
| except Exception as e: | |
| raise argparse.ArgumentTypeError(f"Invalid JSON for --params: {e}") | |
| def main(): | |
| ap = argparse.ArgumentParser(description="Time-series modeler with per-timestamp loop and metrics.") | |
| ap.add_argument('csv', help='Input CSV file (two columns: time, value).') | |
| ap.add_argument('--out', default='fitted_out.csv', help='Output CSV file with time, observed, model, residual.') | |
| ap.add_argument('--plot', action='store_true', help='Show plot of observed vs model (requires matplotlib).') | |
| ap.add_argument('--low', default=0.0) | |
| ap.add_argument('--high', default=3000.0) | |
| ap.add_argument('--cc', action='store_true') | |
| ap.add_argument('--random', action='store_true') | |
| ap.add_argument('--scale', default=1.0) | |
| args = ap.parse_args() | |
| global use_pearson | |
| if args.cc: | |
| use_pearson = True | |
| print("using CC") | |
| else: | |
| use_pearson = False | |
| global use_random | |
| if args.random: | |
| use_random = True | |
| print("using random") | |
| else: | |
| use_random = False | |
| global time_series_name | |
| time_series_name = args.csv | |
| # Read CSV | |
| time, y = read_two_column_csv(args.csv) | |
| if time.size == 0: | |
| raise SystemExit("No data read from CSV.") | |
| y = normalize_rms_y(y) | |
| # Read JSON file with same name appended by ".p" | |
| json_path = args.csv + '.p' | |
| data_dict = read_json_p(json_path) | |
| params = data_dict | |
| model_fn = model_step_algorithm | |
| Low = float(args.low) | |
| High = float(args.high) | |
| # Make a clone of the series (not strictly necessary but requested) | |
| cloned = clone_series(y) | |
| result = simple_descent( | |
| time, y, model_fn, params, | |
| metric_fn=compute_metrics_region, | |
| step=float(os.environ.get('STEP', '0.05')), | |
| max_iters=int(os.environ.get('MAX_ITERS', '400')), | |
| tol=1e-12, verbose=True, Time_Low=Low, Time_High=High | |
| ) | |
| params = result["best_params"] | |
| mask = build_mask(time, Low, High, True) | |
| # Run the time-stepping loop (explicit loop per timestamp) | |
| model_vals, final_state, forcing = run_loop_time_series(time, cloned, model_fn, params, mask) | |
| # Compute metrics | |
| metrics = compute_metrics(time, y, model_vals, Low, High) | |
| # Prepare output DataFrame | |
| out_df = pd.DataFrame({ | |
| 'time': time, | |
| 'observed': y, | |
| 'model': model_vals, | |
| 'forcing' : forcing, | |
| 'residual': metrics['residuals'] | |
| }) | |
| out_df.to_csv(args.out, index=False) | |
| # Update data dictionary with metadata and save to .p file | |
| data_dict_out = dict(params) # copy new | |
| write_json_p(json_path, data_dict_out) | |
| # Print summary | |
| print(f"Processed {time.size} samples from: {args.csv}") | |
| print(f"Output CSV: {args.out}") | |
| print(f"Updated JSON data file: {json_path}") | |
| print("Metrics:") | |
| print(f" CV = {metrics['CV']:.6e}") | |
| print(f" MSE = {metrics['mse']:.6e}") | |
| print(f" Pearson r = {metrics['pearson_r']}, p-value = {metrics.get('pearson_p', None)}") | |
| # Optional plotting | |
| mask = (time > Low) & (time < High) | |
| plt.figure(figsize=(12, 6)) | |
| plt.plot(time, y, label='observed', lw=0.5) | |
| plt.plot(time, float(args.scale)*model_vals, label='model', lw=0.5, color='red') | |
| # plt.plot(time, metrics['residuals'], label='residual', lw=0.8, alpha=0.7) | |
| plt.plot(time[mask], [0.0] * len(time[mask]), 'k--', label='cross-validation', linewidth=3) | |
| plt.legend() | |
| plt.xlabel('time') | |
| plt.ylabel('value') | |
| plt.title(f"Model: {args.csv} Pearson r={metrics['pearson_r']:.4g} CV={metrics['CV']:.4g}") | |
| # plt.grid(True) | |
| if args.plot: | |
| plt.show() | |
| else: | |
| plt.savefig(args.csv+'-'+args.low+'-'+args.high+'.png', bbox_inches='tight') # Save as PNG with tight bounding box | |
| if __name__ == '__main__': | |
| main() |
| """ | |
| Sinusoidal multiple linear regression with optional single linear X column | |
| This is a simplified version that removes the previous `linear_scales` feature | |
| and instead provides a boolean flag `add_linear_x`. When True, a single column | |
| equal to X is appended to the design matrix (after sin/cos columns). The model | |
| fits terms: | |
| Y ≈ intercept + sum_i [a_i * sin(k_i N_i X) + b_i * cos(k_i N_i X)] + c * X | |
| Usage: | |
| - build_design_matrix(X, N_list, k=1.0, add_intercept=True, add_linear_x=False) | |
| - fit_sinusoidal_regression(..., add_linear_x=False) | |
| - predict_from_coefs(..., add_linear_x=False) | |
| """ | |
| from typing import Sequence, Union, Tuple, Dict, Any, Optional | |
| import numpy as np | |
| def build_design_matrix( | |
| X: np.ndarray, | |
| N_list: Sequence[int], | |
| k: Union[float, Sequence[float]] = 1.0, | |
| add_intercept: bool = True, | |
| add_linear_x: bool = False, | |
| ) -> Tuple[np.ndarray, np.ndarray]: | |
| """ | |
| Build the design matrix with columns: | |
| [1 (optional), sin(k_i * N_i * X), cos(k_i * N_i * X) for each N_i, | |
| X (optional single linear column)] | |
| Returns: | |
| - A: design matrix (n_samples, n_columns) | |
| - k_arr: numpy array of k values aligned with N_list | |
| """ | |
| X = np.asarray(X) | |
| if X.ndim != 1: | |
| raise ValueError("X must be a 1-D array of sample positions (e.g., time or angle).") | |
| N_arr = np.asarray(N_list, dtype=int) | |
| if N_arr.ndim != 1: | |
| raise ValueError("N_list must be a 1-D sequence of integers.") | |
| # Normalize k to an array of same length as N_list | |
| if np.isscalar(k): | |
| k_arr = np.full_like(N_arr, float(k), dtype=float) | |
| else: | |
| k_arr = np.asarray(k, dtype=float) | |
| if k_arr.shape != N_arr.shape: | |
| raise ValueError("k must be a scalar or have the same length as N_list.") | |
| n = X.shape[0] | |
| cols = [] | |
| # sinusoidal terms | |
| for ki, Ni in zip(k_arr, N_arr): | |
| arg = ki * Ni * X | |
| cols.append(np.sin(arg)) | |
| cols.append(np.cos(arg)) | |
| # single linear X column if requested | |
| if add_linear_x: | |
| cols.append(X) | |
| A = np.column_stack(cols) if cols else np.empty((n, 0)) | |
| if add_intercept: | |
| A = np.column_stack([np.ones(n), A]) | |
| return A, k_arr | |
| def fit_sinusoidal_regression( | |
| X: np.ndarray, | |
| Y: np.ndarray, | |
| N_list: Sequence[int], | |
| k: Union[float, Sequence[float]] = 1.0, | |
| intercept: bool = True, | |
| ridge: Optional[float] = None, | |
| rcond: Optional[float] = None, | |
| add_linear_x: bool = False, | |
| ) -> Dict[str, Any]: | |
| """ | |
| Fit linear regression: | |
| Y ≈ intercept + sum_i [a_i * sin(k_i N_i X) + b_i * cos(k_i N_i X)] + c * X (optional) | |
| Parameters: | |
| add_linear_x: when True, include one column equal to X (coefficient returned as 'coef_x'). | |
| Returns: | |
| result dict with keys including: | |
| - 'coefs', 'intercept', 'coefs_by_N', 'coef_x' (if add_linear_x True), | |
| 'predict', 'R2', 'mse', 'A', 'k_arr', ... | |
| """ | |
| X = np.asarray(X) | |
| Y = np.asarray(Y) | |
| if X.ndim != 1: | |
| raise ValueError("X must be 1-D array of positions.") | |
| if Y.shape[0] != X.shape[0]: | |
| raise ValueError("X and Y must have the same number of samples in axis 0.") | |
| A, k_arr = build_design_matrix(X, N_list, k=k, add_intercept=intercept, add_linear_x=add_linear_x) | |
| n_samples, n_cols = A.shape | |
| y_was_1d = (Y.ndim == 1) | |
| if y_was_1d: | |
| Y = Y.reshape(-1, 1) | |
| if ridge is None: | |
| coefs, residuals, rank, s = np.linalg.lstsq(A, Y, rcond=rcond) | |
| else: | |
| ATA = A.T @ A | |
| n_cols = ATA.shape[0] | |
| reg = np.eye(n_cols) * float(ridge) | |
| if intercept and n_cols > 0: | |
| reg[0, 0] = 0.0 | |
| ATA_reg = ATA + reg | |
| ATy = A.T @ Y | |
| coefs = np.linalg.solve(ATA_reg, ATy) | |
| residuals = None | |
| rank = None | |
| s = None | |
| # Extract intercept and coefficient body | |
| if intercept: | |
| intercept_val = coefs[0, :] | |
| coef_body = coefs[1:, :] | |
| else: | |
| intercept_val = np.zeros((1, Y.shape[1]))[0] | |
| coef_body = coefs | |
| # Map sin/cos coefficients to N -> (sin_coef, cos_coef) | |
| N_arr = np.asarray(list(N_list), dtype=int) | |
| coefs_by_N = {} | |
| m = N_arr.size | |
| for i, Ni in enumerate(N_arr): | |
| sin_idx = 2 * i | |
| cos_idx = 2 * i + 1 | |
| sin_coef = coef_body[sin_idx, :] if coef_body.shape[0] > sin_idx else np.zeros((Y.shape[1],)) | |
| cos_coef = coef_body[cos_idx, :] if coef_body.shape[0] > cos_idx else np.zeros((Y.shape[1],)) | |
| if y_was_1d: | |
| coefs_by_N[int(Ni)] = (float(sin_coef[0]), float(cos_coef[0])) | |
| else: | |
| coefs_by_N[int(Ni)] = (sin_coef.copy(), cos_coef.copy()) | |
| # Extract the single linear X coefficient if present | |
| coef_x = None | |
| if add_linear_x: | |
| idx = 2 * m # position after all sin/cos columns in coef_body | |
| if coef_body.shape[0] > idx: | |
| linear_coef = coef_body[idx, :] | |
| else: | |
| linear_coef = np.zeros((Y.shape[1],)) | |
| if y_was_1d: | |
| coef_x = float(linear_coef[0]) | |
| else: | |
| coef_x = linear_coef.copy() | |
| def predict_fn(X_new: np.ndarray) -> np.ndarray: | |
| A_new, _ = build_design_matrix(X_new, N_list, k=k_arr, add_intercept=intercept, add_linear_x=add_linear_x) | |
| preds = A_new @ coefs | |
| if y_was_1d: | |
| return preds.ravel() | |
| return preds | |
| # Compute predictions/residuals using the same shapes to avoid broadcasting issues | |
| preds = A @ coefs # shape (n_samples, n_targets) | |
| residuals_arr = Y - preds | |
| ss_res = np.sum(residuals_arr ** 2, axis=0) | |
| ss_tot = np.sum((Y - np.mean(Y, axis=0)) ** 2, axis=0) | |
| with np.errstate(divide='ignore', invalid='ignore'): | |
| R2 = 1.0 - ss_res / ss_tot | |
| mse = np.mean(residuals_arr ** 2, axis=0) | |
| if y_was_1d: | |
| coefs_out = coefs.ravel() | |
| intercept_out = float(intercept_val[0]) if intercept else 0.0 | |
| R2 = float(R2[0]) | |
| mse = float(mse[0]) | |
| result = { | |
| "coefs": coefs_out if y_was_1d else coefs, | |
| "intercept": intercept_out if y_was_1d else intercept_val, | |
| "coefs_by_N": coefs_by_N, | |
| "coef_x": coef_x, | |
| "predict": predict_fn, | |
| "R2": R2, | |
| "mse": mse, | |
| "A": A, | |
| "k_arr": k_arr, | |
| "residuals": residuals, | |
| "lstsq_rank": rank, | |
| "lstsq_singular_values": s, | |
| } | |
| return result | |
| def predict_from_coefs( | |
| X: np.ndarray, | |
| coefs: np.ndarray, | |
| N_list: Sequence[int], | |
| k: Union[float, Sequence[float]] = 1.0, | |
| intercept: bool = True, | |
| add_linear_x: bool = False, | |
| ) -> np.ndarray: | |
| A, _ = build_design_matrix(X, N_list, k=k, add_intercept=intercept, add_linear_x=add_linear_x) | |
| return A @ coefs | |
| if __name__ == "__main__": | |
| # Demo: include a linear c * X term in addition to sin/cos terms | |
| import pprint | |
| rng = np.random.default_rng(1) | |
| n = 500 | |
| X = np.linspace(0, 2 * np.pi, n) | |
| N_list = [1, 3, 7] | |
| k_true = 0.9 | |
| true_coefs = { | |
| 1: (1.5, -0.7), | |
| 3: (0.3, 0.8), | |
| 7: (-0.9, 0.1), | |
| } | |
| intercept_true = 0.25 | |
| c_true = 2.3 # true linear coefficient for X | |
| y_true = np.full_like(X, fill_value=intercept_true, dtype=float) | |
| for N in N_list: | |
| a, b = true_coefs[N] | |
| y_true += a * np.sin(k_true * N * X) + b * np.cos(k_true * N * X) | |
| y_true += c_true * X # add linear trend | |
| # NO NOISE so a perfect fit should be possible when we include the linear term | |
| noise_sigma = 0.0 | |
| Y = y_true + rng.normal(scale=noise_sigma, size=X.shape) | |
| # Fit with add_linear_x=True to add the X column (so fitted coefficient corresponds to c) | |
| model = fit_sinusoidal_regression(X, Y, N_list=N_list, k=k_true, intercept=True, add_linear_x=True) | |
| print("mse:", model["mse"]) | |
| print("R2:", model["R2"]) | |
| print("intercept:", model["intercept"]) | |
| print("coefs_by_N:", model["coefs_by_N"]) | |
| print("coef_x:", model["coef_x"]) | |
| print("norm residuals:", np.linalg.norm(Y - model["predict"](X))) |
Wismar #8
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.06865604188611381,
-0.25915358679930633,
0.22090989782850767,
-0.09713068523535898,
0.037748816664216325,
0.25623142373963426,
-0.16926275417746936
],
"AliasedPhase": [
14.131971361396506,
3.2439272902495895,
5.195678982346267,
1.6749104011451375,
8.608091594740811,
11.935576721278485,
10.543770538611284
],
"DC": -0.12423808951744805,
"Damp": 0.05456007735928286,
"DeltaTime": 3.3382480100083334,
"Harmonics": [
1,
7
],
"Hold": 0.0014755897941032168,
"Imp_Amp": 196.64601354387912,
"Imp_Amp2": 0.5799170862856585,
"Imp_Stride": 10,
"Initial": 0.11997589253883073,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 136.74476922871057,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5760430638872,
0.12501688896426782,
0.0722107314346754,
-0.09159914058935532,
-0.010453316458117149,
0.024842982720618974,
-0.046414795115091015,
-0.057702880307387704,
0.019079943912844316,
-0.0089049019625861,
-0.05043994226353918,
-0.04757300412707229,
-0.02048748284336966,
0.001016251916970729,
-0.0184425968105452,
-0.012858996294449814,
-0.01285016287458977,
-0.016320311443792094,
0.04432227787621819,
0.061660308969607155,
-0.01937040778031875,
-0.028870307813067025
],
"PeriodsPhase": [
14.563703557036122,
8.417616567782739,
5.78535881148441,
5.051859038837522,
9.325059294294475,
8.847420491188636,
13.550095920076044,
7.120936988761767,
8.065606386142496,
3.891241423336842,
4.452948263272959,
7.2919811741074785,
1.8992779141269203,
5.424291480758038,
5.934309595513711,
6.935067947448513,
4.121374837088468,
4.7805236638934705,
7.724722363222789,
7.502661944086427,
3.3639252736171135,
7.552509372529399
],
"Year": 365.24708113327955
}
Cristobal, Panama #169
applied high LTE coefficients, harmonics adjusted from starting point
"Harmonics": [
1,
7,
15,
30,
60,
90,
180
],
to
[harmonic] accepted Harmonics = 7 -> metric 0.394515
[harmonic] accepted Harmonics = 68 -> metric 0.393305
[harmonic] accepted Harmonics = 2 -> metric 0.385933
[harmonic] accepted Harmonics = 77 -> metric 0.383996
[harmonic] accepted Harmonics = 35 -> metric 0.379977
[harmonic] accepted Harmonics = 48 -> metric 0.378582
[harmonic] accepted Harmonics = 62 -> metric 0.377787
[harmonic] accepted Harmonics = 70 -> metric 0.375846
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.2522309995625013,
-0.2000594370774592,
0.17873734726279142,
-0.2544121553333023,
0.26042125426268586,
0.07640089658134729,
-0.24986534319849535
],
"AliasedPhase": [
9.065527116429877,
3.5673128118605435,
4.211444968797153,
0.9526151156775778,
8.416940209402044,
10.582547786429881,
12.691119883942708
],
"DC": -0.12423808951744805,
"Damp": 0.08063473342689056,
"DeltaTime": 7.599787171148627,
"Harmonics": [
1,
2,
15,
7,
62,
70,
180
],
"Hold": 0.001490592955194318,
"Imp_Amp": 193.59892073565175,
"Imp_Amp2": 1.7613383468512895,
"Imp_Stride": 3,
"Initial": 0.14608693637746378,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 136.70598840699768,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5602431050634464,
0.13699152782717128,
0.07961781919569497,
-0.10903505448340975,
-0.01845835353332017,
0.01828437569848052,
-0.048454085957261944,
-0.042270936253260304,
0.014423138785882335,
-0.008656309342386745,
-0.034408855379617014,
-0.03531388898355181,
-0.020433456885373104,
0.0007489999263182004,
-0.02056607140535374,
-0.010031192063505265,
-0.004755082927887884,
-0.010451885151740168,
0.04277912289379632,
0.06767906758354754,
-0.01968715734259757,
-0.04928665352880314
],
"PeriodsPhase": [
14.624374493059443,
8.140605417923654,
5.69427900235415,
5.642228849421976,
8.132358655871565,
8.833636268068606,
12.563901960102367,
7.503042404498913,
7.736429629362617,
4.93603632802139,
5.206893191989684,
8.862213040943782,
1.5362065130899905,
5.297566771453176,
6.314851297033993,
7.4863663894836465,
2.1516145261656145,
3.4590568229381486,
8.029378092123137,
7.4591229498389655,
3.117217200484947,
7.129195106050026
],
"Year": 365.24708113327955
}
AO
revisit with adjustable harmonics

1,
7,
9,
96, flipped from 11
13,
33
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.14914218307677216,
0.09211268535903916,
0.1478943612263781,
-0.14213536227692897,
0.08848177211794445,
0.04660421467886394,
-0.1777270843076939
],
"AliasedPhase": [
10.300640543361038,
6.40139578604698,
4.478841145933187,
-0.24217088058978317,
6.335649996482343,
13.180347181240188,
8.91037163174551
],
"DC": -0.08712197565502076,
"Damp": 0.06577547490993992,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1,
7,
9,
96,
13,
33
],
"Hold": 0.0014934961844356531,
"Imp_Amp": 194.69041845597516,
"Imp_Amp2": 2.321027612344463,
"Imp_Stride": 1,
"Initial": -0.2112113924374196,
"LTE_Amp": 0.7366977409258476,
"LTE_Amp2": 0.8757671513026981,
"LTE_Freq": 136.2869349004119,
"LTE_Freq2": 0.08111809255988764,
"LTE_Phase": 1.3911870213630064,
"LTE_Phase2": 8.226077238363752,
"LTE_Zero": -0.07041388485483531,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5476904575263383,
0.1262152870391498,
0.08004287694707113,
-0.12233515157933589,
-0.03142632243516138,
0.028007693001873873,
-0.053914009043361376,
-0.04739113862373045,
0.013579054910480347,
-0.005094033589172208,
-0.025140294808580325,
-0.03521219788183813,
-0.021196483461792596,
0.000764300648610519,
-0.016026994181992063,
-0.011424809623033974,
-0.016988407712042537,
-0.008596052747668469,
0.032871513442898966,
0.08415081862254632,
-0.0299579652984813,
-0.06436874172072794
],
"PeriodsPhase": [
14.567764671986634,
8.40898371734593,
5.569688207926648,
5.090874340503449,
9.251214040614858,
8.625822911285184,
13.472671139741282,
7.6940054926911845,
7.352242562368276,
5.815937724368722,
5.14161312765974,
8.003723761652784,
1.5680593678833128,
6.246496537837596,
6.783802293167124,
7.1304188866734775,
2.6482401005228873,
5.3580091441829545,
7.293139680212582,
7.2970819738569705,
4.224029506325266,
6.284284955061745
],
"Year": 365.24708113327955
}
Wismar, DE #8
Harmonics added
revisit

{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.049618119719241176,
-0.3028643216693562,
0.19914839342945487,
-0.09852471640663212,
0.04456330133510319,
0.2634212662571628,
-0.19077990926454036
],
"AliasedPhase": [
13.852156636831005,
3.2811307721820206,
5.198497828195737,
1.695298151383701,
8.577448055713313,
11.804581181108624,
10.899058267215343
],
"DC": -0.12423808951744805,
"Damp": 0.054497601685109906,
"DeltaTime": 3.3382480100083334,
"Harmonics": [
1,
7,
14,
49
],
"Hold": 0.0014759595883307515,
"Imp_Amp": 196.92870461055585,
"Imp_Amp2": 0.5442188046091948,
"Imp_Stride": 10,
"Initial": 0.11997589253883073,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 136.63711825880415,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5760430638872,
0.1253931296922821,
0.07225018989635504,
-0.09167810809793622,
-0.010272819858286297,
0.0243933780844774,
-0.04609105381126964,
-0.05749658820089569,
0.019079943912844316,
-0.009114413081107074,
-0.05040648443907696,
-0.047487408184737796,
-0.020436967488072583,
0.0010116264300856915,
-0.018520599825760885,
-0.012845476337814148,
-0.012868015468238441,
-0.01640716476085496,
0.04486720133994736,
0.06164936392111757,
-0.018964995283902945,
-0.02919756898177908
],
"PeriodsPhase": [
14.563703557036122,
8.416809395869787,
5.78149829328626,
5.057771359834988,
9.449708635382727,
8.851668354293006,
13.547549062155644,
7.127085053088832,
8.05548006558094,
3.9054180442772646,
4.462308713742377,
7.31537782830521,
1.9160414408199342,
5.403303952079204,
5.936583255644839,
6.9292431788285445,
4.090904331878877,
4.792624713256159,
7.7318986934812175,
7.507194893259534,
3.3756389755387195,
7.59261202682353
],
"Year": 365.24708113327955
}
St. John NB, CA #195
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.1750895955534446,
-0.030452661911780022,
0.1833908208346667,
-0.026512306517606457,
0.07897238565442666,
0.42374931649609343,
-0.2993864386790258
],
"AliasedPhase": [
5.423689673982107,
6.5129316792059315,
8.778335525750025,
0.47494252962054073,
6.050364165598256,
11.271360099426037,
9.515703529586881
],
"DC": -0.12423808951744805,
"Damp": 0.07044953519410392,
"DeltaTime": 3.7597105124718224,
"Harmonics": [
1
],
"Hold": 0.001490592955194318,
"Imp_Amp": 192.99021563025735,
"Imp_Amp2": 0.7628489670157103,
"Imp_Stride": 2,
"Initial": -0.1906984022480848,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 135.8456478344581,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5591929493115206,
0.13617543087668302,
0.07961781919569497,
-0.10903505448340975,
-0.01845835353332017,
0.018253307968383443,
-0.04855756933284213,
-0.04226767726342105,
0.015418164214488068,
-0.01879090697300738,
-0.03393860158873026,
-0.03732924419346166,
-0.03089618301397834,
0.0007503263913829923,
-0.020793334900369498,
-0.010470019903667867,
-0.004728553170392381,
-0.014145312631924208,
0.04108274903400395,
0.06642694375181565,
-0.01968715734259757,
-0.04930584737042731
],
"PeriodsPhase": [
14.649380573316199,
9.518131467371642,
5.847503784830045,
4.432933362335181,
8.293950122230589,
7.77256825956597,
12.903846820600387,
9.127613849649048,
8.664322928582665,
4.491835662177091,
5.52750593055,
5.415797186432357,
3.2723422585920363,
0.8872128049850123,
7.134730866387159,
8.241234904680933,
1.088350146350762,
5.626664842860959,
9.389277117258034,
5.622939784626737,
7.041128072892428,
4.025942956137139
],
"Year": 365.24708113327955
}
Fernandina Beach FL USA #112
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
-0.057555613508427465,
-0.08285577755094596,
0.044473904442536516,
-0.13566837951881072,
0.16079367890183868,
0.3002021874510283,
-0.11670138351030494
],
"AliasedPhase": [
4.482655991921676,
5.316338558715212,
0.7309969871916848,
0.43011652085925445,
5.289771741195146,
10.614626127027849,
10.091787408512317
],
"DC": -0.12423808951744805,
"Damp": 0.03635613183984467,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1,
7,
15
],
"Hold": 0.001490592955194318,
"Imp_Amp": 195.4134108636822,
"Imp_Amp2": 1.6684715344719885,
"Imp_Stride": 11,
"Initial": 0.3797641174113531,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 135.66217087246477,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5581586259726958,
0.15401856655334584,
0.08055360307373859,
-0.10598527494671774,
-0.01845835353332017,
0.017598933947432442,
-0.048970424420686054,
-0.04105447206220204,
0.013953351583433748,
-0.01877773279673608,
-0.0366706825763154,
-0.03568177003628198,
-0.019754832943573695,
0.0007503263913829923,
-0.02056607140535374,
-0.010031192063505265,
-0.003997958340302967,
-0.010451885151740168,
0.042834390943015145,
0.062073228712065706,
-0.019483562257197794,
-0.047284068402164904
],
"PeriodsPhase": [
14.521103461887233,
8.22760807556152,
5.5015357229779625,
5.040903392884266,
8.074185955550481,
7.924711515371393,
14.631554120605676,
7.6480711310663585,
7.9129804242839725,
6.531356209221879,
4.5972668859598,
8.34244038830869,
2.0245019646686986,
4.2563612602125005,
6.640488335471075,
6.4177487231284,
1.9285971473462127,
4.15911762610287,
7.497684820273984,
7.151625761172486,
3.6317519859148795,
6.477678400267924
],
"Year": 365.24708113327955
}
Manila, PH #145
Port Lyttelton NZ #259
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.4020881809355977,
-0.11034301244404757,
0.18441563705428646,
-0.09887820891650238,
0.033697529006302435,
0.3572024442585324,
-0.5551386863507125
],
"AliasedPhase": [
10.194532049465595,
1.8190138689998703,
5.955506075676501,
-2.166140328431231,
3.7484735306486687,
12.091916191166783,
13.929479757986652
],
"DC": -0.12423808951744805,
"Damp": 0.08238144578602241,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1
],
"Hold": 0.001490592955194318,
"Imp_Amp": 194.73876231829894,
"Imp_Amp2": 2.4651748742799953,
"Imp_Stride": 6,
"Initial": 0.1552095024632388,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 137.21221763745726,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5633954367610651,
0.1366352543026317,
0.08374051728207171,
-0.10681044512069936,
-0.0206913887819673,
0.018253307968383443,
-0.0480276525236275,
-0.042270936253260304,
0.014403850120632581,
-0.008785472060559782,
-0.03592228920892104,
-0.037729927843659784,
-0.01833075825226522,
0.0007874089487731815,
-0.02056607140535374,
-0.010031192063505265,
-0.010077787396537044,
-0.010451885151740168,
0.04277912289379632,
0.06751669746236998,
-0.01968715734259757,
-0.04006142498962807
],
"PeriodsPhase": [
14.576694138251169,
8.332081868943959,
5.776749420763802,
4.878470406030936,
8.838388298277447,
8.123018313696706,
13.698613074767005,
7.82136771034345,
7.807392643951987,
4.342402395908406,
4.76405807872703,
8.864732474638334,
1.759710425251289,
5.878802307555689,
6.22805585960769,
7.405581289291711,
2.480146328374681,
4.709866929651147,
7.699010533004459,
7.533094933177059,
4.131178977510733,
6.864791481678003
],
"Year": 365.24708113327955
}


QBO30
Kept all tidal factor amplitudes fixed, only the phase to vary to allow alignment, as QBO slides along the timeline depending on altitude
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.2151409108540754,
-0.2722526208850203,
0.29382646586391037,
-0.10833536844490509,
0.05711881821450643,
0.17766476601172748,
-0.41978842020233453
],
"AliasedPhase": [
6.478660527904039,
3.760224151698393,
5.009471164492376,
1.389999336864612,
7.249040930402332,
7.657905364018221,
11.65727296646182
],
"DC": 0.4606026255623981,
"Damp": 0.07050761904515396,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1,
7,
15
],
"Hold": 0.01490592955194318,
"Imp_Amp": 112.966324132153,
"Imp_Amp2": -82.39956562908114,
"Imp_Stride": 1,
"Initial": 0.2534002033533554,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 2.494995628433717,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5602431050634464,
0.13699152782717128,
0.07961781919569497,
-0.10903505448340975,
-0.01845835353332017,
0.018253307968383443,
-0.048454085957261944,
-0.042270936253260304,
0.014423138785882335,
-0.008645872134132608,
-0.034408855379617014,
-0.035238540888359336,
-0.020433456885373104,
0.0007503263913829923,
-0.02056607140535374,
-0.010031192063505265,
-0.004728553170392381,
-0.010451885151740168,
0.04277912289379632,
0.0677113068326575,
-0.01968715734259757,
-0.04930584737042731
],
"PeriodsPhase": [
14.626664907315426,
7.196213050122936,
4.0693102583165075,
4.0686120768782095,
9.400880864705737,
8.836358968693148,
14.888104612422763,
8.050320916641375,
7.972687495998342,
4.085674320893779,
1.6177278574312712,
8.425269042951161,
-3.291414250687574,
2.6318990289439372,
8.05571786772537,
7.58649190204672,
0.7684860344348196,
5.596107772833543,
10.436463220267816,
7.977389844973048,
2.198426502027651,
8.831170612083238
],
"Scale": 3.87984487345023,
"Year": 365.24708113327955
}
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.2151409108540754,
-0.2722526208850203,
0.29382646586391037,
-0.10833536844490509,
0.05711881821450643,
0.17766476601172748,
-0.41978842020233453
],
"AliasedPhase": [
6.296259285485721,
2.4362266839880453,
4.608643511412993,
1.3266270148191892,
7.146699766166859,
9.982820651632526,
10.171941424591608
],
"DC": 0.4606026255623981,
"Damp": 1.7646760469753189,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1
],
"Hold": 0.01490592955194318,
"Imp_Amp": 129.29877458665226,
"Imp_Amp2": -17.49729439688319,
"Imp_Stride": 1,
"Initial": 0.0561280769045093,
"LTE_Amp": 1.3138405796661277,
"LTE_Amp2": 0.4298032835098847,
"LTE_Freq": 14.899940007680097,
"LTE_Freq2": 0.011180925598876339,
"LTE_Phase": 1.7029770680785028,
"LTE_Phase2": 7.873797958554444,
"LTE_Zero": 0.15390410621348005,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5602431050634464,
0.13699152782717128,
0.07961781919569497,
-0.10903505448340975,
-0.01845835353332017,
0.018253307968383443,
-0.048454085957261944,
-0.042270936253260304,
0.014423138785882335,
-0.008645872134132608,
-0.034408855379617014,
-0.035238540888359336,
-0.020433456885373104,
0.0007503263913829923,
-0.02056607140535374,
-0.010031192063505265,
-0.004728553170392381,
-0.010451885151740168,
0.04277912289379632,
0.0677113068326575,
-0.01968715734259757,
-0.04930584737042731
],
"PeriodsPhase": [
14.948784211878044,
7.991748425246889,
4.054956267015049,
4.196335098273111,
7.785092797221102,
10.47958483045336,
16.627927106325515,
8.207826934601364,
8.538586221112912,
3.751576676453439,
1.8255815236797501,
8.915736628128041,
-1.721493984702149,
7.278631091406096,
8.498086119086889,
8.697689162867563,
0.47100503289080553,
4.389916073188118,
10.576988825013341,
8.385820170335068,
3.2699059167937556,
8.59278498450938
],
"Scale": 3.87984487345023,
"Year": 365.24708113327955
}


EMI
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.32586762139891284,
-0.23847441503396954,
0.3251217109725268,
-0.08827661768102513,
0.047895840706057805,
0.4048174693967734,
-0.14676305527841121
],
"AliasedPhase": [
5.993443792676869,
4.878163392588478,
3.175244513331906,
4.094924557090009,
5.226967166958929,
7.430561301954834,
12.809328707752663
],
"Damp": 0.08588549460372283,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1,
7,
15,
21
],
"Hold": 0.0014968712731479145,
"Imp_Amp": 193.0190332815018,
"Imp_Amp2": 0.4961700550139651,
"Imp_Stride": 0,
"Initial": 0.11703514180651958,
"LTE_Freq": 137.2452424208781,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5581732039168739,
0.1322946909638389,
0.081184097681566,
-0.11272696743934751,
-0.029289707610965048,
0.019250021397595143,
-0.04820785057307671,
-0.04589466079305412,
0.015142479586250524,
-0.0036977284675364964,
-0.0278150553522922,
-0.02672892984321188,
-0.02132303170019312,
0.0008647308403387654,
-0.024356640792821998,
-0.008888251656195986,
-0.019506333163843936,
-0.016575332405438986,
0.04003681532963685,
0.057458556246872,
-0.02551132473645345,
-0.059143955870662214
],
"PeriodsPhase": [
14.703646965074674,
8.51199983367772,
5.9039704874819305,
5.110604199152988,
8.959055585856499,
8.448174401629688,
13.294335158093826,
7.47231232140073,
8.024905580433757,
4.263720773719261,
3.6918042584041646,
8.282289820397478,
0.8871421956530393,
2.1918127994311205,
6.842278247239128,
7.227974987618526,
3.330907069626779,
5.1357678597544885,
7.780228390030288,
6.863709427136066,
3.5093356445598523,
6.113001596808769
],
"Year": 365.24708113327955
}
AO
{
"Aliased": [
"0.0537449",
"0.1074898",
"0.220485143",
"1",
"2",
"0.07771",
"0.131456"
],
"AliasedAmp": [
0.15934408166886338,
0.0740804412840454,
0.16390711876072112,
-0.15281486240865905,
0.08154048268674471,
0.05831306830183182,
-0.20470075990514108
],
"AliasedPhase": [
10.245258213339097,
6.4290546823295305,
4.48433111604919,
5.654348401271973e-10,
6.078837462118578,
13.165679344754507,
8.8962296300929
],
"DC": -0.08712197565502076,
"Damp": 0.06577547490993992,
"DeltaTime": 3.4166430998860955,
"Harmonics": [
1,
7,
9,
11,
13,
33
],
"Hold": 0.0014934961844356531,
"Imp_Amp": 194.70652153965506,
"Imp_Amp2": 2.283057464850243,
"Imp_Stride": 1,
"Initial": -0.2112113924374196,
"LTE_Amp": 0.7366977409258476,
"LTE_Amp2": 0.8757671513026981,
"LTE_Freq": 136.28108055108393,
"LTE_Freq2": 0.08111809255988764,
"LTE_Phase": 1.3911870213630064,
"LTE_Phase2": 8.226077238363752,
"LTE_Zero": -0.07041388485483531,
"Periods": [
"27.2122",
"27.3216",
"27.5545",
"13.63339513",
"13.69114014",
"13.6061",
"13.6608",
"13.71877789",
"6795.985773",
"1616.215172",
"2120.513853",
"13.77725",
"3232.430344",
"9.132931547",
"9.108450374",
"9.120674533",
"27.0926041",
"3397.992886",
"9.095011909",
"9.082856397",
"2190.530426",
"6.816697567"
],
"PeriodsAmp": [
0.5476904575263383,
0.1262152870391498,
0.08004287694707113,
-0.12233515157933589,
-0.031464484826292174,
0.02803006745624818,
-0.053914009043361376,
-0.04743805538242568,
0.013579054910480347,
-0.005002054855859456,
-0.025140294808580325,
-0.03521219788183813,
-0.021196483461792596,
0.000764300648610519,
-0.016026994181992063,
-0.011424809623033974,
-0.01681858528814691,
-0.008596052747668469,
0.032871513442898966,
0.08415081862254632,
-0.0299579652984813,
-0.06436874172072794
],
"PeriodsPhase": [
14.567840625653822,
8.405934750056684,
5.570917217175438,
5.090937982605609,
9.24589338941118,
8.623578269827197,
13.471538833515371,
7.691096945597704,
7.352271501060609,
5.7680723735612345,
5.140153737900373,
7.999414672464829,
1.5680267237086847,
6.253886537791528,
6.781275684088271,
7.128656674349704,
2.645502462784979,
5.352416085906952,
7.295623464359417,
7.29797216892652,
4.223174255336961,
6.2872782493813855
],
"Year": 365.24708113327955
}
Darwin