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
45 changes: 38 additions & 7 deletions fdsreader/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,32 @@ def _wall_times_to_utc(stamps: Sequence[str]) -> np.ndarray:
return np.array(utc, dtype="datetime64[ms]")


class StepData(dict):
"""The columns of CHID_steps.csv by name, see :attr:`Simulation.steps`.

The key ``"Time Step"`` holds the wall-clock times for compatibility with earlier versions
and warns once when used: in the FDS User Guide the column ``Time Step`` is the iteration
number, available here as ``"Step"``, and the wall-clock time is ``"Wall Time"``.
"""

_warned = False

def __getitem__(self, key):
if key == "Time Step" and not StepData._warned:
StepData._warned = True
warnings.warn(
'steps["Time Step"] holds the wall-clock time, not the time step number. Use '
'steps["Wall Time"] for the time and steps["Step"] for the number; "Time Step" will '
"become the number, as in the FDS User Guide, in a future release.",
FutureWarning,
stacklevel=2,
)
return super().__getitem__(key)

def get(self, key, default=None):
return self[key] if key in self else default


class Simulation:
"""Master class managing all data for a given simulation.

Expand Down Expand Up @@ -80,7 +106,10 @@ class Simulation:
:ivar geom_data: All geometry data by quantity combined into a :class:`GeometryCollection`.
:ivar cpu: Dictionary mapping .csv header keys to numpy arrays containing cpu data.
:ivar hrr: Dictionary mapping .csv header keys to numpy arrays containing hrr data.
:ivar steps: Dictionary mapping .csv header keys to numpy arrays containing steps data.
:ivar steps: The time step data of CHID_steps.csv as a :class:`StepData` dictionary:
``"Step"`` (iteration number), ``"Wall Time"`` (date and time, UTC), ``"Step Size"`` and
``"Simulation Time"`` (s), and ``"CPU Time"`` (s since FDS started). ``"Time Step"`` still
holds the wall-clock time, but warns: it will become the iteration number.
:ivar ctrl: Dictionary mapping control (&CTRL) names to numpy arrays of their state over time.
Only set if the simulation has a CTRL csv export.
:ivar mass: Dictionary mapping species names to numpy arrays of their mass history. Only set
Expand All @@ -91,7 +120,7 @@ class Simulation:

# Bump when the pickled structure of a Simulation or its data classes changes, so caches
# written by an older layout are parsed again instead of being loaded with missing attributes.
_CACHE_FORMAT = 2
_CACHE_FORMAT = 3
_loading = False

def __new__(cls, path: str):
Expand Down Expand Up @@ -1236,15 +1265,17 @@ def _load_HRR_data(self, file_path: str) -> Dict[str, np.ndarray]:
return self._transform_csv_data(keys, values)

@log_error("csv")
def _load_step_data(self, file_path: str) -> Dict[str, np.ndarray]:
def _load_step_data(self, file_path: str) -> "StepData":
with open(file_path) as infile:
infile.readline()
keys = [name.replace('"', "").replace("\n", "").strip() for name in infile.readline().split(",")][2:]
wall_times = np.loadtxt(file_path, dtype=str, delimiter=",", ndmin=1, usecols=1, skiprows=2)
timesteps = _wall_times_to_utc(wall_times)
steps = np.loadtxt(file_path, dtype=int, delimiter=",", ndmin=1, usecols=0, skiprows=2)
wall_times = _wall_times_to_utc(np.loadtxt(file_path, dtype=str, delimiter=",", ndmin=1, usecols=1, skiprows=2))
float_values = np.loadtxt(file_path, delimiter=",", usecols=range(2, len(keys) + 2), ndmin=2, skiprows=2)
data = self._transform_csv_data(keys, float_values)
data["Time Step"] = timesteps
data = StepData(self._transform_csv_data(keys, float_values))
data["Step"] = steps
data["Wall Time"] = wall_times
data["Time Step"] = wall_times # kept for compatibility, see StepData
return data

@log_error("csv")
Expand Down
1 change: 1 addition & 0 deletions fdsreader/slcf/geomslice.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class GeomSlice(NumpyArrayMixin):
_handled_functions = _HANDLED_FUNCTIONS
_HANDLED_FUNCTIONS[np.mean] = NumpyArrayMixin.mean
_HANDLED_FUNCTIONS[np.std] = NumpyArrayMixin.std
_global_array_hint = "use the 'data', 'vertices' and 'faces' properties, which combine all meshes."

def _array_subitems(self):
return self._subgeomslices.values()
Expand Down
5 changes: 4 additions & 1 deletion fdsreader/utils/array_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class Slice(NumpyArrayMixin):
#: (see class docstring). Left as an empty dict here only as a safe default.
_handled_functions: Dict[Any, Any] = {}

#: How to get one array of all subitems, for the error message of :meth:`__array__`.
_global_array_hint = "use the 'to_global' method and use the returned numpy-array explicitly."

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if cls._handled_functions is NumpyArrayMixin._handled_functions:
Expand Down Expand Up @@ -76,7 +79,7 @@ def __array__(self):
raise TypeError(
f"{name}s can not be converted to numpy arrays, but they support all typical numpy"
" operations such as np.multiply. If a 'global' array containing all subitems is"
" required, use the 'to_global' method and use the returned numpy-array explicitly."
f" required, {self._global_array_hint}"
)

def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_crash_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_steps_load_without_timezone_warning(monkeypatch):
warnings.simplefilter("error")
steps = Simulation(str(CASES_DIR / "steckler_data")).steps
# The wall-clock times are written with an offset of +01:00 and stored as UTC
assert steps["Time Step"][0] == np.datetime64("2024-03-14T08:39:53.199")
assert steps["Wall Time"][0] == np.datetime64("2024-03-14T08:39:53.199")


@pytest.mark.parametrize(
Expand Down
59 changes: 59 additions & 0 deletions tests/test_steps_and_array_hints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""sim.steps follows the column names of the FDS User Guide (section "Time Step Data"), and the
error message for converting a container to a numpy array names a method the class has."""

import warnings
from pathlib import Path

import numpy as np
import pytest

from fdsreader import Simulation, settings
from fdsreader.simulation import StepData

CASES_DIR = Path(__file__).parent / "cases"


@pytest.fixture
def steps(monkeypatch):
monkeypatch.setattr(settings, "ENABLE_CACHING", False)
monkeypatch.setattr(StepData, "_warned", False)
return Simulation(str(CASES_DIR / "steckler_data")).steps


def test_steps_columns(steps):
with warnings.catch_warnings():
warnings.simplefilter("error")
assert steps["Step"].dtype.kind == "i"
assert steps["Step"][:3].tolist() == [1, 2, 3]
assert steps["Wall Time"].dtype == np.dtype("datetime64[ms]")
assert steps["Wall Time"][0] == np.datetime64("2024-03-14T08:39:53.199") # +01:00 as UTC
assert steps["Simulation Time"][0] == pytest.approx(0.10823)
assert len({len(column) for column in steps.values()}) == 1


def test_time_step_key_warns_once_and_still_holds_wall_time(steps):
with pytest.warns(FutureWarning, match="Wall Time") as record:
old = steps["Time Step"]
assert record[0].filename == __file__ # points at the caller, not into fdsreader
np.testing.assert_array_equal(old, steps["Wall Time"])
with warnings.catch_warnings():
warnings.simplefilter("error")
steps["Time Step"]


def test_time_step_key_warns_through_get(steps):
with pytest.warns(FutureWarning):
assert steps.get("Time Step") is not None
assert steps.get("missing", 1) == 1


def test_geomslice_array_hint_names_its_properties():
geomslice = Simulation(str(CASES_DIR / "geomslice_data_fds611")).geomslices[0]
with pytest.raises(TypeError, match="'data', 'vertices' and 'faces'"):
np.asarray(geomslice)


def test_slice_array_hint_names_to_global():
slc = Simulation(str(CASES_DIR / "steckler_data")).slices[0]
with pytest.raises(TypeError, match="to_global"):
np.asarray(slc)
Loading