simjsr 0.0.2__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simjsr-0.0.2 → simjsr-0.0.4}/PKG-INFO +2 -1
- {simjsr-0.0.2 → simjsr-0.0.4}/pyproject.toml +2 -1
- {simjsr-0.0.2 → simjsr-0.0.4}/src/simjsr/__init__.py +1 -1
- {simjsr-0.0.2 → simjsr-0.0.4}/src/simjsr/cli.py +4 -7
- {simjsr-0.0.2 → simjsr-0.0.4}/src/simjsr/config.py +23 -31
- simjsr-0.0.4/src/simjsr/run.py +221 -0
- simjsr-0.0.2/src/simjsr/run.py +0 -211
- {simjsr-0.0.2 → simjsr-0.0.4}/src/simjsr/convert.py +0 -0
- {simjsr-0.0.2 → simjsr-0.0.4}/src/simjsr/data/chem.inp +0 -0
- {simjsr-0.0.2 → simjsr-0.0.4}/src/simjsr/data/chem.yaml +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: simjsr
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Author: Andreas V. Copan
|
|
5
5
|
Author-email: Andreas V. Copan <avcopan@uga.edu>
|
|
6
6
|
Requires-Dist: cantera>=3.2.0
|
|
7
|
+
Requires-Dist: func-timeout>=4.3.5
|
|
7
8
|
Requires-Dist: polars>=1.41.2
|
|
8
9
|
Requires-Dist: pydantic>=2.13.4
|
|
9
10
|
Requires-Dist: pyyaml>=6.0.3
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simjsr"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.4"
|
|
4
4
|
authors = [{name = "Andreas V. Copan", email = "avcopan@uga.edu"}]
|
|
5
5
|
requires-python = ">= 3.13"
|
|
6
6
|
dependencies = [
|
|
7
7
|
"cantera>=3.2.0",
|
|
8
|
+
"func-timeout>=4.3.5",
|
|
8
9
|
"polars>=1.41.2",
|
|
9
10
|
"pydantic>=2.13.4",
|
|
10
11
|
"pyyaml>=6.0.3",
|
|
@@ -48,11 +48,8 @@ def run(
|
|
|
48
48
|
] = False,
|
|
49
49
|
) -> None:
|
|
50
50
|
"""Run a JSR simulation workflow."""
|
|
51
|
-
typer.echo("Loading config file\n")
|
|
52
|
-
configs = Config.multi_from_yaml(file=config_file)
|
|
53
|
-
|
|
54
51
|
multi(
|
|
55
|
-
|
|
52
|
+
config_file,
|
|
56
53
|
pass_state=not no_pass_state,
|
|
57
54
|
output_file=Path(output_file),
|
|
58
55
|
logger=typer.echo,
|
|
@@ -118,9 +115,9 @@ def example(
|
|
|
118
115
|
if multi_composition:
|
|
119
116
|
composition_file = dir_path / File.composition
|
|
120
117
|
data = {
|
|
121
|
-
"CH4": [0.05, 0.
|
|
122
|
-
"O2": [0.21, 0.
|
|
123
|
-
"N2": [0.74, 0.
|
|
118
|
+
"CH4": [0.05, 0.05, 0.05],
|
|
119
|
+
"O2": [0.21, 0.22, 0.23],
|
|
120
|
+
"N2": [0.74, 0.73, 0.72],
|
|
124
121
|
}
|
|
125
122
|
pl.DataFrame(data).write_csv(composition_file)
|
|
126
123
|
update[Key.composition] = File.composition
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Configuration for simulations."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, Self
|
|
5
5
|
|
|
6
6
|
import polars as pl
|
|
7
7
|
import yaml
|
|
@@ -33,7 +33,7 @@ class Config(BaseModel):
|
|
|
33
33
|
description="Starting composition (mole fractions)"
|
|
34
34
|
)
|
|
35
35
|
volume: float = Field(description="Reactor volume (cm^3)", default=1.0)
|
|
36
|
-
time_out:
|
|
36
|
+
time_out: float | None = Field(
|
|
37
37
|
description="Time limit for the simulation (s)", default=None
|
|
38
38
|
)
|
|
39
39
|
chemkin_file: Path | None = Field(
|
|
@@ -101,19 +101,21 @@ class Config(BaseModel):
|
|
|
101
101
|
@classmethod
|
|
102
102
|
def from_yaml(
|
|
103
103
|
cls, *, file: str | Path | None = None, text: str | Path | None = None
|
|
104
|
-
) ->
|
|
104
|
+
) -> Self:
|
|
105
105
|
"""Instantiate from YAML file or text."""
|
|
106
|
-
config, *extra_configs = cls.
|
|
106
|
+
(config, *extra_configs), _ = cls.all_with_dataframe_from_yaml(
|
|
107
|
+
file=file, text=text
|
|
108
|
+
)
|
|
107
109
|
if extra_configs:
|
|
108
110
|
msg = f"Cannot read config from multi-config YAML. {extra_configs = }"
|
|
109
111
|
raise ValueError(msg)
|
|
110
112
|
return config
|
|
111
113
|
|
|
112
114
|
@classmethod
|
|
113
|
-
def
|
|
115
|
+
def all_with_dataframe_from_yaml(
|
|
114
116
|
cls, *, file: str | Path | None = None, text: str | Path | None = None
|
|
115
|
-
) ->
|
|
116
|
-
"""Instantiate
|
|
117
|
+
) -> tuple[list[Self], pl.DataFrame | None]:
|
|
118
|
+
"""Instantiate from YAML file or text and return as a DataFrame."""
|
|
117
119
|
if file is not None:
|
|
118
120
|
text = Path(file).read_text()
|
|
119
121
|
|
|
@@ -122,8 +124,6 @@ class Config(BaseModel):
|
|
|
122
124
|
raise ValueError(msg)
|
|
123
125
|
|
|
124
126
|
data = yaml.safe_load(text)
|
|
125
|
-
|
|
126
|
-
datas = [data]
|
|
127
127
|
temperature = data.get(Key.temperature)
|
|
128
128
|
composition = data.get(Key.composition)
|
|
129
129
|
|
|
@@ -131,32 +131,18 @@ class Config(BaseModel):
|
|
|
131
131
|
msg = f"{temperature = } and {composition = } cannot both be file paths."
|
|
132
132
|
raise TypeError(msg)
|
|
133
133
|
|
|
134
|
+
df = None
|
|
135
|
+
datas = [data]
|
|
134
136
|
if isinstance(temperature, str):
|
|
135
|
-
|
|
136
|
-
datas = [{**data,
|
|
137
|
+
df = pl.read_csv(temperature)
|
|
138
|
+
datas = [{**data, Key.temperature: t} for t in df[:, 0]]
|
|
137
139
|
|
|
138
140
|
if isinstance(composition, str):
|
|
139
|
-
|
|
140
|
-
datas = [{**data,
|
|
141
|
-
|
|
142
|
-
return [cls.model_validate(d) for d in datas]
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _to_yaml_serialized_value(value: object) -> str:
|
|
146
|
-
"""Serialize a value to a YAML string."""
|
|
147
|
-
return yaml.safe_dump(value, default_flow_style=True).splitlines()[0]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _load_temperatures_from_file(temps_file: Path | str) -> list[float]:
|
|
151
|
-
"""Load temperatures (K) from a CSV file with a 'temperature' column."""
|
|
152
|
-
df = pl.read_csv(temps_file)
|
|
153
|
-
return df.get_column(df.columns[0]).cast(pl.Float64).to_list()
|
|
141
|
+
df = pl.read_csv(composition)
|
|
142
|
+
datas = [{**data, Key.composition: c} for c in df.iter_rows(named=True)]
|
|
154
143
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"""Load compositions from a YAML file with a 'compositions' key."""
|
|
158
|
-
df = pl.read_csv(comps_file)
|
|
159
|
-
return df.to_dicts()
|
|
144
|
+
configs = [cls.model_validate(d) for d in datas]
|
|
145
|
+
return configs, df
|
|
160
146
|
|
|
161
147
|
|
|
162
148
|
class Key:
|
|
@@ -164,3 +150,9 @@ class Key:
|
|
|
164
150
|
|
|
165
151
|
temperature = "temperature"
|
|
166
152
|
composition = "composition"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# Helpers
|
|
156
|
+
def _to_yaml_serialized_value(value: object) -> str:
|
|
157
|
+
"""Serialize a value to a YAML string."""
|
|
158
|
+
return yaml.safe_dump(value, default_flow_style=True).splitlines()[0]
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Reactors."""
|
|
2
|
+
|
|
3
|
+
import textwrap
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from copy import replace
|
|
6
|
+
from datetime import UTC, datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from time import perf_counter
|
|
9
|
+
from typing import Any, TypeGuard
|
|
10
|
+
|
|
11
|
+
import cantera as ct
|
|
12
|
+
import polars as pl
|
|
13
|
+
from func_timeout import FunctionTimedOut, func_timeout
|
|
14
|
+
|
|
15
|
+
from . import convert
|
|
16
|
+
from .config import Config
|
|
17
|
+
|
|
18
|
+
Logger = Callable[[str], Any]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def multi(
|
|
22
|
+
config_: Sequence[Config] | Path | str,
|
|
23
|
+
*,
|
|
24
|
+
pass_state: bool = True,
|
|
25
|
+
output_file: Path | None = None,
|
|
26
|
+
logger: Logger = lambda _: None,
|
|
27
|
+
) -> list[dict[str, float]]:
|
|
28
|
+
"""Run multiple jet-stirred reactor simulations.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config_: Configurations for the simulations
|
|
32
|
+
pass_state: Whether to pass solved state to the next simulation, if its
|
|
33
|
+
initial composition is the same
|
|
34
|
+
logger: Optional logger for messages
|
|
35
|
+
output_file: Optional file to save simulation results
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Steady state mole fractions for each simulation
|
|
39
|
+
"""
|
|
40
|
+
df = None
|
|
41
|
+
if isinstance(config_, Sequence) and not isinstance(config_, str):
|
|
42
|
+
configs = config_
|
|
43
|
+
else:
|
|
44
|
+
logger("Loading config file")
|
|
45
|
+
|
|
46
|
+
configs, df = Config.all_with_dataframe_from_yaml(file=config_)
|
|
47
|
+
|
|
48
|
+
if not is_sequence_of(configs, Config):
|
|
49
|
+
msg = "All configurations must be of type Config"
|
|
50
|
+
raise TypeError(msg)
|
|
51
|
+
|
|
52
|
+
config_count = len(configs)
|
|
53
|
+
last_config = last_mole_fracs = None
|
|
54
|
+
mole_fracs_lst = []
|
|
55
|
+
for num, init_config in enumerate(configs, start=1):
|
|
56
|
+
yaml_text = init_config.yaml_text(describe=True)
|
|
57
|
+
logger(f"Run {num} / {config_count}:")
|
|
58
|
+
logger(textwrap.indent(yaml_text.strip(), " "))
|
|
59
|
+
|
|
60
|
+
# If requested, re-use solved composition
|
|
61
|
+
config = init_config
|
|
62
|
+
if (
|
|
63
|
+
pass_state
|
|
64
|
+
and init_config.is_compatible_with(last_config)
|
|
65
|
+
and last_mole_fracs is not None
|
|
66
|
+
):
|
|
67
|
+
config = replace(init_config, composition=last_mole_fracs)
|
|
68
|
+
|
|
69
|
+
mole_fracs = single(config=config, logger=logger, raise_on_timout=False)
|
|
70
|
+
mole_fracs_lst.append(mole_fracs)
|
|
71
|
+
|
|
72
|
+
last_config = init_config
|
|
73
|
+
last_mole_fracs = mole_fracs
|
|
74
|
+
|
|
75
|
+
logger("")
|
|
76
|
+
|
|
77
|
+
if output_file is not None:
|
|
78
|
+
df_out = pl.from_dicts(mole_fracs_lst)
|
|
79
|
+
if df is not None:
|
|
80
|
+
df_out = hconcat_rename_left(df, df_out, suffix="_init")
|
|
81
|
+
|
|
82
|
+
df_out.write_csv(output_file)
|
|
83
|
+
|
|
84
|
+
return mole_fracs_lst
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def single(
|
|
88
|
+
config: Config | Path | str,
|
|
89
|
+
*,
|
|
90
|
+
logger: Logger = lambda _: None,
|
|
91
|
+
output_file: Path | None = None,
|
|
92
|
+
raise_on_timout: bool = True,
|
|
93
|
+
) -> dict[str, float]:
|
|
94
|
+
"""Run a single jet-stirred reactor simulation.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
config: Configuration for the simulation
|
|
98
|
+
logger: Optional logger for messages
|
|
99
|
+
output_file: Optional file to save simulation results
|
|
100
|
+
raise_on_timout: Whether to raise a TimeoutError if the simulation times out
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Steady state mole fractions
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
TimeoutError: If the simulation exceeds the configured time limit
|
|
107
|
+
"""
|
|
108
|
+
start_counter = clock_start(logger, "Starting simulation")
|
|
109
|
+
|
|
110
|
+
config = config if isinstance(config, Config) else Config.from_yaml(file=config)
|
|
111
|
+
|
|
112
|
+
if not config.cantera_file.exists():
|
|
113
|
+
generate_cantera_file_from_chemkin(config, logger=logger)
|
|
114
|
+
|
|
115
|
+
phase = ct.Solution(config.cantera_file)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
mole_fracs = (
|
|
119
|
+
_single(config)
|
|
120
|
+
if config.time_out is None
|
|
121
|
+
else func_timeout(config.time_out, _single, (config,))
|
|
122
|
+
)
|
|
123
|
+
except FunctionTimedOut:
|
|
124
|
+
clock_end(logger, "Simulation timed out", start_counter)
|
|
125
|
+
mole_fracs = {s: float("nan") for s in phase.species_names}
|
|
126
|
+
if raise_on_timout:
|
|
127
|
+
raise
|
|
128
|
+
else:
|
|
129
|
+
clock_end(logger, "Finished simulation", start_counter)
|
|
130
|
+
|
|
131
|
+
if output_file is not None:
|
|
132
|
+
pl.from_dicts([mole_fracs]).write_csv(output_file)
|
|
133
|
+
|
|
134
|
+
return mole_fracs
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _single(config: Config) -> dict[str, float]:
|
|
138
|
+
"""Run a single jet-stirred reactor simulation (no timeout handling or logging).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
config: Configuration for the simulation
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Steady state mole fractions
|
|
145
|
+
"""
|
|
146
|
+
phase = ct.Solution(config.cantera_file)
|
|
147
|
+
|
|
148
|
+
# Use composition from the previous iteration to speed up convergence
|
|
149
|
+
phase.TPX = config.temperature, config.pressure * ct.one_atm, config.composition
|
|
150
|
+
|
|
151
|
+
# Set up JSR: inlet -> flow control -> reactor -> pressure control -> exhaust
|
|
152
|
+
volume_m3 = config.volume * (1e-2) ** 3
|
|
153
|
+
reactor = ct.IdealGasReactor(phase, energy="off", volume=volume_m3, clone=True)
|
|
154
|
+
exhaust = ct.Reservoir(phase, clone=True)
|
|
155
|
+
inlet = ct.Reservoir(phase, clone=True)
|
|
156
|
+
ct.PressureController(
|
|
157
|
+
upstream=reactor,
|
|
158
|
+
downstream=exhaust,
|
|
159
|
+
K=1e-3,
|
|
160
|
+
primary=ct.MassFlowController(
|
|
161
|
+
upstream=inlet,
|
|
162
|
+
downstream=reactor,
|
|
163
|
+
mdot=reactor.mass / config.residence_time,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
reactor_net = ct.ReactorNet([reactor])
|
|
167
|
+
reactor_net.advance_to_steady_state(max_steps=100000)
|
|
168
|
+
return reactor.phase.mole_fraction_dict()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Helpers
|
|
172
|
+
def generate_cantera_file_from_chemkin(
|
|
173
|
+
config: Config, *, logger: Logger = lambda _: None
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Ensure the Cantera file is available."""
|
|
176
|
+
if config.chemkin_file is None:
|
|
177
|
+
msg = "chemkin_file must be provided if cantera_file does not exist."
|
|
178
|
+
raise ValueError(msg)
|
|
179
|
+
|
|
180
|
+
if not config.chemkin_file.exists():
|
|
181
|
+
msg = f"{config.chemkin_file = } does not exist."
|
|
182
|
+
raise ValueError(msg)
|
|
183
|
+
|
|
184
|
+
logger(f"Converting {config.chemkin_file} to Cantera format")
|
|
185
|
+
|
|
186
|
+
convert.from_chemkin(
|
|
187
|
+
chemkin_file=config.chemkin_file,
|
|
188
|
+
chemkin_thermo_file=config.chemkin_thermo_file,
|
|
189
|
+
cantera_file=config.cantera_file,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def clock_start(logger: Logger, event: str) -> float:
|
|
194
|
+
"""Log the start time of an event."""
|
|
195
|
+
start_time = datetime.now(tz=UTC)
|
|
196
|
+
logger(f"{event} at {start_time:%Y-%m-%d %H:%M:%S}")
|
|
197
|
+
return perf_counter()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def clock_end(logger: Logger, event: str, start_counter: float) -> None:
|
|
201
|
+
"""Log the start time of a simulation."""
|
|
202
|
+
end_time = datetime.now(tz=UTC)
|
|
203
|
+
elapsed = timedelta(seconds=perf_counter() - start_counter)
|
|
204
|
+
logger(f"{event} at {end_time:%Y-%m-%d %H:%M:%S}")
|
|
205
|
+
logger(f"Elapsed time: {elapsed}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def is_sequence_of[T](seq: Sequence[object], typ: type[T]) -> TypeGuard[Sequence[T]]:
|
|
209
|
+
"""Check the types of the elements in a sequence."""
|
|
210
|
+
return all(isinstance(x, typ) for x in seq)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def hconcat_rename_left(
|
|
214
|
+
df1: pl.DataFrame, df2: pl.DataFrame, suffix: str = "_left"
|
|
215
|
+
) -> pl.DataFrame:
|
|
216
|
+
"""Horizontally concatenate dataframes, renaming the left dataframe columns."""
|
|
217
|
+
clashes = set(df1.columns) & set(df2.columns)
|
|
218
|
+
|
|
219
|
+
df1 = df1.rename({col: f"{col}{suffix}" for col in clashes})
|
|
220
|
+
|
|
221
|
+
return pl.concat([df1, df2], how="horizontal")
|
simjsr-0.0.2/src/simjsr/run.py
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"""Reactors."""
|
|
2
|
-
|
|
3
|
-
import signal
|
|
4
|
-
import textwrap
|
|
5
|
-
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
-
from copy import replace
|
|
7
|
-
from datetime import UTC, datetime, timedelta
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from time import perf_counter
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
import cantera as ct
|
|
13
|
-
import polars as pl
|
|
14
|
-
|
|
15
|
-
from . import convert
|
|
16
|
-
from .config import Config
|
|
17
|
-
|
|
18
|
-
Logger = Callable[[str], Any]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def single(
|
|
22
|
-
config: Config, *, logger: Logger | None = None, output_file: Path | None = None
|
|
23
|
-
) -> dict[str, float]:
|
|
24
|
-
"""Run a single jet-stirred reactor simulation.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
config: Configuration for the simulation
|
|
28
|
-
logger: Optional logger for messages
|
|
29
|
-
output_file: Optional file to save simulation results
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
Steady state mole fractions
|
|
33
|
-
|
|
34
|
-
Raises:
|
|
35
|
-
TimeoutError: If the simulation exceeds the configured time limit
|
|
36
|
-
"""
|
|
37
|
-
# Set up a timeout handler to prevent simulations from running indefinitely
|
|
38
|
-
if config.time_out is not None:
|
|
39
|
-
signal.signal(signal.SIGALRM, _timeout_handler)
|
|
40
|
-
signal.alarm(config.time_out)
|
|
41
|
-
|
|
42
|
-
if logger is not None:
|
|
43
|
-
start_time = datetime.now(tz=UTC)
|
|
44
|
-
start_counter = perf_counter()
|
|
45
|
-
logger(f"Starting simulation at {start_time:%Y-%m-%d %H:%M:%S}")
|
|
46
|
-
|
|
47
|
-
if not config.cantera_file.exists():
|
|
48
|
-
if config.chemkin_file is None:
|
|
49
|
-
msg = "chemkin_file must be provided if cantera_file does not exist."
|
|
50
|
-
raise ValueError(msg)
|
|
51
|
-
|
|
52
|
-
if not config.chemkin_file.exists():
|
|
53
|
-
msg = f"{config.chemkin_file = } does not exist."
|
|
54
|
-
raise ValueError(msg)
|
|
55
|
-
|
|
56
|
-
if logger is not None:
|
|
57
|
-
logger(
|
|
58
|
-
f"Converting {config.chemkin_file} to Cantera format "
|
|
59
|
-
f"({config.cantera_file})"
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
convert.from_chemkin(
|
|
63
|
-
chemkin_file=config.chemkin_file,
|
|
64
|
-
chemkin_thermo_file=config.chemkin_thermo_file,
|
|
65
|
-
cantera_file=config.cantera_file,
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
try:
|
|
69
|
-
mole_fracs = _single(config=config)
|
|
70
|
-
finally:
|
|
71
|
-
signal.alarm(0)
|
|
72
|
-
|
|
73
|
-
if logger is not None:
|
|
74
|
-
end_time = datetime.now(tz=UTC)
|
|
75
|
-
end_counter = perf_counter()
|
|
76
|
-
elapsed = timedelta(seconds=end_counter - start_counter)
|
|
77
|
-
logger(f"Finished simulation at {end_time:%Y-%m-%d %H:%M:%S}")
|
|
78
|
-
logger(f"Elapsed time: {elapsed}")
|
|
79
|
-
|
|
80
|
-
if output_file is not None:
|
|
81
|
-
pl.from_dicts([mole_fracs]).write_csv(output_file)
|
|
82
|
-
|
|
83
|
-
return mole_fracs
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def multi(
|
|
87
|
-
configs: Sequence[Config],
|
|
88
|
-
*,
|
|
89
|
-
pass_state: bool = True,
|
|
90
|
-
output_file: Path | None = None,
|
|
91
|
-
logger: Logger | None = None,
|
|
92
|
-
) -> list[dict[str, float]]:
|
|
93
|
-
"""Run multiple jet-stirred reactor simulations.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
configs: Configurations for the simulations
|
|
97
|
-
pass_state: Whether to pass solved state to the next simulation, if its
|
|
98
|
-
initial composition is the same
|
|
99
|
-
logger: Optional logger for messages
|
|
100
|
-
output_file: Optional file to save simulation results
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
Steady state mole fractions for each simulation
|
|
104
|
-
"""
|
|
105
|
-
config_count = len(configs)
|
|
106
|
-
last_config = last_mole_fracs = None
|
|
107
|
-
mole_fracs_lst = []
|
|
108
|
-
for num, init_config in enumerate(configs, start=1):
|
|
109
|
-
if logger is not None:
|
|
110
|
-
yaml_text = init_config.yaml_text(describe=True)
|
|
111
|
-
logger(f"Run {num} / {config_count}:")
|
|
112
|
-
logger(textwrap.indent(yaml_text.strip(), " "))
|
|
113
|
-
|
|
114
|
-
# If requested, re-use solved composition
|
|
115
|
-
config = init_config
|
|
116
|
-
if (
|
|
117
|
-
pass_state
|
|
118
|
-
and init_config.is_compatible_with(last_config)
|
|
119
|
-
and last_mole_fracs is not None
|
|
120
|
-
):
|
|
121
|
-
config = replace(init_config, composition=last_mole_fracs)
|
|
122
|
-
|
|
123
|
-
mole_fracs = single(config=config, logger=logger)
|
|
124
|
-
mole_fracs_lst.append(mole_fracs)
|
|
125
|
-
|
|
126
|
-
last_config = init_config
|
|
127
|
-
last_mole_fracs = mole_fracs
|
|
128
|
-
|
|
129
|
-
if logger is not None and num < config_count:
|
|
130
|
-
logger("")
|
|
131
|
-
|
|
132
|
-
if output_file is not None:
|
|
133
|
-
pl.from_dicts(mole_fracs_lst).write_csv(output_file)
|
|
134
|
-
|
|
135
|
-
return mole_fracs_lst
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def multi_temperature(
|
|
139
|
-
config: Config, temperatures: Sequence[float], *, logger: Logger | None = None
|
|
140
|
-
) -> list[dict[str, float]]:
|
|
141
|
-
"""Run multiple jet-stirred reactor simulations at different temperatures.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
config: Base configuration for the simulations
|
|
145
|
-
temperatures: List of temperatures (K)
|
|
146
|
-
logger: Optional logger for messages
|
|
147
|
-
|
|
148
|
-
Returns:
|
|
149
|
-
Steady state mole fractions for each simulation
|
|
150
|
-
"""
|
|
151
|
-
configs = [replace(config, temperature=t) for t in temperatures]
|
|
152
|
-
return multi(configs, logger=logger)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def multi_composition(
|
|
156
|
-
config: Config,
|
|
157
|
-
compositions: Sequence[Mapping[str, float]],
|
|
158
|
-
*,
|
|
159
|
-
logger: Logger | None = None,
|
|
160
|
-
) -> list[dict[str, float]]:
|
|
161
|
-
"""Run multiple jet-stirred reactor simulations at different compositions.
|
|
162
|
-
|
|
163
|
-
Args:
|
|
164
|
-
config: Base configuration for the simulations
|
|
165
|
-
compositions: List of compositions (mole fractions)
|
|
166
|
-
logger: Optional logger for messages
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
Steady state mole fractions for each simulation
|
|
170
|
-
"""
|
|
171
|
-
configs = [replace(config, composition=comp) for comp in compositions]
|
|
172
|
-
return multi(configs, logger=logger)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _single(config: Config) -> dict[str, float]:
|
|
176
|
-
"""Run a single jet-stirred reactor simulation (no timeout handling or logging).
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
config: Configuration for the simulation
|
|
180
|
-
|
|
181
|
-
Returns:
|
|
182
|
-
Steady state mole fractions
|
|
183
|
-
"""
|
|
184
|
-
phase = ct.Solution(config.cantera_file)
|
|
185
|
-
|
|
186
|
-
# Use composition from the previous iteration to speed up convergence
|
|
187
|
-
phase.TPX = config.temperature, config.pressure * ct.one_atm, config.composition
|
|
188
|
-
|
|
189
|
-
# Set up JSR: inlet -> flow control -> reactor -> pressure control -> exhaust
|
|
190
|
-
volume_m3 = config.volume * (1e-2) ** 3
|
|
191
|
-
reactor = ct.IdealGasReactor(phase, energy="off", volume=volume_m3, clone=True)
|
|
192
|
-
exhaust = ct.Reservoir(phase, clone=True)
|
|
193
|
-
inlet = ct.Reservoir(phase, clone=True)
|
|
194
|
-
ct.PressureController(
|
|
195
|
-
upstream=reactor,
|
|
196
|
-
downstream=exhaust,
|
|
197
|
-
K=1e-3,
|
|
198
|
-
primary=ct.MassFlowController(
|
|
199
|
-
upstream=inlet,
|
|
200
|
-
downstream=reactor,
|
|
201
|
-
mdot=reactor.mass / config.residence_time,
|
|
202
|
-
),
|
|
203
|
-
)
|
|
204
|
-
reactor_net = ct.ReactorNet([reactor])
|
|
205
|
-
reactor_net.advance_to_steady_state(max_steps=100000)
|
|
206
|
-
return reactor.phase.mole_fraction_dict()
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def _timeout_handler(_signum: int, _frame: object) -> None:
|
|
210
|
-
msg = "The simulation exceeded the configured time limit."
|
|
211
|
-
raise TimeoutError(msg)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|