Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,10 @@ must be no greater than 10 s, independent of the time-axis origin.

History-driven RCM points require separate `initial_temperature` and
`initial_pressure` quantities, using the existing temperature/pressure units.
This additive shape preserves the meaning of `temperature` and `pressure` in
every existing file: for RCM they still describe the measured compressed state,
not the state at the beginning of compression. The ideal-gas reactor instead
Their `temperature` and `pressure` fields are optional stated labels for the
measured compressed state; when supplied, they retain that meaning rather than
describing the state at the beginning of compression. Both fields remain
required for every point without `volume_history`. The ideal-gas reactor instead
starts at the explicitly supplied initial state and the first history volume.
A unit-area wall drives the exact forward slope of the linearly interpolated
volume, with integration boundaries at every history point. After the history
Expand Down
7 changes: 5 additions & 2 deletions docs/input_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ t3:

# An unversioned experimental file retains the legacy citation + data[{T, P, phi, idt}]
# format and nearest-grid comparison under idt_criterion. A version: 1 file instead contains
# points with explicit temperature/pressure/IDT units, a normalized list of SMILES mole
# points with explicit IDT units, a normalized list of SMILES mole
# fractions, apparatus ('shock tube' or 'rapid compression machine'), ignition_definition,
# optional uncertainty, and source {doi, record}. Each point is simulated directly at its
# own state. Supported targets: pressure, temperature, OH, OH*, CH, CH*. Supported types:
# own state. Temperature and pressure are required unless a point has volume_history, where
# initial_temperature and initial_pressure are required and temperature/pressure are optional
# stated compressed-state labels. Supported targets: pressure, temperature, OH, OH*, CH, CH*.
# Supported types:
# 'd/dt max', 'max', '1/2 max', 'd/dt max extrapolated'. Missing target species are refused;
# no ground-state or radical proxy is substituted. A target event that has not resolved by
# the integration horizon is also refused rather than scored from a truncated trace. See
Expand Down
6 changes: 4 additions & 2 deletions t3/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ class ExperimentalIDTPoint(BaseModel):
History-driven integration ends no later than 10 s after end of compression,
so its total elapsed duration is bounded by 20 s. History-free points are unchanged.
"""
temperature: ExperimentalTemperature
pressure: ExperimentalPressure
temperature: ExperimentalTemperature | None = None
pressure: ExperimentalPressure | None = None
composition: list[ExperimentalCompositionEntry]
apparatus: ExperimentalApparatusEnum
ignition_definition: ExperimentalIgnitionDefinition
Expand All @@ -374,6 +374,8 @@ def volume_history_horizon(self) -> float:
def validate_volume_history_state(self):
"""Keep existing post-compression state fields unchanged for all RCM points."""
if self.volume_history is None:
if self.temperature is None or self.pressure is None:
raise ValueError('temperature and pressure are required without a volume_history')
if self.initial_temperature is not None or self.initial_pressure is not None:
raise ValueError('initial_temperature and initial_pressure require a volume_history')
return self
Expand Down
4 changes: 2 additions & 2 deletions t3/simulate/cantera_idt.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,8 +918,8 @@ def _compare_versioned_experiment(self, exp: dict) -> dict:
criterion = raw_point['ignition_definition']
experimental_idt = convert_time_to_seconds(point.idt.value, point.idt.units)
comparison = {
'temperature': raw_point['temperature'],
'pressure': raw_point['pressure'],
'temperature': raw_point.get('temperature'),
'pressure': raw_point.get('pressure'),
'composition': raw_point['composition'],
'apparatus': raw_point['apparatus'],
'idt_exp': experimental_idt,
Expand Down
36 changes: 36 additions & 0 deletions tests/test_simulate/test_experimental_idt.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ def test_rcm_volume_history_schema_accepts_units_and_initial_states():
assert parsed.temperature.value == 1400.0


def test_rcm_volume_history_schema_allows_absent_compressed_state_labels():
point = _rcm_history_point()
del point['temperature']
del point['pressure']

parsed = _parsed_rcm_point(point)

assert parsed.temperature is None
assert parsed.pressure is None


@pytest.mark.parametrize('field', ['temperature', 'pressure'])
def test_history_free_point_requires_compressed_state_labels(field):
point = _point()
del point[field]

with pytest.raises(ValidationError, match='temperature and pressure are required without a volume_history'):
ExperimentalIDTFile.model_validate({'version': 1, 'points': [point]})


@pytest.mark.parametrize('mutation, message', [
({'time': {'values': [0.0], 'units': 's'}, 'volume': {'values': [1.0], 'units': 'm3'}}, 'at least two'),
({'time': {'values': [0.0, 1.0], 'units': 's'}}, 'equal length'),
Expand Down Expand Up @@ -260,10 +280,26 @@ def test_rcm_volume_history_reactive_ignition_is_relative_to_compression():
result = adapter._compare_versioned_experiment({'version': 1, 'points': [_rcm_history_point()]})
assert result['n_matched'] == 1
assert result['points'][0]['idt_sim'] == pytest.approx(expected, rel=1e-9)
assert result['points'][0]['temperature'] == {'value': 1400.0, 'units': 'K'}
assert result['points'][0]['pressure'] == {'value': 5.0, 'units': 'bar'}
assert expected > 0
assert np.max(history.T) > 2000


def test_rcm_volume_history_simulates_without_compressed_state_labels():
adapter = _adapter()
point = _rcm_history_point()
del point['temperature']
del point['pressure']

result = adapter._compare_versioned_experiment({'version': 1, 'points': [point]})

assert result['n_matched'] == 1
assert result['points'][0]['idt_sim'] > 0
assert result['points'][0]['temperature'] is None
assert result['points'][0]['pressure'] is None


def test_rcm_volume_history_ignition_does_not_depend_on_time_origin():
adapter = _adapter()
point = _rcm_history_point()
Expand Down
Loading