Skip to content

Instantly share code, notes, and snippets.

@pukpr
Last active November 24, 2025 03:41
Show Gist options
  • Select an option

  • Save pukpr/0b7ac85fad1ea36f65a9b50d6c30958b to your computer and use it in GitHub Desktop.

Select an option

Save pukpr/0b7ac85fad1ea36f65a9b50d6c30958b to your computer and use it in GitHub Desktop.
LTE optimization with MLR on latent factors
#!/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)))
@pukpr
Copy link
Author

pukpr commented Nov 9, 2025

EMI

Name                           Value
----                           -----
ALIGN                          0
EXTRA                          1
FORCING                        0
MAX_ITERS                      60
PYTHONPATH                     ..
RELATIVE                       1
SEMI                           0
STEP                           0.05

 python3 ..\lte_mlr.py .\emi.dat --cc --random --plot --low 1890 --high 1950
emi_1890-1950

{
"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
}

warne_vs_emi_bar2 warne_vs_emi_sin2

AO

ao_1960-2000

{
"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
}

warne_vs_ao_bar warne_vs_ao_sin

Darwin

darwin_0 { "Aliased": [ "0.0537449", "0.1074898", "0.220485143", "1", "2", "0.07771", "0.131456" ], "AliasedAmp": [ -0.11424947850427322, -0.17415122636362612, -0.09387146421412138, -0.036937356707701145, -0.018987052028709636, 0.10385413661627, 0.05830076618852726 ], "AliasedPhase": [ 8.434734540791082, 7.328065914655457, 1.789545024272201, 2.7691790054040366, 11.175830418140741, 10.259843905514588, 4.692000172705181 ], "DC": -0.12423808951744805, "Damp": 0.12434807943037703, "DeltaTime": "3.416666667", "Harmonics": [ 1, 7, 9, 11, 13, 33 ], "Hold": 0.001497241950062863, "Imp_Amp": 199.3838544240002, "Imp_Amp2": -0.7498517268731852, "Imp_Stride": 6, "Initial": 0.10500289401938168, "LTE_Amp": 1.333585491310017, "LTE_Amp2": 0.24475198263665468, "LTE_Freq": 135.79312118936124, "LTE_Freq2": 0.011180925598876339, "LTE_Phase": 0.8841195402917454, "LTE_Phase2": 6.860982494238797, "LTE_Zero": -2.346319244828531, "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.4991832634332175, 0.13006204194389268, 0.099314183593361, -0.1119532539131039, -0.00916985774165216, 0.015441478402155488, -0.041259676962485156, 0.004098963467832427, 0.014303858363245998, -0.017180209307443375, 0.005164982204083786, -0.014775875154360341, -0.011140950678524041, 0.0007104644065088906, -0.022917944023403903, -0.010329919691078545, -0.04730584614164124, -0.00666174866081956, 0.02160876278285345, 0.02542756400407013, -0.021813880659814204, -0.044028002263723144 ], "PeriodsPhase": [ 14.57213478488964, 8.814588001089069, 5.779004182090799, 4.981083413856415, 8.128937711952915, 9.102168601820308, 13.652167518282656, 8.792796485340286, 8.090282942434017, 4.965762988218615, 5.099975134850235, 8.192556754801275, 2.0334172269022184, 5.674382959452959, 6.134006386204361, 7.231623326306104, 4.5431670069536825, 4.327755992103553, 7.59691616373063, 7.380886134482829, 4.636960512051447, 6.64964291030894 ], "Year": "365.2495755" }

@pukpr
Copy link
Author

pukpr commented Nov 9, 2025

Wismar #8

8d_0 { "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 } warne-vs-8d-bar warne-vs-8d-sin 8d_vs_warne_ts

@pukpr
Copy link
Author

pukpr commented Nov 9, 2025

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

169d_0

{
"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
}

warne-vs-169d-bar warne-vs-169d-sin

@pukpr
Copy link
Author

pukpr commented Nov 10, 2025

AO

revisit with adjustable harmonics
ao_1960-2000-1

    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
8d_1
{
"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
}

@pukpr
Copy link
Author

pukpr commented Nov 10, 2025

St. John NB, CA #195

195d_0

{
"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
}

warne-vs-195d-bar warne-vs-195d-sin

Fernandina Beach FL USA #112

112d_0 { "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 } warne-vs-112d-bar warne-vs-112d-sin

Manila, PH #145

145d_0 warne-vs-145d-bar warne-vs-145d-sin

Port Lyttelton NZ #259

259d_0

{
"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
}
warne-vs-259d-bar
warne-vs-259d-sin

@pukpr
Copy link
Author

pukpr commented Nov 10, 2025

QBO30

Kept all tidal factor amplitudes fixed, only the phase to vary to allow alignment, as QBO slides along the timeline depending on altitude

qbo30_0 { "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 } warne_vs_qbo30_bar warne_vs_qbo30_sin
qbo30_1

{
"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
}
warne-vs-qbo30-sin
warne-vs-qbo30-bar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment