simjsr 0.0.2__tar.gz → 0.0.3__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.
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: simjsr
3
- Version: 0.0.2
3
+ Version: 0.0.3
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.2"
3
+ version = "0.0.3"
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",
@@ -1,6 +1,6 @@
1
1
  """simrun."""
2
2
 
3
- __version__ = "0.0.2"
3
+ __version__ = "0.0.3"
4
4
 
5
5
  from . import config, convert, run
6
6
  from .config import Config
@@ -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: int | None = Field(
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(
@@ -1,6 +1,5 @@
1
1
  """Reactors."""
2
2
 
3
- import signal
4
3
  import textwrap
5
4
  from collections.abc import Callable, Mapping, Sequence
6
5
  from copy import replace
@@ -11,6 +10,7 @@ from typing import Any
11
10
 
12
11
  import cantera as ct
13
12
  import polars as pl
13
+ from func_timeout import FunctionTimedOut, func_timeout
14
14
 
15
15
  from . import convert
16
16
  from .config import Config
@@ -19,7 +19,11 @@ Logger = Callable[[str], Any]
19
19
 
20
20
 
21
21
  def single(
22
- config: Config, *, logger: Logger | None = None, output_file: Path | None = None
22
+ config: Config,
23
+ *,
24
+ logger: Logger | None = None,
25
+ output_file: Path | None = None,
26
+ raise_on_timout: bool = True,
23
27
  ) -> dict[str, float]:
24
28
  """Run a single jet-stirred reactor simulation.
25
29
 
@@ -27,6 +31,7 @@ def single(
27
31
  config: Configuration for the simulation
28
32
  logger: Optional logger for messages
29
33
  output_file: Optional file to save simulation results
34
+ raise_on_timout: Whether to raise a TimeoutError if the simulation times out
30
35
 
31
36
  Returns:
32
37
  Steady state mole fractions
@@ -34,48 +39,26 @@ def single(
34
39
  Raises:
35
40
  TimeoutError: If the simulation exceeds the configured time limit
36
41
  """
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}")
42
+ start_counter = clock_start(logger, "Starting simulation")
46
43
 
47
44
  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)
45
+ generate_cantera_file_from_chemkin(config, logger=logger)
55
46
 
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
- )
47
+ phase = ct.Solution(config.cantera_file)
67
48
 
68
49
  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}")
50
+ mole_fracs = (
51
+ _single(config)
52
+ if config.time_out is None
53
+ else func_timeout(config.time_out, _single, (config,))
54
+ )
55
+ except FunctionTimedOut:
56
+ clock_end(logger, "Simulation timed out", start_counter)
57
+ mole_fracs = {s: float("nan") for s in phase.species_names}
58
+ if raise_on_timout:
59
+ raise
60
+ else:
61
+ clock_end(logger, "Finished simulation", start_counter)
79
62
 
80
63
  if output_file is not None:
81
64
  pl.from_dicts([mole_fracs]).write_csv(output_file)
@@ -120,7 +103,7 @@ def multi(
120
103
  ):
121
104
  config = replace(init_config, composition=last_mole_fracs)
122
105
 
123
- mole_fracs = single(config=config, logger=logger)
106
+ mole_fracs = single(config=config, logger=logger, raise_on_timout=False)
124
107
  mole_fracs_lst.append(mole_fracs)
125
108
 
126
109
  last_config = init_config
@@ -206,6 +189,44 @@ def _single(config: Config) -> dict[str, float]:
206
189
  return reactor.phase.mole_fraction_dict()
207
190
 
208
191
 
209
- def _timeout_handler(_signum: int, _frame: object) -> None:
210
- msg = "The simulation exceeded the configured time limit."
211
- raise TimeoutError(msg)
192
+ # Helpers
193
+ def generate_cantera_file_from_chemkin(
194
+ config: Config, *, logger: Logger | None = None
195
+ ) -> None:
196
+ """Ensure the Cantera file is available."""
197
+ if config.chemkin_file is None:
198
+ msg = "chemkin_file must be provided if cantera_file does not exist."
199
+ raise ValueError(msg)
200
+
201
+ if not config.chemkin_file.exists():
202
+ msg = f"{config.chemkin_file = } does not exist."
203
+ raise ValueError(msg)
204
+
205
+ if logger is not None:
206
+ logger(
207
+ f"Converting {config.chemkin_file} to Cantera format "
208
+ f"({config.cantera_file})"
209
+ )
210
+
211
+ convert.from_chemkin(
212
+ chemkin_file=config.chemkin_file,
213
+ chemkin_thermo_file=config.chemkin_thermo_file,
214
+ cantera_file=config.cantera_file,
215
+ )
216
+
217
+
218
+ def clock_start(logger: Logger | None, event: str) -> float:
219
+ """Log the start time of an event."""
220
+ if logger is not None:
221
+ start_time = datetime.now(tz=UTC)
222
+ logger(f"{event} at {start_time:%Y-%m-%d %H:%M:%S}")
223
+ return perf_counter()
224
+
225
+
226
+ def clock_end(logger: Logger | None, event: str, start_counter: float) -> None:
227
+ """Log the start time of a simulation."""
228
+ if logger is not None:
229
+ end_time = datetime.now(tz=UTC)
230
+ elapsed = timedelta(seconds=perf_counter() - start_counter)
231
+ logger(f"{event} at {end_time:%Y-%m-%d %H:%M:%S}")
232
+ logger(f"Elapsed time: {elapsed}")
File without changes
File without changes
File without changes
File without changes