Skip to content

Commit 538b072

Browse files
Serialization study
1 parent c294abe commit 538b072

7 files changed

Lines changed: 1226 additions & 62 deletions

File tree

data/perturbation_study.csv

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.

docs/perturbation_study.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
This study explores how perturbation initial values affects the quality of linear fits for models and for species.
44

55
In @data/evaluate_monomial_models-0.01, we see that about 15% of the models have a deg1_min of at least 0.9 on their training data. This study compares these results with perturbations of initial values. In the studies, the training data are unperturbed time courses. Separately, time courses are constructed where initial values have been changed by $\pm 5\%$, $\pm 10\%$, $\pm 20\%$, $\pm 50\%$ with a perturbation species fraction of 1.0. A separate ``Timecourse`` is constructed for each perturbation (at total of 8 plus one for 0%), and then the model trained on unperturbed data is used to predict perturbed data. Please use and/or add capabilities to the class method ``SystemDiscovery.analyzePerturbations`` (formerly ``perturbationAnalysis``) to do this analysis. The result is a CSV file with the columns: model_name, threshold, r2_0, r2_-05, r2_-10, r2_-20, r2_-50, r2_+05, r2_+10, r2_+20, r2_+50. The output file path is new argument to ``analyzePerturbations``.
6+
67
* The arguments to ``analyzePerturbations``
8+
79
* model: Model
810
* training_df: pd.DataFrame
911
* threshold: float

docs/research_agenda.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
# Research Agenda
22

33
## How well can BioModels be modelled by a system of linear differential equaitons?
4+
45
- [x] Fit first order monomial to all models and calculate $R^2$ for species time courses.
5-
- Minimum $R^2$ is model $R^2$
6-
- Individual $R^2$ are for species.
7-
- [x] Evaluate the density of coefficients in the Jacobian.
6+
7+
- Minimum $R^2$ is model $R^2$
8+
- Individual $R^2$ are for species.
9+
10+
- [ ] Evaluate the density of coefficients in the Jacobian.
11+
- [ ] Distributions don't look correct.
812
- [ ] For the linear models, assess their dimensionality to see if dimension reduction is possible
913
- [ ] Characterize the linear models based on the what is being model and possibly other characteristics.
1014

1115
## How robust is linearity to perturbations of initial values?
16+
1217
- [ ] repeat the linear studies with perturbations of $\pm 5\%$, $\pm 19\%$, $\pm 20\%$, and $\pm 50\%$.
1318
- [ ] Can robustness be improved by training the regression on perturbation data?
1419

1520
## What are the main reasons for nonlinear behavior?
21+
1622
- [ ] Analyze the nonlinear models to determine which species are nonlinear and how/when the Jacobian changes to look at reactions.
1723

1824
## Are some nonlinear models piecewise linear?
25+
1926
- [ ] Use a standard package for partitioning regressions to see if linearity can be achieved in segements of the time course.
2027
- [ ] Normalize Jacobians
2128
- [ ] k-means cluster with cluster distances with minimum cluster size.
22-
- [ ] SystemDiscovery for each cluster
23-
- [ ] Prediction using: (a) ${\bf x} (t)$ using SystemDiscover of cluster at $t$ similarly for adjacent time points; (b) apply gaussian kernel on points; (c) denormalize
29+
- [ ] SystemDiscovery for each cluster
30+
- [ ] Prediction using: (a) ${\bf x} (t)$ using SystemDiscover of cluster at $t$ similarly for adjacent time points; (b) apply gaussian kernel on points; (c) denormalize

notebooks/linearity_analysis.ipynb

Lines changed: 688 additions & 30 deletions
Large diffs are not rendered by default.

scripts/perturbation_study.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
R² is computed using the derivative method.
77
88
Output CSV: data/perturbation_study.csv
9-
Columns: model_name, threshold, r2_-50, r2_-20, r2_-10, r2_-05, r2_0,
10-
r2_+05, r2_+10, r2_+20, r2_+50
9+
Columns: model_name, threshold, and for each perturbation level
10+
(r2_-50, r2_-20, r2_-10, r2_-05, r2_0, r2_+05, r2_+10, r2_+20, r2_+50):
11+
three columns _min, _med, _max (clamped derivative R² across species).
1112
"""
1213

1314
import os
1415
import sys
1516

16-
import pandas as pd
17+
import pandas as pd # type: ignore
1718

1819
import src.constants as cn
1920
from src.system_discovery import SystemDiscovery
@@ -26,15 +27,20 @@
2627

2728
SOURCE_PATH = os.path.join(cn.DATA_DIR, "evaluate_monomial_models-0.01.csv")
2829
OUTPUT_PATH = os.path.join(cn.DATA_DIR, "perturbation_study.csv")
29-
MIN_DEG1 = 0.9
30+
MIN_R2 = 0.8
31+
COL_DEG1_MODEL_MAX = "deg1_max"
32+
33+
EXCLUDES = [
34+
"BIOMD0000000718",
35+
]
3036

3137

3238
def main(is_initialize: bool = False) -> pd.DataFrame:
3339
source_df = pd.read_csv(SOURCE_PATH)
3440
model_names: set[str] = set(
35-
source_df.loc[source_df["deg1_min"] >= MIN_DEG1, cn.COL_MODEL_NAME]
41+
source_df.loc[source_df[COL_DEG1_MODEL_MAX] >= MIN_R2, cn.COL_MODEL_NAME]
3642
)
37-
print(f"Models with deg1_min >= {MIN_DEG1}: {len(model_names)}")
43+
print(f"Models ({len(source_df)}) with deg1_min >= {MIN_R2}: {len(model_names)}")
3844

3945
if not is_initialize and os.path.isfile(OUTPUT_PATH):
4046
print(f"Loading existing results from {OUTPUT_PATH}...")
@@ -53,6 +59,9 @@ def main(is_initialize: bool = False) -> pd.DataFrame:
5359
if item.model_name in already_done:
5460
print(f"Skipping {item.model_name} (already processed)", flush=True)
5561
continue
62+
if item.model_name in EXCLUDES:
63+
print(f"Skipping {item.model_name} (excluded)", flush=True)
64+
continue
5665
print(f"Processing {item.model_name}...", flush=True)
5766
try:
5867
series = SystemDiscovery.analyzePerturbations(

src/system_discovery.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import pysindy as ps # type: ignore
4949
from pysindy.feature_library import PolynomialLibrary # type: ignore
5050
from scipy.integrate import solve_ivp # type: ignore
51+
import sys
5152
from typing import Literal, cast
5253
import warnings
5354

@@ -170,7 +171,6 @@ def __init__(
170171
self.species_cols = species_cols
171172
self._X_list: list[np.ndarray] = [d[species_cols].to_numpy(dtype=float) for d in dfs]
172173
self._time_list: list[np.ndarray] = [d.index.to_numpy(dtype=float) for d in dfs]
173-
# First trajectory used for simulation and plotting
174174
self.time_arr: np.ndarray = self._time_list[0]
175175
self.X: np.ndarray = self._X_list[0]
176176
#
@@ -301,17 +301,17 @@ def analyzePerturbations(
301301
poly_degree: int = 1,
302302
frac_keep: float = 0.2,
303303
is_plot: bool = True,
304-
) -> "pd.Series":
304+
) -> pd.Series:
305305
"""Fit on training_df and evaluate derivative R² at each perturbation level.
306306
307307
For each value in *perturbations* a fresh Timecourse is simulated from
308308
*model* with that ``perturbation_value_fraction``. The SINDy model
309309
(fitted on the unperturbed *training_df*) is evaluated against each
310310
perturbed timecourse using the derivative R² method. The reported R²
311-
per perturbation is the minimum clamped R² across all species.
311+
per perturbation are the min, median, and max clamped R² across all species.
312312
313313
When *is_plot* is True, a trajectory comparison figure is also shown
314-
(simulation R² shown in the legend for visual context).
314+
(derivative) R² shown in the legend for visual context).
315315
316316
Parameters
317317
----------
@@ -337,9 +337,9 @@ def analyzePerturbations(
337337
Returns
338338
-------
339339
pd.Series
340-
Index: ``model_name``, ``threshold``, and one ``r2_*`` key per
341-
perturbation value. R² values are the minimum clamped derivative
342-
R² across species, in [0, 1].
340+
Index: ``model_name``, ``threshold``, and three keys per
341+
perturbation value (``r2_*_min``, ``r2_*_med``, ``r2_*_max``).
342+
values are clamped derivative R² across species, in [0, 1].
343343
"""
344344
import src.constants as cn # avoid circular at module level
345345

@@ -368,19 +368,22 @@ def analyzePerturbations(
368368
)
369369
test_df = tc.timecourse_df
370370
r2_dict = disc.calculateRsq(method="derivative", test_df=test_df)
371-
result[col_name] = cls._normalize_rsq(
372-
float(np.min(list(r2_dict.values())))
373-
)
371+
r2_clamped = {k: cls._normalize_rsq(v) for k, v in r2_dict.items()}
372+
vals = list(r2_clamped.values())
373+
result[f"{col_name}_min"] = float(np.min(vals))
374+
result[f"{col_name}_med"] = float(np.median(vals))
375+
result[f"{col_name}_max"] = float(np.max(vals))
374376
if is_plot:
375377
try:
376378
pred_df: pd.DataFrame | None = disc.predict(test_df)
377379
except Exception:
378380
pred_df = None
379-
r2_sim = disc.calculateRsq(method="simulation", test_df=test_df)
380-
plot_records.append((p, test_df, pred_df, r2_sim))
381+
plot_records.append((p, test_df, pred_df, r2_clamped))
381382
except Exception as exc:
382383
print(f" [p={p}] {model.model_name}: {exc}", file=sys.stderr)
383-
result[col_name] = float("nan")
384+
result[f"{col_name}_min"] = float("nan")
385+
result[f"{col_name}_med"] = float("nan")
386+
result[f"{col_name}_max"] = float("nan")
384387

385388
if is_plot and plot_records:
386389
n = len(disc.species_names)
@@ -532,7 +535,7 @@ def plotResult(
532535
pred_df = None
533536
prediction_ok = False
534537

535-
r2_vals = self.calculateRsq(method="simulation", test_df=test_df)
538+
r2_vals = self.calculateRsq(method="derivative", test_df=test_df)
536539

537540
num_skip_point = max(1, len(time_arr) // num_true_point)
538541
for idx, name in enumerate(self.species_names):
@@ -545,7 +548,7 @@ def plotResult(
545548
r2 = r2_vals.get(name, float("nan"))
546549
title = f"{name}"
547550
if not np.isnan(r2):
548-
title += f" R²={r2:.4f}" # may be negative; clamping removed
551+
title += f" R²={r2:.4f}"
549552
low_y = X[:, idx].min()
550553
high_y = X[:, idx].max()
551554
if np.isclose(low_y, high_y):
@@ -678,17 +681,16 @@ def minR2(self, test_df: pd.DataFrame = NULL_DF) -> float:
678681
Parameters
679682
----------
680683
test_df : pd.DataFrame, optional
681-
If provided, R² is evaluated against this DataFrame (simulation
682-
from its initial conditions vs observed). When omitted, the
683-
training data are used.
684+
If provided, R² is evaluated against this DataFrame (derivative)
685+
When omitted, the training data are used.
684686
685687
Returns
686688
-------
687689
float
688690
Minimum R² across species, clamped to [0, 1].
689691
"""
690692
self._require_fitted()
691-
r2_raw = self.calculateRsq(method="simulation", test_df=test_df)
693+
r2_raw = self.calculateRsq(method="derivative", test_df=test_df)
692694
return self._normalize_rsq(float(np.min(list(r2_raw.values()))))
693695

694696
def score(self) -> ScoreInfo:
@@ -967,7 +969,7 @@ def discoverNetwork(
967969
print()
968970

969971
try:
970-
r2_sim = disc.calculateRsq(method="simulation", test_df=test_df)
972+
r2_sim = disc.calculateRsq(method="derivative", test_df=test_df)
971973
print("R² on simulated trajectories per species:")
972974
for name, val in r2_sim.items():
973975
print(f" {name}: {val:.6f}")

tests/test_perturbation_study.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
sys.path.insert(0, os.path.join(cn.PROJECT_DIR, "scripts"))
1717

1818
from perturbation_study import ( # type: ignore # noqa: E402
19-
MIN_DEG1,
19+
MIN_R2,
2020
PERTURBATIONS,
2121
THRESHOLD,
2222
main,

0 commit comments

Comments
 (0)