@@ -30,6 +30,10 @@ def load_data(simname):
3030 n_species = len (species )
3131 n_reactions = len (reactions )
3232
33+ # Load amounts of species in the total simulation volume
34+ amounts = np .loadtxt (simname + '_amounts.txt' )
35+ amounts = amounts [:, 1 :] # Drop time column
36+
3337 assert stoich_matrix .shape == (n_species , n_reactions ), \
3438 f"Stoichiometric matrix shape { stoich_matrix .shape } != " \
3539 f"({ n_species } , { n_reactions } )"
@@ -45,6 +49,7 @@ def load_data(simname):
4549 "species" : species ,
4650 "reactions" : reactions ,
4751 "stoich_matrix" : stoich_matrix ,
52+ "amounts" : amounts
4853 }
4954
5055
@@ -156,7 +161,7 @@ def kept_reactions_from_species(stoich_matrix, kept_species_set):
156161 return kept
157162
158163
159- def drg_reduce (data , target_species , threshold = 0.01 ):
164+ def drg_reduce (data , target_species , threshold = 0.01 , verbose = False ):
160165 """
161166 Apply the DRG method to reduce the mechanism.
162167
@@ -172,16 +177,13 @@ def drg_reduce(data, target_species, threshold=0.01):
172177
173178 Returns
174179 -------
175- kept_species : list of str
176- Species retained after reduction.
177- kept_reactions : list of str
178- Reactions retained after reduction.
179- kept_species_idx : list of int
180- Indices of retained species.
181- kept_reactions_idx : list of int
182- Indices of retained reactions.
183- r_AB : array
184- The DRG coefficient matrix.
180+ result : dict
181+ Dictionary with keys:
182+ - kept_species : list of str
183+ - kept_reactions : list of str
184+ - kept_species_idx : list of int
185+ - kept_reactions_idx : list of int
186+ - r_AB : array, the DRG coefficient matrix
185187 """
186188 species = data ["species" ]
187189 reactions = data ["reactions" ]
@@ -205,9 +207,7 @@ def drg_reduce(data, target_species, threshold=0.01):
205207 if not target_indices :
206208 raise ValueError ("No valid target species provided!" )
207209
208- print ("\n Computing DRG coefficients" )
209210 r_AB = compute_drg_coefficients (stoich_matrix , rates )
210- print (f"DRG coefficient matrix computed (max={ r_AB .max ():.4f} )" )
211211
212212 kept_species_set = bfs_reachable (r_AB , target_indices , threshold )
213213 kept_species_idx = sorted (kept_species_set )
@@ -217,13 +217,20 @@ def drg_reduce(data, target_species, threshold=0.01):
217217 kept_species_set )
218218 kept_reactions = [reactions [i ] for i in kept_reactions_idx ]
219219
220- print (f"\n DRG Reduction Results (threshold={ threshold } ):" )
221- print (f" Species: { n_species } -> { len (kept_species )} "
222- f"(removed { n_species - len (kept_species )} )" )
223- print (f" Reactions: { n_reactions } -> { len (kept_reactions )} "
224- f"(removed { n_reactions - len (kept_reactions )} )" )
220+ if verbose :
221+ print (f"\n DRG Reduction Results (threshold={ threshold } ):" )
222+ print (f" Species: { n_species } -> { len (kept_species )} "
223+ f"(removed { n_species - len (kept_species )} )" )
224+ print (f" Reactions: { n_reactions } -> { len (kept_reactions )} "
225+ f"(removed { n_reactions - len (kept_reactions )} )" )
225226
226- return kept_species , kept_reactions , kept_species_idx , kept_reactions_idx , r_AB
227+ return {
228+ "kept_species" : kept_species ,
229+ "kept_reactions" : kept_reactions ,
230+ "kept_species_idx" : kept_species_idx ,
231+ "kept_reactions_idx" : kept_reactions_idx ,
232+ "r_AB" : r_AB ,
233+ }
227234
228235
229236def sweep_thresholds (data , target_species , thresholds = None ):
@@ -288,6 +295,135 @@ def sweep_thresholds(data, target_species, thresholds=None):
288295 return results
289296
290297
298+ def detect_qss_species (data , threshold = 0.1 , min_fraction = 0.9 ,
299+ exclude_species = None , verbose = False ):
300+ """
301+ Detect species satisfying the quasi-steady-state approximation.
302+
303+ A species is flagged as QSS if, for at least `min_fraction` of all
304+ time steps, its net production/consumption imbalance is below
305+ `threshold`.
306+
307+ Parameters
308+ ----------
309+ data : dict
310+ Data loaded by load_data().
311+ threshold : float
312+ Maximum allowed relative imbalance delta_A for QSS.
313+ min_fraction : float
314+ Fraction of time steps that must satisfy the criterion
315+ (avoids flagging species that are only transiently in steady state).
316+ exclude_species : list of str, optional
317+ Species that should never be marked as QSS (e.g., targets,
318+ major species, electrons).
319+
320+ Returns
321+ -------
322+ qss_species : list of str
323+ Species names identified as quasi-steady-state.
324+ delta : array, shape (n_times, n_species)
325+ The imbalance metric at each time step for each species.
326+ """
327+ if exclude_species is None :
328+ exclude_species = []
329+
330+ stoich = data ["stoich_matrix" ] # (n_species, n_reactions)
331+ rates = data ["rates" ] # (n_times, n_reactions)
332+ species = data ["species" ]
333+
334+ n_times , n_reactions = rates .shape
335+ n_species = len (species )
336+
337+ # Contribution of each reaction to each species: nu_{A,i} * R_i(t)
338+ # shape: (n_times, n_species, n_reactions)
339+ omega = stoich [np .newaxis , :, :] * rates [:, np .newaxis , :]
340+
341+ # Production: positive contributions; Consumption: negative contributions
342+ production = np .where (omega > 0 , omega , 0.0 ).sum (axis = 2 ) # (n_times, n_species)
343+ consumption = np .where (omega < 0 , - omega , 0.0 ).sum (axis = 2 ) # (n_times, n_species)
344+
345+ # Net rate and imbalance
346+ net = np .abs (production - consumption )
347+ scale = np .maximum (production , consumption )
348+ scale = np .maximum (scale , ATOL ) # avoid division by zero
349+
350+ delta = net / scale # (n_times, n_species)
351+
352+ # A species is QSS if delta < threshold for >= min_fraction of time steps
353+ fraction_steady = (delta < threshold ).sum (axis = 0 ) / n_times # (n_species,)
354+
355+ qss_species = []
356+
357+ if verbose :
358+ print (f"\n { 'Species' :>20} | { 'Frac steady' :>12} | { 'Mean delta' :>12} | "
359+ f"{ 'QSS?' :>5} " )
360+ print ("-" * 60 )
361+
362+ for i , sp in enumerate (species ):
363+ is_qss = (fraction_steady [i ] >= min_fraction
364+ and sp not in exclude_species )
365+ if is_qss :
366+ qss_species .append (sp )
367+
368+ # Print species that are close to QSS for diagnostics
369+ if verbose and (fraction_steady [i ] > 0.5 or is_qss ):
370+ print (f"{ sp :>20} | { fraction_steady [i ]:>11.3f} | "
371+ f"{ delta [:, i ].mean ():>12.4e} | "
372+ f"{ 'YES' if is_qss else 'no' :>5} " )
373+
374+ if verbose :
375+ print (f"\n Identified { len (qss_species )} QSS species (threshold="
376+ f"{ threshold } , min_fraction={ min_fraction } )" )
377+
378+ return qss_species , delta
379+
380+
381+ def drg_with_qss (data , target_species , threshold = 0.1 , qss_threshold = 0.1 ,
382+ qss_min_fraction = 0.9 , verbose = False ):
383+ """
384+ Two-stage reduction:
385+ 1. DRG to remove unimportant species
386+ 2. QSS detection among the kept species to identify algebraic species
387+ """
388+ # Stage 1: DRG graph-based removal
389+ drg_result = drg_reduce (data , target_species , threshold )
390+ kept_species = drg_result ["kept_species" ]
391+ kept_reactions = drg_result ["kept_reactions" ]
392+ r_AB = drg_result ["r_AB" ]
393+
394+ # Stage 2: Among kept species, find QSS candidates
395+ qss_candidates , delta = detect_qss_species (
396+ data ,
397+ threshold = qss_threshold ,
398+ min_fraction = qss_min_fraction ,
399+ exclude_species = target_species , # never eliminate targets
400+ verbose = verbose
401+ )
402+
403+ # Only consider QSS species that survived DRG
404+ qss_in_kept = [sp for sp in qss_candidates if sp in kept_species ]
405+
406+ # Kept species that are not qss
407+ ode_species = [sp for sp in kept_species if sp not in qss_in_kept ]
408+
409+ if verbose :
410+ print (f" Original: { len (data ['species' ])} species, "
411+ f"{ len (data ['reactions' ])} reactions" )
412+ print (f" After DRG: { len (kept_species )} species, "
413+ f"{ len (kept_reactions )} reactions" )
414+ print (f" QSS species: { len (qss_in_kept )} species (removed by QSS)" )
415+ print (f" ODE species: { len (ode_species )} species (kept after QSS)" )
416+
417+ return {
418+ "kept_species" : kept_species ,
419+ "kept_reactions" : kept_reactions ,
420+ "qss_species" : qss_in_kept ,
421+ "ode_species" : ode_species ,
422+ "r_AB" : r_AB ,
423+ "delta" : delta ,
424+ }
425+
426+
291427def main ():
292428 import argparse
293429
@@ -304,16 +440,55 @@ def main():
304440 help = "DRG threshold" )
305441 p .add_argument ("-s" , "--sweep" , action = "store_true" ,
306442 help = "Sweep over multiple thresholds" )
443+ p .add_argument ("-q" , "--qss" , action = "store_true" ,
444+ help = "Enable QSS detection after DRG reduction" )
445+ p .add_argument ("--qss-threshold" , type = float , default = 0.1 ,
446+ help = "QSS imbalance threshold" )
447+ p .add_argument ("--qss-min-fraction" , type = float , default = 0.9 ,
448+ help = "Min. fraction of time steps satisfying QSS criterion" )
449+ p .add_argument ("-v" , "--verbose" , action = "store_true" ,
450+ help = "Enable verbose output" )
307451
308452 args = p .parse_args ()
309453
310454 data = load_data (args .simname )
311455
312456 if args .sweep :
313457 sweep_thresholds (data , args .target )
458+ elif args .qss :
459+ result = drg_with_qss (data , args .target ,
460+ threshold = args .threshold ,
461+ qss_threshold = args .qss_threshold ,
462+ verbose = args .verbose )
463+
464+ print (f"\n ODE species ({ len (result ['ode_species' ])} ):" )
465+ for sp in result ["ode_species" ]:
466+ print (f" { sp } " )
467+
468+ print (f"\n QSS species ({ len (result ['qss_species' ])} ):" )
469+ for sp in result ["qss_species" ]:
470+ print (f" { sp } " )
471+
472+ removed_species = sorted (
473+ set (data ["species" ]) - set (result ["kept_species" ]))
474+ print (f"\n Removed species ({ len (removed_species )} ):" )
475+ for sp in removed_species :
476+ print (f" { sp } " )
477+
478+ print (f"\n Kept reactions ({ len (result ['kept_reactions' ])} ):" )
479+ for rx in result ["kept_reactions" ]:
480+ print (f" { rx } " )
481+
482+ removed_reactions = sorted (
483+ set (data ["reactions" ]) - set (result ["kept_reactions" ]))
484+ print (f"\n Removed reactions ({ len (removed_reactions )} ):" )
485+ for rx in removed_reactions :
486+ print (f" { rx } " )
314487 else :
315- kept_species , kept_reactions , kept_species_idx , kept_reactions_idx , r_AB = \
316- drg_reduce (data , args .target , args .threshold )
488+ result = drg_reduce (data , args .target , args .threshold ,
489+ verbose = args .verbose )
490+ kept_species = result ["kept_species" ]
491+ kept_reactions = result ["kept_reactions" ]
317492
318493 print (f"\n Kept species ({ len (kept_species )} ):" )
319494 for sp in kept_species :
@@ -328,7 +503,8 @@ def main():
328503 for rx in kept_reactions :
329504 print (f" { rx } " )
330505
331- removed_reactions = sorted (set (data ["reactions" ]) - set (kept_reactions ))
506+ removed_reactions = sorted (
507+ set (data ["reactions" ]) - set (kept_reactions ))
332508 print (f"\n Removed reactions ({ len (removed_reactions )} ):" )
333509 for rx in removed_reactions :
334510 print (f" { rx } " )
0 commit comments