Skip to content

Commit 9ea8997

Browse files
Add QSS (quasi steady state) analysis in chemistry_reduce.py
1 parent 6b26cd1 commit 9ea8997

1 file changed

Lines changed: 198 additions & 22 deletions

File tree

tools/chemistry_reduce.py

Lines changed: 198 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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("\nComputing 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"\nDRG 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"\nDRG 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

229236
def 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"\nIdentified {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+
291427
def 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"\nODE species ({len(result['ode_species'])}):")
465+
for sp in result["ode_species"]:
466+
print(f" {sp}")
467+
468+
print(f"\nQSS 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"\nRemoved species ({len(removed_species)}):")
475+
for sp in removed_species:
476+
print(f" {sp}")
477+
478+
print(f"\nKept 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"\nRemoved 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"\nKept 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"\nRemoved reactions ({len(removed_reactions)}):")
333509
for rx in removed_reactions:
334510
print(f" {rx}")

0 commit comments

Comments
 (0)