@@ -26,7 +26,7 @@ def __init__(self, model: Model,
2626 end_time : Optional [float ] = None ,
2727 num_point : int = cn .NUM_POINTS ,
2828 timecourse_df : pd .DataFrame = pd .DataFrame (),
29- jacobian_collection_arr : np .ndarray = np .array ([])
29+ jacobian_collection_arr : np .ndarray = np .array ([]),
3030 ) -> None :
3131 """
3232 Parameters
@@ -39,6 +39,10 @@ def __init__(self, model: Model,
3939 Time to end the simulation.
4040 num_points : int
4141 Number of time points to simulate.
42+ timecourse_df : pd.DataFrame
43+ Optional pre-computed timecourse DataFrame (index: time, columns: species).
44+ jacobian_collection_arr : np.ndarray
45+ Optional pre-computed Jacobian collection (shape: [num_time_points, num_species, num_species]).
4246 """
4347 self .model = model
4448 self .start_time = start_time
@@ -48,6 +52,20 @@ def __init__(self, model: Model,
4852 self ._timecourse_df = timecourse_df
4953 self ._jacobian_collection_arr = jacobian_collection_arr
5054
55+ def __eq__ (self , other : object ) -> bool :
56+ if not isinstance (other , Timecourse ):
57+ return NotImplemented
58+ return (self .model == other .model and
59+ bool (np .isclose (self .start_time , other .start_time )) and
60+ (self .end_time == other .end_time
61+ if self .end_time is None or other .end_time is None
62+ else bool (np .isclose (self .end_time , other .end_time ))) and
63+ self .num_point == other .num_point and
64+ bool (np .allclose (self .timecourse_df .values ,
65+ other .timecourse_df .values )) and
66+ bool (np .allclose (self .jacobian_collection_arr ,
67+ other .jacobian_collection_arr )))
68+
5169 def _updateEndtime (self , end_time : Optional [float ]= None )-> float | None :
5270 """Determine the end time and its source."""
5371 if end_time is not None :
@@ -83,6 +101,14 @@ def jacobian_collection_arr(self) -> np.ndarray:
83101 self ._jacobian_collection_arr = simulation_result .jacobian_collection_arr
84102 self ._timecourse_df = simulation_result .timecourse_df
85103 return self ._jacobian_collection_arr
104+
105+ def _checkSpeciesNames (self , names : List [str ]) -> None :
106+ """Check that the species names in the simulation result match the model."""
107+ result_species = list (names )
108+ if result_species != self .model .species_names :
109+ raise ValueError (
110+ f"Simulation species { result_species } do not match "
111+ f"model species { self .model .species_names } ." )
86112
87113 def _simulate (self , is_jacobian_collection : bool = False ) -> SimulationResult :
88114 """Create a Trajectory by running a simulation.
@@ -112,12 +138,18 @@ def _simulate(self, is_jacobian_collection: bool = False) -> SimulationResult:
112138 if self .start_time > 0 :
113139 rr .simulate (0 , self .start_time , 2 )
114140 try :
115- result_arr = np .array (rr .simulate (self .start_time ,
116- self .end_time , self .num_point ))
141+ rr_result = rr .simulate (self .start_time , self .end_time , self .num_point )
117142 except Exception as e :
118143 raise ValueError (f"Simulation failed: { e } " )
144+ # Check column order before converting to ndarray (colnames lost after np.array).
145+ # Skip the leading 'time' column and strip brackets from species names.
146+ result_species = [
147+ c [1 :- 1 ] if c .startswith ("[" ) and c .endswith ("]" ) else c
148+ for c in rr_result .colnames [1 :] # type: ignore
149+ ]
150+ self ._checkSpeciesNames (result_species )
151+ result_arr = np .array (rr_result )
119152 timepoint_arr = result_arr [:, 0 ]
120- # FIXME: Use species names in NamedArray and sort by model.species_names --- IGNORE ---
121153 timecourse_df = pd .DataFrame (
122154 result_arr [:, 1 :],
123155 index = timepoint_arr ,
@@ -136,7 +168,10 @@ def _simulate(self, is_jacobian_collection: bool = False) -> SimulationResult:
136168 rr .simulate (self .start_time , self .start_time + 1e-10 , 2 )
137169 else :
138170 rr .simulate (timepoint_arr [i - 1 ], t , 2 )
139- jacobian_arr = np .array (rr .getFullJacobian ()).copy ()
171+ jacobian_arr = rr .getFullJacobian ()
172+ self ._checkSpeciesNames (jacobian_arr .rownames )
173+ self ._checkSpeciesNames (jacobian_arr .colnames )
174+ jacobian_arr = np .array (jacobian_arr ).copy ()
140175 if np .all (np .isclose (jacobian_arr , 0.0 )):
141176 raise ValueError (
142177 f"Jacobian at t={ t } is all zeros; model may be degenerate." )
@@ -158,8 +193,7 @@ def serialize(self) -> str:
158193 """
159194 if not self .model .model_name :
160195 raise ValueError ("Model must have a name to serialize Timecourse." )
161- path = os .path .join (cn .TIMECOURSE_SERIALIZATION_DIR ,
162- f"{ self .model .model_name } _timecourse.pkl" )
196+ path = self .makeBiomodelSerializePath (self .model .model_name )
163197 dct = {
164198 "model" : self .model ,
165199 "start_time" : self .start_time ,
@@ -171,17 +205,38 @@ def serialize(self) -> str:
171205 pickle .dump (dct , f )
172206 return path
173207
208+ @staticmethod
209+ def makeBiomodelSerializePath (model_name : str ) -> str :
210+ """
211+ Get the expected path for a serialized Timecourse of a BioModel.
212+
213+ Parameters:
214+ model_name (str): The name of the BioModel.
215+ """
216+ return os .path .join (cn .TIMECOURSE_SERIALIZATION_DIR , f"{ model_name } _timecourse.pkl" )
217+
174218 @classmethod
175- def deserialize (cls , path : str ) -> 'Timecourse' :
219+ def deserialize (cls , path : str = "" , model_name : str = "" ) -> 'Timecourse' :
176220 """
177221 Deserialize a Timecourse from a file
222+ At least one of `path` or `model_name` must be provided.
223+ If both are provided, `path` takes precedence.
178224
179225 Parameters:
180226 path (str): The path to the serialized file.
227+ model_name (str): The name of the BioModel (used if path is not specified).
181228
182229 Returns:
183230 Timecourse: The deserialized Timecourse object.
184231 """
232+ if not path and not model_name :
233+ raise ValueError ("At least one of `path` or `model_name` must be provided." )
234+ if not path :
235+ path = cls .makeBiomodelSerializePath (model_name )
236+ # Check if the file exists
237+ if not os .path .isfile (path ):
238+ raise FileNotFoundError (f"No serialized Timecourse found at { path } " )
239+ # Deserialize
185240 with open (path , 'rb' ) as f :
186241 dct = pickle .load (f )
187242 return cls (
0 commit comments