componavt commited on
Commit
723b602
·
1 Parent(s): 4eb99bf

+ temp old streamlit

Browse files
differential_equations_streamlit_src/archive/gene_regulatory_ODE_system_read_from_pkl.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import pandas as pd
5
+
6
+ # Load saved DataFrame from .pkl file
7
+ @st.cache_data
8
+ def load_data(path="data/013_DOP853_25radius_points171K_round3_float32.pkl"):
9
+ return pd.read_pickle(path)
10
+
11
+ df = load_data()
12
+
13
+ # Extract all unique parameter values
14
+ alpha_list = sorted(df['alpha'].unique())
15
+ gamma1_list = sorted(df['gamma1'].unique())
16
+ gamma2_list = sorted(df['gamma2'].unique())
17
+
18
+ # Sliders and explanation after the plot
19
+ st.markdown("---")
20
+ st.latex(r"""
21
+ \begin{cases}
22
+ \frac{dx}{dt} = \frac{K\,x^{1/\alpha}}{b^{1/\alpha} + x^{1/\alpha}} \;-\; \gamma_1\,x,\\[6pt]
23
+ \frac{dy}{dt} = \frac{K\,y^{1/\alpha}}{b^{1/\alpha} + y^{1/\alpha}} \;-\; \gamma_2\,y.
24
+ \end{cases}
25
+ """)
26
+
27
+ st.markdown("Select parameters to display the solution using solver DOP853.")
28
+
29
+ # Selection widgets
30
+ col1, col2, col3 = st.columns(3)
31
+ with col1:
32
+ alpha = st.selectbox("Alpha (α)", options=alpha_list, format_func=lambda a: f"{a:.0e}")
33
+ with col2:
34
+ gamma1 = st.slider("γ₁ (Gamma 1)", min_value=min(gamma1_list), max_value=max(gamma1_list),
35
+ value=gamma1_list[0], step=gamma1_list[1] - gamma1_list[0])
36
+ with col3:
37
+ gamma2 = st.slider("γ₂ (Gamma 2)", min_value=min(gamma2_list), max_value=max(gamma2_list),
38
+ value=gamma2_list[0], step=gamma2_list[1] - gamma2_list[0])
39
+
40
+ # Filter data for selected parameters
41
+ filtered_df = df[(df['alpha'] == alpha) & (df['gamma1'] == gamma1) & (df['gamma2'] == gamma2)]
42
+
43
+ # Display the plot
44
+ fig, ax = plt.subplots(figsize=(7, 5))
45
+ ax.set_title(f"DOP853, α={alpha:.0e}, γ₁={gamma1}, γ₂={gamma2}", fontsize=11)
46
+ ax.set_xlabel("x(t)", fontsize=9)
47
+ ax.set_ylabel("y(t)", fontsize=9)
48
+ ax.grid(True, linestyle='--', linewidth=0.5)
49
+ ax.tick_params(labelsize=8)
50
+ ax.set_xlim(0.5, 2.0)
51
+ ax.set_ylim(0.5, 2.0)
52
+
53
+ for _, row in filtered_df.iterrows():
54
+ x0 = row['x0']
55
+ y0 = row['y0']
56
+ t = row['t']
57
+ x = row['x']
58
+ y = row['y']
59
+ label_text = f"x₀={x0:.3f}, y₀={y0:.3f}"
60
+ ax.plot(x, y, label=label_text, linewidth=1.5)
61
+ skip = max(3, len(x) // 30)
62
+ x_skip = x[::skip]
63
+ y_skip = y[::skip]
64
+ if len(x_skip) >= 2:
65
+ dx = np.gradient(x_skip)
66
+ dy = np.gradient(y_skip)
67
+ ax.quiver(x_skip, y_skip, dx, dy, angles='xy', scale_units='xy', scale=2.5, width=0.003, alpha=0.5)
68
+ ax.plot(x[0], y[0], marker='o', color='black', markersize=4)
69
+ ax.plot(x[-1], y[-1], marker='x', color='red', markersize=4)
70
+
71
+ ax.legend(fontsize=7, loc='best')
72
+ st.pyplot(fig)
73
+
74
+ # Display note
75
+ st.markdown("**Note:** The start point of the trajectory is marked with a circle (●), and the end point with a cross (×).")
76
+ st.markdown("Initial points (x₀, y₀) are placed on a circle with radius 0.01. There are 25 such initial points in total.")
differential_equations_streamlit_src/core/integrate.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy.integrate import solve_ivp
3
+
4
+
5
+ # Constants for integration
6
+ DEFAULT_SOLVER_METHOD = 'DOP853'
7
+ DEFAULT_TOLERANCE = 1e-9
8
+
9
+
10
+ class BaseSolver:
11
+ """
12
+ Base class for ODE solvers.
13
+ """
14
+ def solve(self, rhs, x0, t_eval):
15
+ raise NotImplementedError("Subclasses must implement solve method")
16
+
17
+
18
+ class SciPySolver(BaseSolver):
19
+ """
20
+ Solver using scipy.integrate.solve_ivp
21
+ """
22
+ def __init__(self, method=DEFAULT_SOLVER_METHOD, rtol=DEFAULT_TOLERANCE, atol=DEFAULT_TOLERANCE):
23
+ self.method = method
24
+ self.rtol = rtol
25
+ self.atol = atol
26
+
27
+ def solve(self, rhs, x0, t_eval):
28
+ """
29
+ Solve ODE using scipy.integrate.solve_ivp
30
+
31
+ Args:
32
+ rhs: Right-hand side function of the ODE system
33
+ x0: Initial conditions
34
+ t_eval: Time points to evaluate the solution
35
+
36
+ Returns:
37
+ Tuple of (solution_successful, x_solution, y_solution)
38
+ """
39
+ try:
40
+ sol = solve_ivp(rhs, (t_eval[0], t_eval[-1]), x0, method=self.method,
41
+ rtol=self.rtol, atol=self.atol, t_eval=t_eval)
42
+ if sol.success:
43
+ return True, sol.y[0], sol.y[1]
44
+ else:
45
+ return False, None, None
46
+ except Exception:
47
+ return False, None, None
48
+
49
+
50
+ class NeuralFlowSolver(BaseSolver):
51
+ """
52
+ Neural network solver that learns the vector field (x, y) -> (dx/dt, dy/dt)
53
+ """
54
+ def __init__(self, model=None, epochs=2000, lr=1e-3):
55
+ self.model = model
56
+ self.epochs = epochs
57
+ self.lr = lr
58
+ self.trained = False
59
+
60
+ def train(self, rhs, x0, t_train, y_train):
61
+ """
62
+ Train the neural network to learn the vector field
63
+
64
+ Args:
65
+ rhs: Right-hand side function of the ODE system (used for generating training data)
66
+ x0: Initial conditions
67
+ t_train: Time points for training
68
+ y_train: Target values for training (derivatives)
69
+ """
70
+ # This is a placeholder implementation - a real implementation would involve
71
+ # training a neural network to approximate the vector field
72
+ # For now, we'll just store the target data
73
+ self.t_train = t_train
74
+ self.y_train = y_train
75
+ self.trained = True
76
+
77
+ def solve(self, rhs, x0, t_eval):
78
+ """
79
+ Solve ODE using the trained neural network
80
+
81
+ Args:
82
+ rhs: Right-hand side function of the ODE system
83
+ x0: Initial conditions
84
+ t_eval: Time points to evaluate the solution
85
+
86
+ Returns:
87
+ Tuple of (solution_successful, x_solution, y_solution)
88
+ """
89
+ if not self.trained:
90
+ raise ValueError("Model must be trained before solving")
91
+
92
+ # This is a placeholder implementation
93
+ # A real implementation would use the trained neural network to solve the ODE
94
+ # For now, we'll fall back to scipy solver if model is not implemented
95
+ try:
96
+ sol = solve_ivp(rhs, (t_eval[0], t_eval[-1]), x0, method=DEFAULT_SOLVER_METHOD, t_eval=t_eval)
97
+ if sol.success:
98
+ return True, sol.y[0], sol.y[1]
99
+ else:
100
+ return False, None, None
101
+ except Exception:
102
+ return False, None, None
differential_equations_streamlit_src/core/metrics.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy.integrate import solve_ivp
3
+
4
+
5
+ def compute_ftle_metrics(rhs, x0, y0, te, t_eval, x, y):
6
+ """
7
+ Computes FTLE (Finite-Time Lyapunov Exponent) and related metrics.
8
+
9
+ Args:
10
+ rhs: Right-hand side function of the ODE system
11
+ x0, y0: Initial conditions
12
+ te: End time
13
+ t_eval: Time points array
14
+ x, y: Solution arrays from the main trajectory
15
+
16
+ Returns:
17
+ tuple: (ftle, final_d, ftle_r2) or (np.nan, np.nan, np.nan) if computation fails
18
+ """
19
+ eps = 1e-6 * (1.0 + abs(x0) + abs(y0))
20
+ xp0, yp0 = x0 + eps, y0 + 0.5 * eps
21
+ try:
22
+ sol_p = solve_ivp(rhs, (0, te), (xp0, yp0), method='DOP853', t_eval=t_eval)
23
+ if sol_p.success:
24
+ xp, yp = sol_p.y
25
+ dist = np.sqrt((x - xp) ** 2 + (y - yp) ** 2)
26
+ dist = np.where(dist <= 0, 1e-12, dist)
27
+ final_d = float(dist[-1])
28
+ s_idx, e_idx = int(0.25 * len(t_eval)), int(0.75 * len(t_eval))
29
+ if e_idx > s_idx + 1:
30
+ d_slice = dist[s_idx:e_idx]
31
+ t_slice = t_eval[s_idx:e_idx]
32
+ d_slice = np.clip(d_slice, 1e-12, None)
33
+ ln_d = np.log(d_slice)
34
+ # linear fit and r2 diagnostics
35
+ slope, intercept = np.polyfit(t_slice, ln_d, 1)
36
+ ftle = float(slope)
37
+ resid = ln_d - (slope * t_slice + intercept)
38
+ ss_res = np.sum(resid ** 2)
39
+ ss_tot = np.sum((ln_d - np.mean(ln_d)) ** 2)
40
+ ftle_r2 = 1 - ss_res / ss_tot if ss_tot > 0 else np.nan
41
+ return ftle, final_d, ftle_r2
42
+ # Return NaN values if computation was unsuccessful
43
+ return np.nan, np.nan, np.nan
44
+ except Exception:
45
+ # Return NaN values in case of exception
46
+ return np.nan, np.nan, np.nan
47
+
48
+
49
+ def hurst_rs(ts):
50
+ """
51
+ Compute the Hurst exponent using the Rescaled Range (R/S) method.
52
+
53
+ Args:
54
+ ts: Time series data
55
+
56
+ Returns:
57
+ float: Hurst exponent or np.nan if computation fails
58
+ """
59
+ x = np.array(ts, dtype=float)
60
+ N = len(x)
61
+ if N < 20:
62
+ return np.nan
63
+ x = x - np.mean(x)
64
+ Y = np.cumsum(x)
65
+ R = np.zeros(N)
66
+ S = np.zeros(N)
67
+ for n in range(10, N // 2 + 1):
68
+ seg = x[:n]
69
+ Yseg = Y[:n]
70
+ Rn = np.max(Yseg) - np.min(Yseg)
71
+ Sn = np.std(seg, ddof=0)
72
+ if Sn > 0:
73
+ R[n - 1] = Rn
74
+ S[n - 1] = Sn
75
+ valid = (S > 0) & (R > 0)
76
+ if np.sum(valid) < 3:
77
+ return np.nan
78
+ rs = R[valid] / S[valid]
79
+ ns = np.arange(1, N + 1)[valid]
80
+ try:
81
+ H = np.polyfit(np.log(ns), np.log(rs), 1)[0]
82
+ except Exception:
83
+ H = np.nan
84
+ return float(H)
85
+
86
+
87
+ def curvature_radius_stats(x, y, t, max_radius=1e6, clip_inf=True):
88
+ """
89
+ Compute robust curvature/radius statistics for a parametric curve (x(t), y(t)).
90
+
91
+ Args:
92
+ x, y: Coordinates of the curve
93
+ t: Parameter values
94
+ max_radius: Maximum radius to consider (values above are clipped)
95
+ clip_inf: Whether to clip infinite/very large radii
96
+
97
+ Returns:
98
+ dict: Dictionary containing various curvature statistics
99
+ """
100
+ x_t = np.gradient(x, t)
101
+ y_t = np.gradient(y, t)
102
+ x_tt = np.gradient(x_t, t)
103
+ y_tt = np.gradient(y_t, t)
104
+ denom = (x_t ** 2 + y_t ** 2) ** 1.5
105
+ num = np.abs(x_t * y_tt - y_t * x_tt)
106
+ with np.errstate(divide='ignore', invalid='ignore'):
107
+ kappa = np.where(denom > 0, num / denom, np.nan)
108
+ radius = np.where(np.isfinite(kappa) & (kappa != 0), 1.0 / kappa, np.nan)
109
+ if clip_inf:
110
+ radius = np.where(radius > max_radius, np.nan, radius)
111
+ finite = np.isfinite(radius)
112
+ stats = {
113
+ "count_total": len(radius),
114
+ "count_finite": int(np.sum(finite)),
115
+ "frac_finite": float(np.sum(finite) / len(radius)),
116
+ "mean": float(np.nanmean(radius)) if np.isfinite(np.nanmean(radius)) else np.nan,
117
+ "median": float(np.nanmedian(radius)) if np.isfinite(np.nanmedian(radius)) else np.nan,
118
+ "p10": float(np.nanpercentile(radius, 10)) if np.isfinite(np.nanpercentile(radius, 10)) else np.nan,
119
+ "p90": float(np.nanpercentile(radius, 90)) if np.isfinite(np.nanpercentile(radius, 90)) else np.nan,
120
+ "std": float(np.nanstd(radius)) if np.isfinite(np.nanstd(radius)) else np.nan,
121
+ "radius_array": radius,
122
+ "kappa_array": (1.0 / radius) # may contain inf/nan for radius==0
123
+ }
124
+ return stats
125
+
126
+
127
+ def compute_path_length(x, y):
128
+ """
129
+ Compute the total path length of a curve (x(t), y(t)).
130
+
131
+ Args:
132
+ x, y: Coordinates of the curve
133
+
134
+ Returns:
135
+ float: Total path length
136
+ """
137
+ dx = np.diff(x)
138
+ dy = np.diff(y)
139
+ seg_lengths = np.sqrt(dx * dx + dy * dy)
140
+ return float(np.sum(seg_lengths))
141
+
142
+
143
+ # Constants for metrics computation
144
+ EPSILON = 1e-12
145
+ FTLE_START_FRAC = 0.25
146
+ FTLE_END_FRAC = 0.75
147
+ HURST_MIN_SIZE = 20
148
+ CURVATURE_RADIUS_MAX = 1e6
149
+
150
+ def compute_anomaly_score(ftle, path_len, max_kappa, ftle_r2, hurst=None):
151
+ """
152
+ Compute an anomaly score combining multiple indicators.
153
+
154
+ Args:
155
+ ftle: Finite-Time Lyapunov Exponent
156
+ path_len: Path length
157
+ max_kappa: Maximum curvature
158
+ ftle_r2: R^2 of FTLE fit
159
+ hurst: Hurst exponent (optional)
160
+
161
+ Returns:
162
+ float: Anomaly score
163
+ """
164
+ # Normalize inputs using robust z-scores (using median and IQR)
165
+ def robust_z_single(value, median, iqr):
166
+ if iqr == 0:
167
+ return 0.0
168
+ return (value - median) / iqr
169
+
170
+ # In a real implementation, we'd compute medians and IQRs from a dataset
171
+ # For now, we'll use placeholder normalization factors
172
+ ftle_norm = ftle # Would be normalized in practice
173
+ path_norm = path_len # Would be normalized in practice
174
+ kappa_norm = max_kappa # Would be normalized in practice
175
+ r2_norm = ftle_r2 # Would be normalized in practice
176
+
177
+ # Basic anomaly score combining multiple indicators
178
+ score = ftle_norm + path_norm + kappa_norm
179
+
180
+ # Penalize low reliability (low r2)
181
+ if not np.isnan(ftle_r2):
182
+ score -= r2_norm
183
+
184
+ # Include Hurst exponent if provided
185
+ if hurst is not None and not np.isnan(hurst):
186
+ score += hurst # Adjust weight as needed
187
+
188
+ return score
differential_equations_streamlit_src/core/ode.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ def gene_regulatory_rhs(alpha_val, K_val, b_val, g1_val, g2_val):
5
+ """
6
+ Create the right-hand side function for the gene regulatory ODE system.
7
+
8
+ The system is:
9
+ dx/dt = (K * x^(1/alpha))/(b^(1/alpha) + x^(1/alpha)) - gamma1 * x
10
+ dy/dt = (K * y^(1/alpha))/(b^(1/alpha) + y^(1/alpha)) - gamma2 * y
11
+ """
12
+ def rhs(t, state):
13
+ x, y = state
14
+ n = 1.0 / alpha_val
15
+ if n > 1000:
16
+ # Handle very large n (approaching infinity) case
17
+ frac_x = K_val if x > b_val else 0.0
18
+ frac_y = K_val if y > b_val else 0.0
19
+ else:
20
+ x_pos, y_pos = max(x, 0.0), max(y, 0.0)
21
+ try:
22
+ pow_b = np.power(b_val, n)
23
+ pow_x = np.power(x_pos, n)
24
+ pow_y = np.power(y_pos, n)
25
+ frac_x = (K_val * pow_x) / (pow_b + pow_x) if np.isfinite(pow_x) else (K_val if x > b_val else 0.0)
26
+ frac_y = (K_val * pow_y) / (pow_b + pow_y) if np.isfinite(pow_y) else (K_val if y > b_val else 0.0)
27
+ except Exception:
28
+ frac_x, frac_y = (K_val if x > b_val else 0.0), (K_val if y > b_val else 0.0)
29
+ return [frac_x - g1_val * x, frac_y - g2_val * y]
30
+
31
+ return rhs
32
+
33
+
34
+ def lotka_volterra_rhs(a, b, k, m):
35
+ """
36
+ Create the right-hand side function for the Lotka-Volterra ODE system.
37
+
38
+ The system is:
39
+ dx/dt = x * (a - b * y)
40
+ dy/dt = y * (k * b * x - m)
41
+ """
42
+ def rhs(t, state):
43
+ x, y = state
44
+ return [x * (a - b * y), y * (k * b * x - m)]
45
+
46
+ return rhs
differential_equations_streamlit_src/core/pinn.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ try:
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.optim as optim
6
+ TORCH_AVAILABLE = True
7
+ except ImportError:
8
+ TORCH_AVAILABLE = False
9
+
10
+
11
+ if TORCH_AVAILABLE:
12
+ class PhysicsInformedNeuralNetwork(nn.Module):
13
+ """
14
+ Physics-Informed Neural Network (PINN) for learning the vector field of an ODE system.
15
+ Instead of learning the solution t -> (x(t), y(t)), this learns the vector field (x, y) -> (dx/dt, dy/dt).
16
+ """
17
+ def __init__(self, hidden_size=64):
18
+ super().__init__()
19
+ self.net = nn.Sequential(
20
+ nn.Linear(2, hidden_size), # Input: (x, y)
21
+ nn.Tanh(),
22
+ nn.Linear(hidden_size, hidden_size),
23
+ nn.Tanh(),
24
+ nn.Linear(hidden_size, hidden_size),
25
+ nn.Tanh(),
26
+ nn.Linear(hidden_size, 2), # Output: (dx/dt, dy/dt)
27
+ )
28
+
29
+ def forward(self, xy):
30
+ """
31
+ Forward pass: (x, y) -> (dx/dt, dy/dt)
32
+ """
33
+ return self.net(xy)
34
+
35
+
36
+ def train_pinn(rhs_func, x0, y0, t_train, initial_conditions=None, epochs=2000, lr=1e-3):
37
+ """
38
+ Train a PINN to learn the vector field of the ODE system.
39
+
40
+ Args:
41
+ rhs_func: Right-hand side function of the ODE system that returns [dx/dt, dy/dt]
42
+ x0, y0: Initial conditions
43
+ t_train: Time points for training
44
+ initial_conditions: Additional initial conditions for training (optional)
45
+ epochs: Number of training epochs
46
+ lr: Learning rate
47
+
48
+ Returns:
49
+ Trained PINN model
50
+ """
51
+ # Generate training data by evaluating the known RHS function
52
+ # This simulates having access to the derivative values for training
53
+ xy_train = []
54
+ dydt_train = []
55
+
56
+ # For each initial condition and time point, generate training pairs
57
+ for t in t_train:
58
+ # For a PINN, we want to learn the general vector field function
59
+ # So we'll generate training points by evaluating the RHS at various (x,y) positions
60
+ # For simplicity, we'll use the actual solution points plus some variations
61
+
62
+ # Get the solution at time t using the original solver (this is for generating training data)
63
+ # In a real PINN, we'd rely more on the physics constraints rather than exact solution points
64
+ from scipy.integrate import solve_ivp
65
+ sol = solve_ivp(rhs_func, (0, t), [x0, y0], method='DOP853', t_eval=[t])
66
+
67
+ if sol.success and len(sol.y[0]) > 0:
68
+ x_t, y_t = sol.y[0][-1], sol.y[1][-1]
69
+
70
+ # Evaluate the RHS at this point to get the true derivatives
71
+ true_derivatives = rhs_func(None, [x_t, y_t])
72
+
73
+ # Add this as a training sample
74
+ xy_train.append([x_t, y_t])
75
+ dydt_train.append(true_derivatives)
76
+
77
+ # Convert to tensors
78
+ xy_tensor = torch.tensor(xy_train, dtype=torch.float32)
79
+ dydt_tensor = torch.tensor(dydt_train, dtype=torch.float32)
80
+
81
+ # Initialize model
82
+ model = PhysicsInformedNeuralNetwork()
83
+ optimizer = optim.Adam(model.parameters(), lr=lr)
84
+ loss_fn = nn.MSELoss()
85
+
86
+ # Training loop
87
+ for epoch in range(epochs):
88
+ optimizer.zero_grad()
89
+
90
+ # Predict derivatives
91
+ pred_dydt = model(xy_tensor)
92
+
93
+ # Compute loss
94
+ loss = loss_fn(pred_dydt, dydt_tensor)
95
+
96
+ # Backpropagate
97
+ loss.backward()
98
+ optimizer.step()
99
+
100
+ if epoch % 500 == 0:
101
+ print(f"Epoch {epoch}, Loss: {loss.item():.6f}")
102
+
103
+ return model
104
+
105
+
106
+ def predict_with_pinn(model, initial_condition, t_eval):
107
+ """
108
+ Solve the ODE using the trained PINN by integrating the learned vector field.
109
+
110
+ Args:
111
+ model: Trained PINN model
112
+ initial_condition: Starting point [x0, y0]
113
+ t_eval: Time points to evaluate
114
+
115
+ Returns:
116
+ x_pred, y_pred arrays
117
+ """
118
+ if model is None:
119
+ return None, None
120
+
121
+ # Use scipy integrator with the learned vector field
122
+ def learned_rhs(t, state):
123
+ with torch.no_grad():
124
+ state_tensor = torch.tensor(state, dtype=torch.float32).reshape(1, -1)
125
+ deriv_tensor = model(state_tensor)
126
+ derivatives = deriv_tensor.numpy().flatten()
127
+ return derivatives
128
+
129
+ from scipy.integrate import solve_ivp
130
+ sol = solve_ivp(learned_rhs, (t_eval[0], t_eval[-1]), initial_condition,
131
+ method='DOP853', t_eval=t_eval)
132
+
133
+ if sol.success:
134
+ return sol.y[0], sol.y[1]
135
+ else:
136
+ return None, None
137
+ else:
138
+ def train_pinn(rhs_func, x0, y0, t_train, initial_conditions=None, epochs=2000, lr=1e-3):
139
+ """
140
+ Placeholder function when torch is not available
141
+ """
142
+ print("PyTorch not available, skipping PINN training")
143
+ return None
144
+
145
+
146
+ def predict_with_pinn(model, initial_condition, t_eval):
147
+ """
148
+ Placeholder function when torch is not available
149
+ """
150
+ return None, None
differential_equations_streamlit_src/core/shadowing.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+
4
+ # Constants for shadowing analysis
5
+ SHADOWING_EPSILON_THRESHOLD = 1e-3
6
+
7
+
8
+ def compute_epsilon_t(distances):
9
+ """
10
+ Compute epsilon(t) as the running maximum of distances.
11
+
12
+ Args:
13
+ distances: Array of distances between two trajectories at each time point
14
+
15
+ Returns:
16
+ Array representing epsilon(t) - the running maximum of distances
17
+ """
18
+ return np.maximum.accumulate(distances)
19
+
20
+
21
+ def find_shadowing_breakdown_time(epsilon_t, t_eval, epsilon_threshold=SHADOWING_EPSILON_THRESHOLD):
22
+ """
23
+ Find the shadowing breakdown time t* where epsilon(t) exceeds a threshold.
24
+
25
+ Args:
26
+ epsilon_t: Array of epsilon(t) values (running maximum of distances)
27
+ t_eval: Time points corresponding to epsilon(t) values
28
+ epsilon_threshold: Threshold above which shadowing is considered broken
29
+
30
+ Returns:
31
+ tuple: (shadowing_time, shadowing_length, shadowing_ratio)
32
+ shadowing_time: Time t* where epsilon(t) > epsilon_threshold (or None if never exceeded)
33
+ shadowing_length: Length of time where epsilon(t) <= epsilon_threshold
34
+ shadowing_ratio: Ratio of valid shadowing time to total time
35
+ """
36
+ if len(epsilon_t) != len(t_eval):
37
+ raise ValueError("epsilon_t and t_eval must have the same length")
38
+
39
+ # Find the first time where epsilon(t) exceeds the threshold
40
+ exceed_indices = np.where(epsilon_t > epsilon_threshold)[0]
41
+
42
+ if len(exceed_indices) == 0:
43
+ # If threshold is never exceeded, shadowing holds for the entire duration
44
+ shadowing_time = None # Indicates no breakdown occurred
45
+ shadowing_length = t_eval[-1] - t_eval[0]
46
+ shadowing_ratio = 1.0
47
+ else:
48
+ # Take the first occurrence where threshold is exceeded
49
+ first_exceed_idx = exceed_indices[0]
50
+ shadowing_time = t_eval[first_exceed_idx]
51
+ shadowing_length = t_eval[first_exceed_idx] - t_eval[0]
52
+ shadowing_ratio = shadowing_length / (t_eval[-1] - t_eval[0])
53
+
54
+ return shadowing_time, shadowing_length, shadowing_ratio
55
+
56
+
57
+ def compute_shadowing_diagnostics(dop_solution, nn_solution, t_eval, epsilon_threshold=SHADOWING_EPSILON_THRESHOLD):
58
+ """
59
+ Compute comprehensive shadowing diagnostics by comparing two solutions.
60
+
61
+ Args:
62
+ dop_solution: Dictionary with 'x' and 'y' arrays from DOP853 solver
63
+ nn_solution: Dictionary with 'x' and 'y' arrays from neural network solver
64
+ t_eval: Time points array
65
+ epsilon_threshold: Threshold for shadowing breakdown
66
+
67
+ Returns:
68
+ dict: Dictionary containing shadowing diagnostics
69
+ """
70
+ # Calculate distance between DOP853 and NN solutions
71
+ dist = np.sqrt((dop_solution['x'] - nn_solution['x'])**2 + (dop_solution['y'] - nn_solution['y'])**2)
72
+
73
+ # Calculate epsilon(t) as the running maximum (sup norm)
74
+ epsilon_t = compute_epsilon_t(dist)
75
+
76
+ # Calculate shadowing breakdown diagnostics
77
+ shadowing_time, shadowing_length, shadowing_ratio = find_shadowing_breakdown_time(
78
+ epsilon_t, t_eval, epsilon_threshold
79
+ )
80
+
81
+ return {
82
+ 'epsilon_t': epsilon_t,
83
+ 'distances': dist,
84
+ 'shadowing_time': shadowing_time,
85
+ 'shadowing_length': shadowing_length,
86
+ 'shadowing_ratio': shadowing_ratio,
87
+ 'epsilon_threshold': epsilon_threshold,
88
+ 'has_breakdown': shadowing_time is not None
89
+ }
differential_equations_streamlit_src/gene_regulatory_ODE_system.py ADDED
@@ -0,0 +1,627 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.lines import Line2D
5
+ import pandas as pd
6
+
7
+ from core.ode import gene_regulatory_rhs
8
+ from core.integrate import SciPySolver, NeuralFlowSolver
9
+ from core.metrics import compute_ftle_metrics, hurst_rs, curvature_radius_stats, compute_path_length, compute_anomaly_score
10
+ from core.shadowing import compute_shadowing_diagnostics
11
+ from core.pinn import train_pinn, predict_with_pinn
12
+ from utils.plain_text_parameters import parameters_to_text, text_to_parameters
13
+ from utils.highlight_extreme_values_in_table import highlight_extreme_values_in_table
14
+ import streamlit.components.v1 as components
15
+
16
+ # --------------------------------------------------
17
+ # Main Streamlit App for Gene Regulatory ODE System
18
+ # --------------------------------------------------
19
+
20
+ # Default parameter values
21
+ DEFAULTS = {
22
+ "alpha": 0.001,
23
+ "K": 1.0,
24
+ "b": 1.0,
25
+ "gamma1": 1.0,
26
+ "gamma2": 1.0,
27
+ "initial_radius": 0.01,
28
+ "num_points": 12,
29
+ "t_number": 100,
30
+ "t_train_end": 1.0,
31
+ "t_full_end": 3.0,
32
+ "circle_start_end": (0, 360),
33
+ }
34
+
35
+ # --- Sidebar: widgets ---
36
+ st.sidebar.header("Simulation Settings")
37
+
38
+ # Time / solver resolution
39
+ t_number = st.sidebar.slider("t_number", min_value=10, max_value=1000, step=10,
40
+ value=int(DEFAULTS["t_number"]))
41
+ t_train_end = st.sidebar.slider("Training interval end (t_train_end)", 0.1, 5.0, float(DEFAULTS["t_train_end"]), 0.1)
42
+ t_full_end = st.sidebar.slider("Full integration end (t_full_end)", t_train_end + 0.1, 10.0, float(DEFAULTS["t_full_end"]), 0.1)
43
+
44
+ alpha = st.sidebar.number_input("alpha (1/alpha exponent)", min_value=1e-9, max_value=10.0,
45
+ value=float(DEFAULTS["alpha"]), format="%g")
46
+ K = st.sidebar.slider("K", min_value=0.1, max_value=5.0, step=0.1,
47
+ value=float(DEFAULTS["K"]))
48
+ b = st.sidebar.number_input("b", min_value=0.0, max_value=10.0,
49
+ value=float(DEFAULTS["b"]), format="%g")
50
+
51
+ gamma1 = st.sidebar.number_input("gamma1", min_value=0.0, max_value=10.0,
52
+ value=float(DEFAULTS["gamma1"]), format="%g")
53
+ gamma2 = st.sidebar.number_input("gamma2", min_value=0.0, max_value=10.0,
54
+ value=float(DEFAULTS["gamma2"]), format="%g")
55
+
56
+ initial_radius = st.sidebar.number_input("Initial radius (R)", min_value=0.0, max_value=10.0,
57
+ value=float(DEFAULTS["initial_radius"]), format="%g")
58
+ num_points = st.sidebar.slider("Number of trajectories", min_value=3, max_value=50, step=1,
59
+ value=int(DEFAULTS["num_points"]))
60
+
61
+ circle_start_end = st.sidebar.slider("Sector on circle (degrees)", 0, 360,
62
+ tuple(map(int, DEFAULTS["circle_start_end"])), step=1)
63
+
64
+ # Add controllable center of the initial circle
65
+ center_x = st.sidebar.number_input("Center X (circle center)", value=float(b), format="%g")
66
+ center_y = st.sidebar.number_input("Center Y (circle center)", value=float(b), format="%g")
67
+
68
+ # Add solver selection
69
+ solver_type = st.sidebar.selectbox(
70
+ "Select Solver Type",
71
+ ("SciPy DOP853", "PINN (Physics-Informed Neural Network)"),
72
+ index=0,
73
+ help="Choose the solver for the ODE system"
74
+ )
75
+
76
+ # --- Plain text area and two buttons ---
77
+
78
+ def collect_params_from_widgets():
79
+ cs, ce = circle_start_end
80
+ params = {
81
+ "t_number": int(t_number),
82
+ "t_train_end": float(t_train_end),
83
+ "t_full_end": float(t_full_end),
84
+ "alpha": float(alpha),
85
+ "K": float(K),
86
+ "b": float(b),
87
+ "gamma1": float(gamma1),
88
+ "gamma2": float(gamma2),
89
+ "initial_radius": float(initial_radius),
90
+ "num_points": int(num_points),
91
+ "circle_start": int(cs),
92
+ "circle_end": int(ce),
93
+ "center_x": float(center_x),
94
+ "center_y": float(center_y),
95
+ "solver_type": solver_type,
96
+ }
97
+ # Save only enabled checkboxes if available
98
+ if "enabled_checkboxes" in st.session_state:
99
+ enabled = st.session_state.enabled_checkboxes
100
+ if enabled:
101
+ params["enabled_idx"] = ",".join(map(str, enabled))
102
+ return params
103
+
104
+ if "params_text" not in st.session_state:
105
+ st.session_state.params_text = parameters_to_text(collect_params_from_widgets())
106
+
107
+ st.sidebar.markdown("**Parameters (plain text)**")
108
+ params_text = st.sidebar.text_area("Edit parameters here:", value=st.session_state.params_text, height=20, key="params_text")
109
+
110
+ # Callback: parse text and apply to widgets
111
+ def apply_text_to_sliders():
112
+ parsed = text_to_parameters(st.session_state.params_text)
113
+ if not parsed:
114
+ return
115
+ int_keys = {"t_number", "num_points", "circle_start", "circle_end"}
116
+ float_keys = {"t_train_end", "t_full_end", "alpha", "K", "b", "gamma1", "gamma2", "initial_radius", "center_x", "center_y"}
117
+
118
+ for key, val in parsed.items():
119
+ if key in int_keys:
120
+ try:
121
+ st.session_state[key] = int(val)
122
+ except Exception:
123
+ pass
124
+ elif key in float_keys:
125
+ try:
126
+ st.session_state[key] = float(val)
127
+ except Exception:
128
+ pass
129
+ elif key == "enabled_idx":
130
+ enabled_idx = [int(x) for x in str(val).split(",") if x.strip().isdigit()]
131
+ npts = int(parsed.get("num_points", st.session_state.get("num_points", num_points)))
132
+ enabled_idx = [i for i in enabled_idx if 0 <= i < npts]
133
+ st.session_state.enabled_checkboxes = enabled_idx
134
+ elif key == "solver_type":
135
+ st.session_state["solver_type"] = val
136
+
137
+ if "circle_start" in parsed or "circle_end" in parsed:
138
+ cs = int(parsed.get("circle_start", circle_start_end[0]))
139
+ ce = int(parsed.get("circle_end", circle_start_end[1]))
140
+ st.session_state.circle_start_end = (cs, ce)
141
+
142
+ if "num_points" in parsed:
143
+ npts = int(parsed["num_points"])
144
+ enabled = st.session_state.get("enabled_checkboxes", [])
145
+ st.session_state.enabled_checkboxes = [i for i in enabled if 0 <= i < npts]
146
+
147
+ st.session_state.params_text = parameters_to_text(collect_params_from_widgets())
148
+
149
+ # Callback: read widget values and put into text area
150
+ def read_sliders_to_text():
151
+ st.session_state.params_text = parameters_to_text(collect_params_from_widgets())
152
+
153
+ col_apply, col_read = st.sidebar.columns(2)
154
+ col_apply.button("Apply text → sliders", on_click=apply_text_to_sliders)
155
+ col_read.button("Read sliders → text", on_click=read_sliders_to_text)
156
+
157
+ # --- Build angle array ---
158
+ cs_val, ce_val = circle_start_end
159
+ span = (ce_val - cs_val) % 360
160
+ if np.isclose(span, 0.0):
161
+ angles = np.linspace(0, 2 * np.pi, num_points, endpoint=False)
162
+ else:
163
+ cs = cs_val % 360
164
+ ce = ce_val % 360
165
+ if ce >= cs:
166
+ degs = np.linspace(cs, ce, num_points, endpoint=False)
167
+ else:
168
+ span2 = (ce + 360) - cs
169
+ degs = (cs + np.linspace(0, span2, num_points, endpoint=False)) % 360
170
+ angles = np.deg2rad(degs)
171
+
172
+ # --- Prepare solver settings ---
173
+ alpha_val = float(alpha)
174
+ K_val = float(K)
175
+ b_val = float(b)
176
+ g1_val = float(gamma1)
177
+ g2_val = float(gamma2)
178
+ R_val = float(initial_radius)
179
+
180
+ tn = int(t_number)
181
+ tte = float(t_train_end)
182
+ tfe = float(t_full_end)
183
+
184
+ # --- Create initial conditions from parameters ---
185
+ initial_conditions = []
186
+ for angle in angles:
187
+ x0 = center_x + R_val * np.cos(angle)
188
+ y0 = center_y + R_val * np.sin(angle)
189
+ initial_conditions.append((x0, y0))
190
+
191
+ # --- Create ODE RHS function ---
192
+ rhs_func = gene_regulatory_rhs(alpha_val, K_val, b_val, g1_val, g2_val)
193
+
194
+ # --- Main computation ---
195
+ solutions, metrics = [], []
196
+
197
+ # Main loop: solve for each initial condition
198
+ for idx, (x0, y0) in enumerate(initial_conditions):
199
+ # Solve ODE on full interval [0, t_full_end]
200
+ t_eval_full = np.linspace(0, tfe, tn)
201
+
202
+ # Select and configure solver
203
+ if solver_type == "SciPy DOP853":
204
+ solver = SciPySolver(method='DOP853')
205
+ success, x_full, y_full = solver.solve(rhs_func, (x0, y0), t_eval_full)
206
+ else: # PINN
207
+ # For PINN, we need to train first on the full interval
208
+ solver = NeuralFlowSolver() # Placeholder - in reality, PINN would be handled differently
209
+ # For now, we'll use SciPy solver as fallback and note that PINN implementation is more complex
210
+ scipy_solver = SciPySolver(method='DOP853')
211
+ success, x_full, y_full = scipy_solver.solve(rhs_func, (x0, y0), t_eval_full)
212
+
213
+ # Train PINN separately if needed
214
+ try:
215
+ pinn_model = train_pinn(rhs_func, x0, y0, t_eval_full, epochs=1000)
216
+ pinn_x_full, pinn_y_full = predict_with_pinn(pinn_model, [x0, y0], t_eval_full)
217
+ except Exception as e:
218
+ st.warning(f"PINN training failed: {str(e)}, falling back to SciPy solver")
219
+ pinn_x_full, pinn_y_full = x_full, y_full
220
+
221
+ if not success or x_full is None or y_full is None:
222
+ # If primary solver failed, try alternative
223
+ scipy_solver = SciPySolver(method='RK45')
224
+ success, x_full, y_full = scipy_solver.solve(rhs_func, (x0, y0), t_eval_full)
225
+ if not success or x_full is None or y_full is None:
226
+ st.warning(f"Solver failed for initial condition {idx}: ({x0}, {y0})")
227
+ continue
228
+
229
+ # For PINN, use the PINN solution if available
230
+ if solver_type == "PINN (Physics-Informed Neural Network)" and 'pinn_x_full' in locals():
231
+ x_full, y_full = pinn_x_full, pinn_y_full
232
+
233
+ # Get solution on training interval [0, t_train_end] for neural network training
234
+ t_eval_train = np.linspace(0, tte, max(50, tn//2)) # Use fewer points for training
235
+ scipy_train_solver = SciPySolver(method='DOP853')
236
+ train_success, x_train, y_train = scipy_train_solver.solve(rhs_func, (x0, y0), t_eval_train)
237
+ if not train_success:
238
+ continue
239
+
240
+ # Train neural network on [0, t_train_end] - keeping the original neural ODE approach
241
+ from utils.neural_ode_solver import train_neural_ode, predict_with_neural_ode
242
+ nn_model = train_neural_ode(t_eval_train, x_train, y_train, epochs=10)
243
+ x_nn_full, y_nn_full = predict_with_neural_ode(nn_model, t_eval_full)
244
+
245
+ # Store solutions for plotting
246
+ solutions.append({
247
+ "dop853_x": x_full,
248
+ "dop853_y": y_full,
249
+ "nn_x": x_nn_full,
250
+ "nn_y": y_nn_full,
251
+ "t_full": t_eval_full
252
+ })
253
+
254
+ # Calculate metrics using the primary solution on the full interval
255
+ amp = float(np.max(np.sqrt(x_full * x_full + y_full * y_full)) - np.min(np.sqrt(x_full * x_full + y_full * y_full)))
256
+
257
+ ftle, final_d, ftle_r2 = compute_ftle_metrics(rhs_func, x0, y0, tfe, t_eval_full, x_full, y_full)
258
+
259
+ # Hurst: compute for x and y and take mean
260
+ hx = hurst_rs(x_full)
261
+ hy = hurst_rs(y_full)
262
+ hurst_val = np.nanmean([hx, hy])
263
+
264
+ # curvature radius stats
265
+ cr_stats = curvature_radius_stats(x_full, y_full, t_eval_full)
266
+ curv_mean = cr_stats["mean"]
267
+ curv_median = cr_stats["median"]
268
+ curv_std = cr_stats["std"]
269
+
270
+ # path length
271
+ path_len = compute_path_length(x_full, y_full)
272
+
273
+ # maximum curvature (kappa) and fraction of high curvature points
274
+ kappa_arr = cr_stats.get("kappa_array")
275
+ # kappa_array may include inf/nan; handle robustly
276
+ kappa_vals = np.array(kappa_arr)
277
+ kappa_vals = kappa_vals[np.isfinite(kappa_vals)] if kappa_vals is not None else np.array([])
278
+ max_kappa = float(np.nanmax(kappa_vals)) if kappa_vals.size > 0 else np.nan
279
+ # fraction of points with radius < 10 -> kappa > 0.1 (example threshold)
280
+ frac_high_curv = float(np.sum(kappa_vals > 0.1) / len(t_eval_full)) if kappa_vals.size > 0 else np.nan
281
+
282
+ metrics.append({
283
+ "idx": idx,
284
+ "ftle": ftle,
285
+ "ftle_r2": ftle_r2,
286
+ "amp": amp,
287
+ "final_dist": final_d,
288
+ "hurst": hurst_val,
289
+ "curv_radius_mean": curv_mean,
290
+ "curv_radius_median": curv_median,
291
+ "curv_radius_std": curv_std,
292
+ "curv_p10": cr_stats["p10"],
293
+ "curv_p90": cr_stats["p90"],
294
+ "curv_count_finite": cr_stats["count_finite"],
295
+ "initial_x": float(x0),
296
+ "initial_y": float(y0),
297
+ "path_len": path_len,
298
+ "max_kappa": max_kappa,
299
+ "frac_high_curv": frac_high_curv,
300
+ })
301
+
302
+
303
+ # Compute local z-score of curvature median versus nearest neighbours (fallback without sklearn available)
304
+ try:
305
+ from sklearn.neighbors import NearestNeighbors
306
+ use_sklearn = True
307
+ except Exception:
308
+ use_sklearn = False
309
+
310
+ if metrics:
311
+ arr_init = np.array([[m["initial_x"], m["initial_y"]] for m in metrics])
312
+ rad_meds = np.array([m["curv_radius_median"] for m in metrics])
313
+ local_z = np.full(len(metrics), np.nan)
314
+ if len(metrics) > 1:
315
+ nbrs_k = min(5, len(metrics) - 1)
316
+ if use_sklearn:
317
+ nbrs = NearestNeighbors(n_neighbors=nbrs_k + 1).fit(arr_init)
318
+ distances, indices = nbrs.kneighbors(arr_init)
319
+ for i in range(len(metrics)):
320
+ neigh_idx = indices[i, 1:]
321
+ neigh_vals = rad_meds[neigh_idx]
322
+ neigh_vals = neigh_vals[np.isfinite(neigh_vals)]
323
+ if not np.isfinite(rad_meds[i]) or len(neigh_vals) < 1:
324
+ local_z[i] = np.nan
325
+ else:
326
+ mu = np.mean(neigh_vals)
327
+ sigma = np.std(neigh_vals)
328
+ local_z[i] = (rad_meds[i] - mu) / sigma if sigma != 0 else np.nan
329
+ else:
330
+ for i in range(len(metrics)):
331
+ dists = np.linalg.norm(arr_init - arr_init[i : i + 1], axis=1)
332
+ order = np.argsort(dists)
333
+ neigh_idx = order[1 : 1 + nbrs_k]
334
+ neigh_vals = rad_meds[neigh_idx]
335
+ neigh_vals = neigh_vals[np.isfinite(neigh_vals)]
336
+ if not np.isfinite(rad_meds[i]) or len(neigh_vals) < 1:
337
+ local_z[i] = np.nan
338
+ else:
339
+ mu = np.mean(neigh_vals)
340
+ sigma = np.std(neigh_vals)
341
+ local_z[i] = (rad_meds[i] - mu) / sigma if sigma != 0 else np.nan
342
+ for i in range(len(metrics)):
343
+ metrics[i]["curv_radius_local_zscore"] = float(local_z[i]) if np.isfinite(local_z[i]) else np.nan
344
+
345
+ # Build dataframe
346
+ df_metrics = pd.DataFrame(metrics)
347
+
348
+ # --- Compute anomaly score (combine multiple indicators) ---
349
+ # Prepare columns for scoring: ftle (higher), path_len (higher), max_kappa (higher), ftle_r2 (higher means reliable)
350
+ # We'll compute robust z-scores (subtract median, divide by IQR) to avoid influence of outliers
351
+
352
+ def robust_z(arr):
353
+ arr = np.array(arr, dtype=float)
354
+ finite = np.isfinite(arr)
355
+ out = np.full_like(arr, np.nan)
356
+ if np.sum(finite) == 0:
357
+ return out
358
+ median = np.nanmedian(arr[finite])
359
+ q1 = np.nanpercentile(arr[finite], 25)
360
+ q3 = np.nanpercentile(arr[finite], 75)
361
+ iqr = q3 - q1 if q3 - q1 != 0 else 1.0
362
+ out[finite] = (arr[finite] - median) / iqr
363
+ return out
364
+
365
+ if not df_metrics.empty:
366
+ ftle_z = robust_z(df_metrics['ftle'].values)
367
+ path_z = robust_z(df_metrics['path_len'].values)
368
+ kappa_z = robust_z(df_metrics['max_kappa'].values)
369
+ r2_z = robust_z(df_metrics['ftle_r2'].fillna(0).values)
370
+ hurst_z = robust_z(df_metrics['hurst'].fillna(0).values)
371
+ # score = ftle_z + path_z + kappa_z - r2_z + hurst_z (penalize low r2 by subtracting its z, include hurst)
372
+ score_arr = ftle_z + path_z + kappa_z - r2_z + hurst_z
373
+ df_metrics['anomaly_score'] = score_arr
374
+
375
+ # --- Selection UI (checkbox list, sorted by anomaly_score desc) ---
376
+ selected_idx = []
377
+ st.sidebar.markdown("**Select trajectories to display (sorted by anomaly score)**")
378
+
379
+ # Checkbox for connecting lines between DOP853 and NN
380
+ show_connections = st.sidebar.checkbox("Show connections (DOP853 ↔ NN)", value=False, key="show_connections")
381
+
382
+ # Connection stride parameter
383
+ if show_connections:
384
+ connection_stride = st.sidebar.slider("Connection stride", min_value=1, max_value=20, value=5, step=1)
385
+ else:
386
+ connection_stride = 5 # default value when not showing connections
387
+
388
+ if not df_metrics.empty:
389
+ df_sorted = df_metrics.sort_values(by="anomaly_score", ascending=False, na_position="last")
390
+
391
+ # Master checkbox to hide all trajectories
392
+ hide_all = st.sidebar.checkbox("Hide all trajectories", value=True, key="hide_all_trajectories")
393
+
394
+ selected_idx = []
395
+ new_enabled = []
396
+
397
+ for m, row in df_sorted.iterrows():
398
+ idx = int(row["idx"])
399
+ # Updated label to include Hurst exponent
400
+ label = f"{idx}: H={row.get('hurst', np.nan):.3g} | score={row.get('anomaly_score', np.nan):.3g}, FTLE={row.get('ftle', np.nan):.3g}"
401
+
402
+ checked = st.sidebar.checkbox(
403
+ label,
404
+ value=(idx in st.session_state.get("enabled_checkboxes", [])),
405
+ key=f"traj_{idx}"
406
+ )
407
+
408
+ if checked:
409
+ new_enabled.append(idx)
410
+ selected_idx.append(idx)
411
+
412
+ # FINAL FILTER: if hide_all is True, clear selected_idx
413
+ if hide_all:
414
+ selected_idx = []
415
+
416
+ st.session_state.enabled_checkboxes = new_enabled
417
+
418
+ # --- Plot trajectories ---
419
+ fig, ax = plt.subplots(figsize=(8, 6))
420
+ styles, colors = ['-', '--', '-.', ':'], plt.cm.tab20.colors
421
+
422
+ for m, solution_data in enumerate(solutions):
423
+ if m not in selected_idx:
424
+ continue
425
+ color = colors[m % len(colors)]
426
+
427
+ # Plot DOP853 solution (solid line)
428
+ x_dop = solution_data["dop853_x"]
429
+ y_dop = solution_data["dop853_y"]
430
+ ax.plot(x_dop, y_dop, linestyle='-', color=color, linewidth=1.2, label=f'DOP853 traj {m}' if m == selected_idx[0] else "")
431
+
432
+ # Plot Neural Network solution on full interval (dashed line)
433
+ if solution_data["nn_x"] is not None and solution_data["nn_y"] is not None:
434
+ x_nn = solution_data["nn_x"]
435
+ y_nn = solution_data["nn_y"]
436
+ t_full = solution_data["t_full"]
437
+
438
+ # Plot NN solution as dashed line throughout the full interval
439
+ ax.plot(x_nn, y_nn, linestyle='--', color=color, linewidth=1.0, alpha=0.7, label=f'NN traj {m}' if m == selected_idx[0] else "")
440
+
441
+ # Mark initial and final points
442
+ ax.plot(x_dop[0], y_dop[0], 'o', color=color, markersize=4)
443
+ ax.plot(x_dop[-1], y_dop[-1], 'x', color=color, markersize=6)
444
+ ax.text(x_dop[-1] + 0.01, y_dop[-1] + 0.01, f"{m}", fontsize=8, color=color)
445
+
446
+ # Mark t_train_end point with a triangle for both DOP853 and NN
447
+ train_idx = np.searchsorted(t_full, tte)
448
+ if train_idx < len(x_dop):
449
+ ax.plot(x_dop[train_idx], y_dop[train_idx], '^', color=color, markersize=8, markeredgecolor='black')
450
+ # Also mark the corresponding point on NN curve
451
+ if x_nn is not None and y_nn is not None and train_idx < len(x_nn):
452
+ ax.plot(x_nn[train_idx], y_nn[train_idx], '^', color=color, markersize=8, markeredgecolor='black', markerfacecolor='none')
453
+
454
+ # Always draw line between triangles at t_train_end regardless of show_connections setting
455
+ ax.plot([x_dop[train_idx], x_nn[train_idx]], [y_dop[train_idx], y_nn[train_idx]],
456
+ color=color, linewidth=1.0, alpha=0.7, linestyle=':')
457
+
458
+ # If showing connections, draw lines between DOP853 and NN points
459
+ if show_connections:
460
+ for i in range(0, len(x_dop), connection_stride):
461
+ ax.plot([x_dop[i], x_nn[i]], [y_dop[i], y_nn[i]],
462
+ color=color, linewidth=0.5, alpha=0.3, linestyle='-')
463
+
464
+ # Add trajectory number label near the end point for both curves
465
+ ax.text(x_dop[-1] + 0.01, y_dop[-1] + 0.01, f"{m}", fontsize=8, color=color)
466
+ if x_nn is not None and y_nn is not None:
467
+ ax.text(x_nn[-1] + 0.01, y_nn[-1] + 0.01, f"{m}", fontsize=8, color=color)
468
+
469
+ ax.set_title(f"Gene regulatory trajectories ({solver_type}) — t_train_end={tte:.2f}, t_full_end={tfe:.2f}, t_points={tn}")
470
+ ax.set_xlabel("x(t)")
471
+ ax.set_ylabel("y(t)")
472
+ ax.grid(True)
473
+ # Only show legend if there are few trajectories to avoid clutter
474
+ if len(selected_idx) <= 3:
475
+ ax.legend()
476
+ else:
477
+ # Create custom legend with unique labels
478
+ legend_elements = [Line2D([0], [0], color='gray', lw=2, linestyle='-', label='DOP853'),
479
+ Line2D([0], [0], color='gray', lw=2, linestyle='--', label='NN')]
480
+ ax.legend(handles=legend_elements)
481
+ st.pyplot(fig)
482
+
483
+ # --- Time series plot: x(t) and y(t) ---
484
+ fig_ts, ax_ts = plt.subplots(figsize=(8, 6))
485
+
486
+ for m, solution_data in enumerate(solutions):
487
+ if m not in selected_idx:
488
+ continue
489
+ color = colors[m % len(colors)]
490
+
491
+ t_full = solution_data["t_full"]
492
+ x_dop = solution_data["dop853_x"]
493
+ y_dop = solution_data["dop853_y"]
494
+
495
+ # Plot x(t) as solid line and y(t) as dashed line
496
+ ax_ts.plot(t_full, x_dop, linestyle='-', color=color, linewidth=1.2, label=f'x(t) traj {m}' if m == selected_idx[0] else "")
497
+ ax_ts.plot(t_full, y_dop, linestyle='--', color=color, linewidth=1.2, label=f'y(t) traj {m}' if m == selected_idx[0] else "")
498
+
499
+ ax_ts.set_title(f"Time series ({solver_type}) — x(t) and y(t) — t_train_end={tte:.2f}, t_full_end={tfe:.2f}, t_points={tn}")
500
+ ax_ts.set_xlabel("t")
501
+ ax_ts.set_ylabel("x(t), y(t)")
502
+ ax_ts.grid(True)
503
+ ax_ts.legend()
504
+ st.pyplot(fig_ts)
505
+
506
+ # --- Shadowing plot: epsilon(t) ---
507
+ if show_connections:
508
+ # Import shadowing functionality
509
+ from core.shadowing import compute_epsilon_t, find_shadowing_breakdown_time
510
+
511
+ fig_shad, ax_shad = plt.subplots(figsize=(8, 6))
512
+
513
+ for m, solution_data in enumerate(solutions):
514
+ if m not in selected_idx:
515
+ continue
516
+ color = colors[m % len(colors)]
517
+
518
+ t_full = solution_data["t_full"]
519
+ x_dop = solution_data["dop853_x"]
520
+ y_dop = solution_data["dop853_y"]
521
+ x_nn = solution_data["nn_x"]
522
+ y_nn = solution_data["nn_y"]
523
+
524
+ # Calculate distance between DOP853 and NN solutions
525
+ dist = np.sqrt((x_dop - x_nn)**2 + (y_dop - y_nn)**2)
526
+ # Calculate epsilon(t) as the running maximum (sup norm)
527
+ epsilon_t = compute_epsilon_t(dist)
528
+
529
+ ax_shad.plot(epsilon_t, t_full, color=color, linewidth=1.2, label=f'ε(t) traj {m}' if m == selected_idx[0] else "")
530
+
531
+ # Calculate and annotate shadowing breakdown time
532
+ shadowing_time, shadowing_length, shadowing_ratio = find_shadowing_breakdown_time(epsilon_t, t_full)
533
+ if shadowing_time is not None:
534
+ ax_shad.axhline(y=shadowing_time, color=color, linestyle=':', alpha=0.7, label=f't*={shadowing_time:.2f} traj {m}')
535
+
536
+ # Add vertical line to indicate the transition from training to extrapolation
537
+ ax_shad.axhline(y=tte, color='red', linestyle='--', alpha=0.7, label=f't_train_end={tte}')
538
+
539
+ ax_shad.set_title(f"Shadowing ({solver_type}) — ε(t) vs t — t_train_end={tte:.2f}, t_full_end={tfe:.2f}, t_points={tn}")
540
+ ax_shad.set_xlabel("ε(t)")
541
+ ax_shad.set_ylabel("t")
542
+ ax_shad.grid(True)
543
+ # Only show legend if there are few trajectories to avoid clutter
544
+ if len(selected_idx) <= 3:
545
+ ax_shad.legend()
546
+ else:
547
+ # Create custom legend with unique labels
548
+ legend_elements = [Line2D([0], [0], color='gray', lw=2, linestyle='-', label='ε(t)'),
549
+ Line2D([0], [0], color='red', lw=1, linestyle='--', label=f'train/extrap boundary')]
550
+ ax_shad.legend(handles=legend_elements)
551
+ st.pyplot(fig_shad)
552
+
553
+ # Display shadowing diagnostics
554
+ st.markdown("**Shadowing Diagnostics:**")
555
+ shadowing_diagnostics_text = ""
556
+ for m, solution_data in enumerate(solutions):
557
+ if m not in selected_idx:
558
+ continue
559
+
560
+ t_full = solution_data["t_full"]
561
+ x_dop = solution_data["dop853_x"]
562
+ y_dop = solution_data["dop853_y"]
563
+ x_nn = solution_data["nn_x"]
564
+ y_nn = solution_data["nn_y"]
565
+
566
+ # Calculate distance between DOP853 and NN solutions
567
+ dist = np.sqrt((x_dop - x_nn)**2 + (y_dop - y_nn)**2)
568
+ # Calculate epsilon(t) as the running maximum (sup norm)
569
+ epsilon_t = compute_epsilon_t(dist)
570
+
571
+ # Calculate shadowing breakdown diagnostics
572
+ shadowing_time, shadowing_length, shadowing_ratio = find_shadowing_breakdown_time(epsilon_t, t_full)
573
+
574
+ if shadowing_time is not None:
575
+ shadowing_diagnostics_text += f"Trajectory {m}: t* = {shadowing_time:.3f}, length = {shadowing_length:.3f}, ratio = {shadowing_ratio:.3f}\n"
576
+ else:
577
+ shadowing_diagnostics_text += f"Trajectory {m}: No breakdown (ε(t) ≤ threshold for entire duration)\n"
578
+
579
+ if shadowing_diagnostics_text:
580
+ st.text(shadowing_diagnostics_text)
581
+
582
+ # --- Show info about solvers ---
583
+ st.info(f"DOP853 solved on full interval [0, {tfe:.2f}], NN (Feedforward) trained on [0, {tte:.2f}] and applied to full interval [0, {tfe:.2f}]")
584
+
585
+ # --- Show metrics table (rounded) ---
586
+ st.markdown("**Per-trajectory metrics (rounded to 3 decimals)**")
587
+ if not df_metrics.empty:
588
+ df_to_display = df_metrics.reset_index(drop=True)
589
+
590
+ # Create a copy of the dataframe with shorter column names for display purposes
591
+ df_display_shortened = df_to_display.copy()
592
+
593
+ # Define mapping for shortened column names (only for long 'curv_' columns)
594
+ column_rename_map = {
595
+ 'curv_radius_mean': 'curv_rad_mn',
596
+ 'curv_radius_median': 'curv_rad_med',
597
+ 'curv_radius_std': 'curv_rad_std',
598
+ 'curv_radius_local_zscore': 'curv_rad_lcl_z',
599
+ 'curv_count_finite': 'curv_ct_fin'
600
+ # Note: curv_p10 and curv_p90 are already short
601
+ }
602
+
603
+ df_display_shortened = df_display_shortened.rename(columns=column_rename_map)
604
+
605
+ # Format only numeric columns, keeping 'idx' as integer
606
+ numeric_columns = df_display_shortened.select_dtypes(include=[np.number]).columns.tolist()
607
+ formatter = {col: "{:.3f}" for col in numeric_columns}
608
+
609
+ # Apply styling - note that our highlight function now supports both original and shortened names
610
+ styled_df = df_display_shortened.style.format(formatter).apply(highlight_extreme_values_in_table, axis=None)
611
+
612
+ st.dataframe(styled_df)
613
+
614
+ # CSV export with rounding
615
+ if not df_metrics.empty:
616
+ csv_name = "export_metrics_rounded.csv"
617
+ df_metrics.round(3).to_csv(csv_name, index=False, float_format="%.3f")
618
+ st.download_button("Download metrics CSV (rounded)", data=open(csv_name, 'rb'), file_name=csv_name)
619
+
620
+ st.markdown("**Parameters currently used:**")
621
+ st.text(parameters_to_text(collect_params_from_widgets()))
622
+
623
+ # Import the documentation function
624
+ from utils.documentation import display_ode_documentation
625
+
626
+ # Display the ODE system documentation
627
+ display_ode_documentation()
differential_equations_streamlit_src/pages/1_Lotka-Volterra.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from scipy.integrate import solve_ivp
5
+
6
+ # Sidebar controls
7
+ st.sidebar.header("Simulation Settings")
8
+ # Sliders for parameters a, b, k, m
9
+ a = st.sidebar.slider("Prey growth rate a", min_value=0.0, max_value=2.0, step=0.1, value=0.2)
10
+ b = st.sidebar.slider("Predation rate b", min_value=0.0, max_value=2.0, step=0.1, value=0.2)
11
+ k = st.sidebar.slider("Predator efficiency k", min_value=0.0, max_value=2.0, step=0.1, value=0.5)
12
+ m = st.sidebar.slider("Predator death rate m", min_value=0.0, max_value=2.0, step=0.1, value=0.1)
13
+ # End time slider from 1 to 50
14
+ t_end = st.sidebar.slider("End time (t_end)", min_value=1, max_value=50, step=1, value=35)
15
+
16
+ # Number of evaluation points and time array
17
+ N = 500
18
+ t_eval = np.linspace(0, t_end, N)
19
+
20
+ # Solver choice
21
+ method = "DOP853"
22
+
23
+ # Initial conditions for prey (x0) and predator y0
24
+ x0_values = np.arange(1.0, 2.01, 0.1)
25
+ y0 = 1.0
26
+
27
+ # Compute trajectories without caching (to reflect t_end changes)
28
+ def compute_trajectories(params):
29
+ a_, b_, k_, m_ = params
30
+ trajs = []
31
+ for x0 in x0_values:
32
+ def lv_system(t, z):
33
+ x, y = z
34
+ return [x * (a_ - b_ * y), y * (k_ * b_ * x - m_)]
35
+ sol = solve_ivp(lv_system, (0, t_end), [x0, y0], method=method, t_eval=t_eval)
36
+ trajs.append(sol.y)
37
+ return trajs
38
+
39
+ trajectories = compute_trajectories((a, b, k, m))
40
+
41
+ # Plot static phase trajectories
42
+ fig, ax = plt.subplots(figsize=(8, 6))
43
+ title = f"Lotka–Volterra (a={a}, b={b}, k={k}, m={m}; t_end={t_end})"
44
+ ax.set_title(title, fontsize=14)
45
+ ax.set_xlabel("Prey population x(t)")
46
+ ax.set_ylabel("Predator population y(t)")
47
+ ax.grid(True, linestyle='--', linewidth=0.5)
48
+
49
+ for idx, (x, y) in enumerate(trajectories):
50
+ ax.plot(x, y, linewidth=1.0)
51
+ ax.plot(x[0], y[0], 'o', color='black', markersize=4)
52
+ ax.plot(x[-1], y[-1], 'x', color='red', markersize=4)
53
+ ax.text(x[-1], y[-1], f"x0={x0_values[idx]:.1f}", fontsize=8)
54
+
55
+ st.pyplot(fig)
56
+
57
+ # Mathematical formulation and notes on main page
58
+ st.markdown("---")
59
+ st.markdown("**Lotka–Volterra system of equations:**")
60
+ st.latex(r"""
61
+ \begin{cases}
62
+ \frac{dx}{dt} = x \left(a - b y\right),\\
63
+ \frac{dy}{dt} = y \left(k b x - m\right),
64
+ \end{cases}
65
+ """)
66
+
67
+ # Notes
68
+ st.markdown("---")
69
+ st.markdown("""
70
+ **Notes:**
71
+ - Start point ●, end point ×.
72
+ - Initial x₀ from 1.0 to 2.0 step 0.1; y₀ = 1.0.
73
+ - Solver: DOP853, points N = 500.
74
+ """)
75
+
76
+ # App title at bottom
77
+ st.markdown("---")
78
+ st.markdown("*Lotka–Volterra Model Explorer*")
differential_equations_streamlit_src/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ streamlit>=1.32
2
+ matplotlib>=3.8
3
+ numpy>=1.26
4
+ scipy
5
+ torch
differential_equations_streamlit_src/utils/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .documentation import display_ode_documentation
2
+ from .plain_text_parameters import parameters_to_text, text_to_parameters
3
+ from .compute_metrics import compute_ftle_metrics
4
+ from .highlight_extreme_values_in_table import highlight_extreme_values_in_table
5
+
6
+ __all__ = ['display_ode_documentation', 'parameters_to_text', 'text_to_parameters', 'compute_ftle_metrics', 'highlight_extreme_values_in_table']
differential_equations_streamlit_src/utils/compute_metrics.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from scipy.integrate import solve_ivp
3
+
4
+
5
+ def compute_ftle_metrics(rhs, x0, y0, te, t_eval, x, y):
6
+ """
7
+ Computes FTLE (Finite-Time Lyapunov Exponent) and related metrics.
8
+
9
+ Args:
10
+ rhs: Right-hand side function of the ODE system
11
+ x0, y0: Initial conditions
12
+ te: End time
13
+ t_eval: Time points array
14
+ x, y: Solution arrays from the main trajectory
15
+
16
+ Returns:
17
+ tuple: (ftle, final_d, ftle_r2) or (np.nan, np.nan, np.nan) if computation fails
18
+ """
19
+ eps = 1e-6 * (1.0 + abs(x0) + abs(y0))
20
+ xp0, yp0 = x0 + eps, y0 + 0.5 * eps
21
+ try:
22
+ sol_p = solve_ivp(rhs, (0, te), (xp0, yp0), method='DOP853', t_eval=t_eval)
23
+ if sol_p.success:
24
+ xp, yp = sol_p.y
25
+ dist = np.sqrt((x - xp) ** 2 + (y - yp) ** 2)
26
+ dist = np.where(dist <= 0, 1e-12, dist)
27
+ final_d = float(dist[-1])
28
+ s_idx, e_idx = int(0.25 * len(t_eval)), int(0.75 * len(t_eval))
29
+ if e_idx > s_idx + 1:
30
+ d_slice = dist[s_idx:e_idx]
31
+ t_slice = t_eval[s_idx:e_idx]
32
+ d_slice = np.clip(d_slice, 1e-12, None)
33
+ ln_d = np.log(d_slice)
34
+ # linear fit and r2 diagnostics
35
+ slope, intercept = np.polyfit(t_slice, ln_d, 1)
36
+ ftle = float(slope)
37
+ resid = ln_d - (slope * t_slice + intercept)
38
+ ss_res = np.sum(resid ** 2)
39
+ ss_tot = np.sum((ln_d - np.mean(ln_d)) ** 2)
40
+ ftle_r2 = 1 - ss_res / ss_tot if ss_tot > 0 else np.nan
41
+ return ftle, final_d, ftle_r2
42
+ # Return NaN values if computation was unsuccessful
43
+ return np.nan, np.nan, np.nan
44
+ except Exception:
45
+ # Return NaN values in case of exception
46
+ return np.nan, np.nan, np.nan
differential_equations_streamlit_src/utils/documentation.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ def display_ode_documentation():
4
+ """
5
+ Displays the ODE system documentation and equations in the Streamlit interface.
6
+ This includes annotations about the curves, metrics, FTLE diagnostics, and the
7
+ mathematical formulation of the gene regulatory ODE system.
8
+ """
9
+ st.markdown("---")
10
+ st.markdown("- Each curve is annotated with its `idx` at the final point.")
11
+ st.markdown("- Table shows robust curvature statistics (median, p10, p90) and path length.")
12
+ st.markdown("- FTLE diagnostics include R^2 of ln(dist) fit (ftle_r2). Anomaly score combines FTLE, path length, max curvature and R^2 reliability.")
13
+
14
+ st.markdown("---")
15
+ st.markdown("**Column Descriptions:**")
16
+ st.markdown("- idx - Index of the trajectory, starting from 0")
17
+ st.markdown("- ftle - Finite-Time Lyapunov Exponent, computed as the slope of linear fit of ln(d(t)) vs t on a central window (25%–75% of t_eval) after clipping d(t) to a minimum (1e-12)")
18
+ st.markdown("- ftle_r2 - Coefficient of determination (R²) of the linear fit for FTLE, indicating reliability of the ftle estimate")
19
+ st.markdown("- amp - Amplitude (max−min of radial distance sqrt(x²+y²))")
20
+ st.markdown("- final_dist - Final distance between the original trajectory and its companion trajectory with tiny perturbation")
21
+ st.markdown("- hurst - Hurst exponent using the rescaled range (R/S) method calculated for x(t) and y(t) and averaged")
22
+ st.markdown("- curv_rad_mn - Mean curvature radius computed from 1/κ(t) where κ(t) is the curvature")
23
+ st.markdown("- curv_rad_med - Median curvature radius computed from 1/κ(t) where κ(t) is the curvature")
24
+ st.markdown("- curv_rad_std - Standard deviation of curvature radius")
25
+ st.markdown("- curv_p10 - 10th percentile of curvature radius")
26
+ st.markdown("- curv_p90 - 90th percentile of curvature radius")
27
+ st.markdown("- curv_ct_fin - Number of finite radius samples")
28
+ st.markdown("- initial_x - X coordinate of the initial point")
29
+ st.markdown("- initial_y - Y coordinate of the initial point")
30
+ st.markdown("- path_len - Total arclength computed as sum of Euclidean distances between consecutive points along the trajectory")
31
+ st.markdown("- max_kappa - Maximum finite curvature value, useful to detect sharp bends")
32
+ st.markdown("- frac_high_curv - Fraction of time points with κ(t) above a threshold (default κ > 0.1, i.e., radius < 10), measuring density of sharp bends along the trajectory")
33
+ st.markdown("- curv_rad_lcl_z - Local z-score of curve median curvature radius relative to nearest neighbors in initial condition space")
34
+ st.markdown("- anomaly_score - Aggregated score combining robust z-scores (IQR-based) of ftle, path_len, max_kappa, and ftle_r2 (ftle + path_len + max_kappa − ftle_r2)")
35
+
36
+ st.markdown("---")
37
+ st.markdown("**System of ODEs (safe):**")
38
+ st.latex(r"""
39
+ \begin{cases}
40
+ \frac{dx}{dt} = \frac{K\,x^{1/\alpha}}{b^{1/\alpha} + x^{1/\alpha}} - \gamma_1\,x,\\[6pt]
41
+ \frac{dy}{dt} = \frac{K\,y^{1/\alpha}}{b^{1/\alpha} + y^{1/\alpha}} - \gamma_2\,y.
42
+ \end{cases}
43
+ """)
differential_equations_streamlit_src/utils/highlight_extreme_values_in_table.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+
5
+ def highlight_extreme_values_in_table(df):
6
+ """
7
+ Applies conditional formatting to highlight extreme values in metrics DataFrame.
8
+ Highlights top 2 values in chocolate color for columns where high values are significant,
9
+ and top 2 minimum values in darkturquoise color for columns where low values are significant.
10
+ Works with both original and shortened column names (e.g., curv_radius_mean → curv_rad_mn).
11
+ """
12
+ # Create a copy of the DataFrame with styling
13
+ styles = pd.DataFrame('', index=df.index, columns=df.columns)
14
+
15
+ # Define possible column names for max values (both original and shortened)
16
+ max_columns_possible = [
17
+ ['ftle'], ['ftle_r2'], ['amp'], ['final_dist'], ['hurst'],
18
+ ['curv_count_finite', 'curv_ct_fin'], # Both original and shortened
19
+ ['path_len'], ['max_kappa'], ['frac_high_curv'], ['anomaly_score']
20
+ ]
21
+
22
+ # Define possible column names for min values (both original and shortened)
23
+ min_columns_possible = [
24
+ ['curv_radius_mean', 'curv_rad_mn'], # Both original and shortened
25
+ ['curv_radius_median', 'curv_rad_med'], # Both original and shortened
26
+ ['curv_radius_std', 'curv_rad_std'], # Both original and shortened
27
+ ['curv_radius_local_zscore', 'curv_rad_lcl_z'], # Both original and shortened
28
+ ['curv_p10'], ['curv_p90']
29
+ ]
30
+
31
+ # Identify actual columns in the DataFrame
32
+ max_columns = []
33
+ for col_group in max_columns_possible:
34
+ for col in col_group:
35
+ if col in df.columns:
36
+ max_columns.append(col)
37
+ break # Only add the first matching column from the group
38
+
39
+ min_columns = []
40
+ for col_group in min_columns_possible:
41
+ for col in col_group:
42
+ if col in df.columns:
43
+ min_columns.append(col)
44
+ break # Only add the first matching column from the group
45
+
46
+ for col in df.columns:
47
+ if col == 'idx': # Skip the index column
48
+ continue
49
+
50
+ if col in max_columns:
51
+ # Highlight top 2 maximum values in chocolate color
52
+ if df[col].dtype in ['float64', 'int64', 'float32', 'int32'] and not df[col].isna().all():
53
+ # Get top 2 values (excluding NaN)
54
+ valid_values = df[col].dropna()
55
+ if len(valid_values) >= 2:
56
+ top2_values = valid_values.nlargest(2)
57
+ styles.loc[valid_values.isin(top2_values), col] = 'background-color: #D2691E' # chocolate
58
+ elif len(valid_values) == 1:
59
+ top1_idx = valid_values.index[0]
60
+ styles.loc[top1_idx, col] = 'background-color: #D2691E' # chocolate
61
+
62
+ elif col in min_columns:
63
+ # Highlight top 2 minimum values in darkturquoise color
64
+ if df[col].dtype in ['float64', 'int64', 'float32', 'int32'] and not df[col].isna().all():
65
+ # Get top 2 minimum values (excluding NaN)
66
+ valid_values = df[col].dropna()
67
+ if len(valid_values) >= 2:
68
+ bottom2_values = valid_values.nsmallest(2)
69
+ styles.loc[valid_values.isin(bottom2_values), col] = 'background-color: #00CED1' # darkturquoise
70
+ elif len(valid_values) == 1:
71
+ bottom1_idx = valid_values.index[0]
72
+ styles.loc[bottom1_idx, col] = 'background-color: #00CED1' # darkturquoise
73
+
74
+ return styles
differential_equations_streamlit_src/utils/neural_ode_solver.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Conditional import for neural ODE solver to handle cases where torch is not available
2
+ try:
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.optim as optim
6
+ TORCH_AVAILABLE = True
7
+ except ImportError:
8
+ TORCH_AVAILABLE = False
9
+ import numpy as np
10
+
11
+
12
+ if TORCH_AVAILABLE:
13
+ class NeuralODE(nn.Module):
14
+ """
15
+ Neural Network surrogate for solving ODEs
16
+ Takes time t as input and outputs (x(t), y(t))
17
+ """
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.net = nn.Sequential(
21
+ nn.Linear(1, 64),
22
+ nn.Tanh(),
23
+ nn.Linear(64, 64),
24
+ nn.Tanh(),
25
+ nn.Linear(64, 64),
26
+ nn.Tanh(),
27
+ nn.Linear(64, 2), # Output x, y
28
+ )
29
+
30
+ def forward(self, t):
31
+ return self.net(t)
32
+
33
+
34
+ def train_neural_ode(t_train, x_train, y_train, epochs=2000, lr=1e-3):
35
+ """
36
+ Train a neural network to approximate the solution on the training interval
37
+
38
+ Args:
39
+ t_train: array of time points for training
40
+ x_train: array of x values for training
41
+ y_train: array of y values for training
42
+ epochs: number of training epochs
43
+ lr: learning rate
44
+
45
+ Returns:
46
+ Trained model
47
+ """
48
+ # Prepare training data
49
+ t_train_tensor = torch.tensor(t_train[:, None], dtype=torch.float32)
50
+ xy_train_tensor = torch.tensor(np.column_stack([x_train, y_train]), dtype=torch.float32)
51
+
52
+ # Initialize model
53
+ model = NeuralODE()
54
+ optimizer = optim.Adam(model.parameters(), lr=lr)
55
+ loss_fn = nn.MSELoss()
56
+
57
+ # Training loop
58
+ for epoch in range(epochs):
59
+ optimizer.zero_grad()
60
+ pred = model(t_train_tensor)
61
+ loss = loss_fn(pred, xy_train_tensor)
62
+ loss.backward()
63
+ optimizer.step()
64
+
65
+ if epoch % 500 == 0:
66
+ print(f"Epoch {epoch}, Loss: {loss.item():.6f}")
67
+
68
+ return model
69
+
70
+
71
+ def predict_with_neural_ode(model, t_eval):
72
+ """
73
+ Predict solution using the trained neural network
74
+
75
+ Args:
76
+ model: trained NeuralODE model
77
+ t_eval: array of time points to evaluate
78
+
79
+ Returns:
80
+ x_pred, y_pred arrays
81
+ """
82
+ if model is None:
83
+ return None, None
84
+
85
+ with torch.no_grad():
86
+ t_tensor = torch.tensor(t_eval[:, None], dtype=torch.float32)
87
+ pred = model(t_tensor).numpy()
88
+ x_pred = pred[:, 0]
89
+ y_pred = pred[:, 1]
90
+
91
+ return x_pred, y_pred
92
+ else:
93
+ def train_neural_ode(t_train, x_train, y_train, epochs=2000, lr=1e-3):
94
+ """
95
+ Placeholder function when torch is not available
96
+ """
97
+ print("PyTorch not available, skipping neural network training")
98
+ return None
99
+
100
+
101
+ def predict_with_neural_ode(model, t_eval):
102
+ """
103
+ Placeholder function when torch is not available
104
+ """
105
+ return None, None
differential_equations_streamlit_src/utils/plain_text_parameters.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # plain_text_parameters.py
2
+ # Utility functions to convert parameters <-> plain text
3
+
4
+ from typing import Dict, Any
5
+
6
+ # Preferred output order for stable copy/paste strings
7
+ _ORDER = [
8
+ "t_number",
9
+ "t_end",
10
+ "alpha",
11
+ "K",
12
+ "b",
13
+ "gamma1",
14
+ "gamma2",
15
+ "initial_radius",
16
+ "num_points",
17
+ "circle_start",
18
+ "circle_end",
19
+ "center_x",
20
+ "center_y",
21
+ "enabled_idx",
22
+ ]
23
+
24
+
25
+ def _fmt_value(v: Any) -> str:
26
+ """Format numbers concisely for plain text output."""
27
+ if isinstance(v, bool):
28
+ return "1" if v else "0"
29
+ if isinstance(v, int):
30
+ return str(v)
31
+ try:
32
+ fv = float(v)
33
+ s = ("%g" % fv)
34
+ return s
35
+ except Exception:
36
+ return str(v)
37
+
38
+
39
+ # --- Convert dictionary to plain text ---
40
+ def parameters_to_text(params: Dict[str, Any]) -> str:
41
+ parts = []
42
+ used = set()
43
+ for k in _ORDER:
44
+ if k in params:
45
+ parts.append(f"{k}={_fmt_value(params[k])}")
46
+ used.add(k)
47
+ for k, v in params.items():
48
+ if k not in used:
49
+ parts.append(f"{k}={_fmt_value(v)}")
50
+ return "; ".join(parts)
51
+
52
+
53
+ # --- Parse plain text to dictionary ---
54
+ def text_to_parameters(text: str) -> Dict[str, Any]:
55
+ """
56
+ Parse strings like "t_number=100; t_end=1.0; alpha=0.001" into a dict.
57
+ Whitespace and newlines are ignored, both ';' and newlines separate pairs.
58
+ Values are auto-cast to int where possible, else float, else raw string.
59
+ """
60
+ result: Dict[str, Any] = {}
61
+ if not isinstance(text, str):
62
+ return result
63
+ raw = text.replace("\n", ";")
64
+ for chunk in raw.split(";"):
65
+ if "=" not in chunk:
66
+ continue
67
+ key, val = chunk.split("=", 1)
68
+ key = key.strip()
69
+ val = val.strip()
70
+ if not key:
71
+ continue
72
+ if val.endswith("°"):
73
+ val = val[:-1]
74
+ try:
75
+ if val.lower().startswith("0x"):
76
+ result[key] = int(val, 16)
77
+ else:
78
+ iv = int(val)
79
+ result[key] = iv
80
+ continue
81
+ except Exception:
82
+ pass
83
+ try:
84
+ fv = float(val)
85
+ result[key] = fv
86
+ continue
87
+ except Exception:
88
+ pass
89
+ result[key] = val
90
+ return result
gitingest.sh ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ # run from repository root folder
3
+ gitingest . \
4
+ --include-pattern "*.py" \
5
+ --include-pattern "README.md" \
6
+ --include-pattern "requirements.txt" \
7
+ --exclude-pattern "LICENSE" \
8
+ --exclude-pattern "*/__pycache__/*" \
9
+ --output out_gitingest/differ_hug_01.txt