Skip to content

Commit f649454

Browse files
First test complete implementation of TrajectoryCollection.
1 parent f8913e1 commit f649454

12 files changed

Lines changed: 467 additions & 169 deletions

scripts/calculate_linear_prediction_scores.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def processModels(first_model_num: int, last_model_num: int, process_index: int,
130130
# Handle large Jacobians
131131
trajectory = Trajectory.makeBiomodel(model_name=model_name)
132132
prediction_df = trajectory.predict(is_adjust_fitted_jacobian=True)
133-
score.addTestResult(trajectory.timecourse, prediction_df, description=model_name)
133+
score.addTestResult(trajectory.timecourse_df, prediction_df, description=model_name)
134134
except Exception as e:
135135
print(f"Error occurred while processing model {model_name}: {e}")
136136
continue

src/l_roadrunner.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ def __init__(self, roadrunner_specification: str,
9292
self._start_time = start_time
9393
self.num_point = num_point
9494
self._end_time: float = end_time if end_time is not None else np.nan
95-
self._sedml_str = sedml_str
95+
self.sedml_str = sedml_str
9696
self.end_time_source: Optional[str] = None
9797
self._species_names: List[str] = []
98-
self._timecourse = pd.DataFrame()
98+
self._timecourse_df = pd.DataFrame()
9999
self.model_name = model_name
100100

101101
@classmethod
@@ -176,7 +176,7 @@ def start_time(self, start_time: float) -> None:
176176
self._start_time = start_time
177177

178178
@property
179-
def timecourse(self) -> pd.DataFrame:
179+
def timecourse_df(self) -> pd.DataFrame:
180180
"""
181181
Simulate the model and return the time course as a DataFrame.
182182
@@ -186,12 +186,12 @@ def timecourse(self) -> pd.DataFrame:
186186
A DataFrame containing the time course of the simulation, with columns for time and each floating species.
187187
The index are the time values.
188188
"""
189-
if self._timecourse.empty:
189+
if self._timecourse_df.empty:
190190
result_arr = self.simulate(is_with_timepoints=True)
191191
df = pd.DataFrame(result_arr, columns=["time"] + self.species_names)
192192
df = df.set_index("time")
193-
self._timecourse = df
194-
return self._timecourse
193+
self._timecourse_df = df
194+
return self._timecourse_df
195195

196196
def getInitialValues(self) -> np.ndarray:
197197
"""
@@ -208,7 +208,6 @@ def getForcingInputs(self) -> np.ndarray:
208208
This is used in the linear predictor to extrapolate from the initial state.
209209
"""
210210
rr = self.getRoadrunner()
211-
#_ = rr.simulate(self.start_time, self.end_time, 2)
212211
_ = rr.simulate(self.end_time, self.end_time*1.001, 2)
213212
jacobian_arr = np.array(rr.getFullJacobian())
214213
f_arr = np.array(rr.getRatesOfChange())
@@ -403,17 +402,17 @@ def _calculateEndtimeSBML(self) -> float:
403402
float or np.nan
404403
The end time specified in the SBML string, or np.nan if no valid specification is found.
405404
"""
406-
if not self._sedml_str:
405+
if not self.sedml_str:
407406
self._msg("No SED-ML string provided, skipping SED-ML end time extraction.")
408407
return self._end_time
409408
#
410-
end_time_match = re.search(r'outputEndTime\s*=\s*"([0-9.]+)"', self._sedml_str)
409+
end_time_match = re.search(r'outputEndTime\s*=\s*"([0-9.]+)"', self.sedml_str)
411410
if end_time_match:
412411
end_time_match = re.search(r'outputEndTime\s*=\s*"([0-9.]+)"',
413-
self._sedml_str)
412+
self.sedml_str)
414413
if end_time_match:
415414
end_time = float(end_time_match.group(1))
416-
if not DEFAULT_END_TIME_STR in self._sedml_str and end_time > 0:
415+
if not DEFAULT_END_TIME_STR in self.sedml_str and end_time > 0:
417416
self._end_time = end_time
418417
elif end_time != SBML_DEFAULT_END_TIME:
419418
self._end_time = end_time
@@ -423,13 +422,6 @@ def _calculateEndtimeSBML(self) -> float:
423422
self._msg("No valid outputEndTime attribute found in SED-ML string.")
424423
else:
425424
self._msg("No outputEndTime attribute found in SED-ML string.")
426-
""" # Validate the end time by simulating to it and checking for errors.
427-
if self._end_time is not np.nan:
428-
try:
429-
rr = self.getRoadrunner()
430-
rr.simulate(self.start_time, self._end_time, 2)
431-
except Exception:
432-
self._end_time = np.nan """
433425
#
434426
return self._end_time
435427

@@ -460,8 +452,11 @@ def simulate(self, is_with_timepoints: bool=False) -> np.ndarray:
460452
idx = 1
461453
if is_with_timepoints:
462454
idx = 0
455+
rr = self.getRoadrunner()
456+
if self.start_time > 0:
457+
result_arr = rr.simulate(0, self.start_time, 2)
463458
try:
464-
result_arr = self.getRoadrunner().simulate(self.start_time, self.end_time, self.num_point)
459+
result_arr = rr.simulate(self.start_time, self.end_time, self.num_point)
465460
except Exception as e:
466461
print(f"Error occurred while simulating: {e}")
467462
return np.array([]).reshape(0, 0)
@@ -485,25 +480,27 @@ def makeJacobians(self)->Tuple[np.ndarray, np.ndarray]:
485480
ValueError
486481
If the model has no floating species after reset.
487482
"""
488-
rr = self.getRoadrunner()
489-
if len(rr.getFloatingSpeciesIds()) == 0:
483+
if len(self.species_names) == 0:
490484
raise ValueError("Model has no floating species; cannot compute Jacobian.")
491485
try:
492-
result_arr = rr.simulate(self.start_time, self.end_time, self.num_point)
486+
result_arr = self.simulate(is_with_timepoints=True)
493487
except RuntimeError as e:
494488
raise ValueError(f"CVODE failed during initial simulation: {e}") from e
495-
times_arr = np.array(result_arr["time"]) # copy before reset invalidates buffer
489+
time_arr = np.array(result_arr[:, 0]) # copy before reset invalidates buffer
496490

491+
rr = self.getRoadrunner()
497492
rr.reset()
493+
if self.start_time > 0:
494+
_ = rr.simulate(0, self.start_time, 2)
498495
jacobians = []
499496
valid_times = []
500-
for i, t in enumerate(times_arr):
497+
for i, t in enumerate(time_arr):
501498
try:
502499
if i == 0:
503500
t2 = self.start_time + 1e-10
504501
rr.simulate(self.start_time, t2, 2)
505502
else:
506-
rr.simulate(times_arr[i - 1], t, 2)
503+
rr.simulate(time_arr[i - 1], t, 2)
507504
except RuntimeError as e:
508505
raise ValueError(f"CVODE failed at t={t}: {e}") from e
509506
try:
@@ -516,7 +513,7 @@ def makeJacobians(self)->Tuple[np.ndarray, np.ndarray]:
516513
valid_times.append(t)
517514
if not jacobians:
518515
raise ValueError("No valid Jacobians could be computed (all timepoints failed, likely due to assignment-rule species).")
519-
_ = self.timecourse # Cache the timecourse DataFrame for later use in plotting, to avoid simulating again. This is done after the Jacobian collection loop to avoid state corruption that can cause getFullJacobian to segfault even after reset().
516+
_ = self.timecourse_df # Cache the timecourse DataFrame for later use in plotting, to avoid simulating again. This is done after the Jacobian collection loop to avoid state corruption that can cause getFullJacobian to segfault even after reset().
520517
return np.array(jacobians), np.array(valid_times)
521518

522519
def _calculateEndtimeJacobian(self) -> float:

src/plot_options.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,47 @@
33
import matplotlib.pyplot as plt # type: ignore
44
from typing import Optional, Tuple
55

6+
AX = plt.gca()
7+
FIG = plt.gcf()
8+
69
class PlotOptions(object):
710

811
def __init__(self,
9-
ax: Optional[plt.Axes] = None, # type: ignore
12+
ax=AX,
13+
fig = FIG, # type: ignore
1014
title: Optional[str] = None,
11-
xlabel: Optional[str] = None,
12-
ylabel: Optional[str] = None,
15+
xlabel: str = "time",
16+
ylabel: str = "concentration",
1317
legend: bool = True,
1418
xlim: Optional[Tuple[float, float]] = None,
1519
ylim: Optional[Tuple[float, float]] = None,
1620
):
17-
if ax is None:
18-
_, ax = plt.subplots()
21+
if (AX == ax) and (FIG == fig):
22+
fig, ax = plt.subplots()
1923
self.ax = ax
24+
self.fig = fig
2025
self.title = title
2126
self.xlabel = xlabel
2227
self.ylabel = ylabel
2328
self.legend = legend
2429
self.xlim = xlim
2530
self.ylim = ylim
31+
32+
def to_dict(self):
33+
return self.__dict__
2634

2735
def apply(self):
2836
if self.title is not None:
29-
self.ax.set_title(self.title)
37+
self.ax.set_title(self.title) # type: ignore
3038
if self.xlabel is not None:
31-
self.ax.set_xlabel(self.xlabel)
39+
self.ax.set_xlabel(self.xlabel) # type: ignore
3240
if self.ylabel is not None:
33-
self.ax.set_ylabel(self.ylabel)
34-
if self.legend:
35-
self.ax.legend()
41+
self.ax.set_ylabel(self.ylabel) # type: ignore
42+
if isinstance(self.legend, bool) and self.legend:
43+
self.ax.legend() # type: ignore
44+
if isinstance(self.legend, list):
45+
self.ax.legend(self.legend) # type: ignore
3646
if self.xlim is not None:
37-
self.ax.set_xlim(self.xlim)
47+
self.ax.set_xlim(self.xlim) # type: ignore
3848
if self.ylim is not None:
39-
self.ax.set_ylim(self.ylim)
49+
self.ax.set_ylim(self.ylim) # type: ignore

src/trajectory.py

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from src.l_roadrunner import LRoadrunner, NULL_L_ROADRUNNER # type: ignore
55
import src.utils as utils
66
from src.plot_options import PlotOptions # type: ignore
7+
from src.score import Score # type: ignore
78

89
import collections
910
import matplotlib.axes as maxes # type: ignore
@@ -169,10 +170,6 @@ def max_cv(self) -> float:
169170
cv_arr[~np.isfinite(cv_arr)] = 0.0
170171
return np.max(cv_arr)
171172

172-
@property
173-
def timecourse(self) -> pd.DataFrame:
174-
return self.l_roadrunner.timecourse
175-
176173
# ------------------------------------------------------------------
177174
# Public methods (alphabetical)
178175
# ------------------------------------------------------------------
@@ -277,7 +274,7 @@ def _calculateResiduals(params: Parameters, ispecies:int) -> np.ndarray:
277274
for i in range(self.num_species):
278275
jacobian_arr[ispecies, i] = params[f'd{i}'].value
279276
prediction_arr = self._predict(jacobian_arr=jacobian_arr)[:, ispecies]
280-
residual_arr = self.timecourse.iloc[:, ispecies].values - prediction_arr
277+
residual_arr = self.timecourse_df.iloc[:, ispecies].values - prediction_arr
281278
return residual_arr[1:] # Exclude timepoint 0 to avoid issues with initial state
282279
##
283280
for ispecies, _ in enumerate(self.l_roadrunner.species_names):
@@ -305,6 +302,11 @@ def fromArrays(cls, jacobian_arr: np.ndarray, timepoint_arr: np.ndarray,
305302
jc._initialize(l_roadrunner, eigenvalues_collection_arr,
306303
eigenvector_collection_arr, **kwargs)
307304
jc._sortArrays()
305+
if l_roadrunner is not NULL_L_ROADRUNNER:
306+
try:
307+
jc.timecourse_df = l_roadrunner.timecourse_df.loc[jc.timepoint_arr]
308+
except KeyError:
309+
pass # timepoint_arr not a subset of timecourse_df; keep full timecourse
308310
return jc
309311

310312
def getCost(self, istart: int, iend: int) -> float:
@@ -557,23 +559,19 @@ def plot(self,
557559
plt.show()
558560
return PlotInfo(top_ax=ax1, bottom_ax=ax2, fig=fig)
559561

560-
def plotPredictions(self,
561-
ax: Optional[plt.Axes] = None, # type: ignore
562-
ylim: Optional[Tuple[float, float]]=None,
563-
xlim: Optional[Tuple[float, float]]=None,
564-
model_name: str = "",
565-
legend: bool = True,
566-
) -> PlotOptions:
562+
def plotPrediction(self, **kwargs) -> PlotOptions:
567563
"""
568564
Plot the predicted timecourse of simulation species concentrations.
569565
The first plot shows how the Jacobian changes over time relative to the centroid.
570566
The second plot shows the dynamics of the model's species concentrations
571-
over time.
567+
over time. Does not plot the first value since this is the initial state and not a prediction.
572568
573569
Parameters
574570
----------
575571
ax : Optional[plt.Axes]
576572
An optional matplotlib Axes
573+
title: str
574+
The title for the plot
577575
fig : Optional[plt.Figure]
578576
An optional matplotlib Figure object to use. If None, a new figure will be created.
579577
is_legend : bool
@@ -586,29 +584,29 @@ def plotPredictions(self,
586584
The model name
587585
"""
588586
if hasattr(self.l_roadrunner, "getRoadrunner"):
589-
roadrunner = self.l_roadrunner.getRoadrunner()
590-
species_ids = roadrunner.getFloatingSpeciesIds()
591-
data_arr = self.l_roadrunner.simulate(is_with_timepoints=True)
592-
species_data = data_arr[:, 1:] # Exclude time column
593-
species_times = data_arr[:, 0] # Extract time column
587+
species_ids = self.l_roadrunner.species_names
588+
species_times = self.timecourse_df.index.values
589+
species_data = self.timecourse_df.values
594590
else:
595591
raise ValueError("Cannot plot species timecourse has a NULL LRoadrunner instance.")
596-
prediction_df = self.predict()
592+
pred_df = self.predict()
593+
# Extract model_name before passing kwargs to PlotOptions (not a PlotOptions param)
594+
model_name = kwargs.pop("model_name", "")
595+
if model_name and "title" not in kwargs:
596+
kwargs["title"] = f"{model_name}: Species Timecourse"
597597
# Timecourse plot
598-
plot_options = PlotOptions(ax=ax,
599-
legend=legend,
600-
ylim=ylim,
601-
xlim=xlim, title=f"{model_name}: Species Timecourse")
602-
ax = plot_options.ax
598+
plt_opt = PlotOptions(**kwargs)
599+
ax = plt_opt.ax
603600
colors = [sns.color_palette("tab10")[i % 10] for i in range(len(species_ids))]
601+
# Do separate loops so that legend works out correctly
602+
for i, species_id in enumerate(species_ids):
603+
ax.plot(species_times, species_data[:, i], # type: ignore
604+
label=species_id, color=colors[i], alpha=0.7)
604605
for i, species_id in enumerate(species_ids):
605-
ax.plot(species_times, species_data[:, i], label=species_id, color=colors[i], alpha=0.7)
606-
ax.scatter(species_times, prediction_df[species_id], s=8, alpha=0.7, color=colors[i])
607-
ax.set_xlabel("Time")
608-
ax.set_ylabel("Concentration")
609-
ax.set_title(f"{model_name}: Species Timecourse")
610-
plot_options.apply()
611-
return plot_options
606+
ax.scatter(species_times[1:], pred_df[species_id].values[1:], # type: ignore
607+
s=8, alpha=0.7, color=colors[i])
608+
plt_opt.apply()
609+
return plt_opt
612610

613611
def predict(self, **kwargs) -> pd.DataFrame:
614612
"""
@@ -761,6 +759,7 @@ def _initialize(self, l_roadrunner: LRoadrunner,
761759
self._fitted_jacobian_arr = cn.NULL_ARRAY
762760
self._num_fit = num_fit
763761
self._jacobian_selection = jacobian_selection
762+
self.timecourse_df = self.l_roadrunner.timecourse_df.copy()
764763

765764
@staticmethod
766765
def _ivp(_: float, x: np.ndarray, jacobian_arr: np.ndarray) -> np.ndarray:
@@ -819,12 +818,12 @@ def ode(t: float, x: np.ndarray) -> np.ndarray:
819818
n_time = len(self.timepoint_arr)
820819
n_species = len(forcing_input_arr)
821820
result_arr = np.full((n_time, n_species), np.nan)
822-
result_arr[0] = self.l_roadrunner.timecourse.iloc[0, :].values # type: ignore
821+
result_arr[0] = self.l_roadrunner.timecourse_df.loc[self.timepoint_arr[0]].values # type: ignore
823822
try:
824823
with warnings.catch_warnings():
825824
warnings.simplefilter("ignore", RuntimeWarning)
826825
for itime, timepoint in enumerate(self.timepoint_arr[:-1]):
827-
initial_state_arr = self.l_roadrunner.timecourse.loc[timepoint].values # type: ignore
826+
initial_state_arr = self.l_roadrunner.timecourse_df.loc[timepoint].values # type: ignore
828827
sol = solve_ivp(ode,
829828
(timepoint, self.timepoint_arr[itime+1]),
830829
initial_state_arr,
@@ -833,7 +832,7 @@ def ode(t: float, x: np.ndarray) -> np.ndarray:
833832
if sol.success and sol.y.shape == (n_species, 1):
834833
result_arr[itime+1] = sol.y.T
835834
return result_arr
836-
except Exception:
835+
except Exception as e:
837836
return np.full((n_time, n_species), np.nan)
838837

839838
def _sortArrays(self) -> None:

0 commit comments

Comments
 (0)