Skip to content

Simulation API

The Simulation class (and its McStas / McXtrace subclasses) is the recommended Python API for compiling McCode instruments and running single-point simulations or parameter scans.

The SimulationOutput class holds all files written to disk by a single run, providing both backward-compatible dict-like access to McCode detector data and richer information about every file the binary produced.


McStas and McXtrace

Use these subclasses directly — they pre-set the instrument flavor so you never have to import Flavor yourself:

mccode_antlr.run.simulation.McStas

Bases: Simulation

A :class:Simulation pre-configured for McStas (neutron) instruments.

Source code in src/mccode_antlr/run/simulation.py
class McStas(Simulation):
    """A :class:`Simulation` pre-configured for McStas (neutron) instruments."""

    def __init__(self, instr: Instr):
        super().__init__(instr, Flavor.MCSTAS)

__init__(instr)

Source code in src/mccode_antlr/run/simulation.py
def __init__(self, instr: Instr):
    super().__init__(instr, Flavor.MCSTAS)

mccode_antlr.run.simulation.McXtrace

Bases: Simulation

A :class:Simulation pre-configured for McXtrace (X-ray) instruments.

Source code in src/mccode_antlr/run/simulation.py
class McXtrace(Simulation):
    """A :class:`Simulation` pre-configured for McXtrace (X-ray) instruments."""

    def __init__(self, instr: Instr):
        super().__init__(instr, Flavor.MCXTRACE)

__init__(instr)

Source code in src/mccode_antlr/run/simulation.py
def __init__(self, instr: Instr):
    super().__init__(instr, Flavor.MCXTRACE)

Simulation

mccode_antlr.run.simulation.Simulation

A compiled McCode instrument simulation.

Provides a convenient Python API for compiling an instrument and running single-point simulations or parameter scans without using the CLI.

Usage::

from mccode_antlr.run import McStas

sim = McStas(instr).compile('/tmp/build')

# Single point
result, dats = sim.run({'x': 1.5, 'y': 2}, ncount=1000)

# Linear parameter scan
results = sim.scan({'x': '1:0.5:5', 'y': 2}, ncount=1000)

# Grid (Cartesian product) scan
results = sim.scan({'x': '1:1:3', 'y': '10:1:12'}, grid=True, ncount=1000)
Source code in src/mccode_antlr/run/simulation.py
class Simulation:
    """A compiled McCode instrument simulation.

    Provides a convenient Python API for compiling an instrument and running single-point
    simulations or parameter scans without using the CLI.

    Usage::

        from mccode_antlr.run import McStas

        sim = McStas(instr).compile('/tmp/build')

        # Single point
        result, dats = sim.run({'x': 1.5, 'y': 2}, ncount=1000)

        # Linear parameter scan
        results = sim.scan({'x': '1:0.5:5', 'y': 2}, ncount=1000)

        # Grid (Cartesian product) scan
        results = sim.scan({'x': '1:1:3', 'y': '10:1:12'}, grid=True, ncount=1000)
    """

    def __init__(self, instr: Instr, flavor: Flavor):
        self.instr = instr
        self.flavor = flavor
        self._binary: Path | None = None
        self._target: CBinaryTarget | None = None
        self._compile_dir: Path | None = None
        self._tmpdir = None  # TemporaryDirectory when compile() owns the dir

    def compile(
        self,
        directory: str | Path | None = None,
        *,
        trace: bool = False,
        source: bool = False,
        verbose: bool = False,
        parallel: bool = False,
        gpu: bool = False,
        process_count: int = 0,
        force: bool = False,
    ) -> 'Simulation':
        """Compile the instrument to a binary.

        :param directory: Directory in which to place the compiled binary and C source.
            When *None* a temporary directory is created automatically and its
            lifetime is tied to this :class:`Simulation` instance — it is cleaned
            up when the instance is garbage collected.
        :param trace: Enable trace mode in the compiled binary.
        :param source: Embed the instrument source in the binary.
        :param verbose: Verbose compiler output.
        :param parallel: Compile with MPI support.
        :param gpu: Compile with OpenACC GPU support.
        :param process_count: MPI process count (0 = system default).
        :param force: Re-compile even if the binary already exists.
        :returns: self, to allow method chaining.
        """
        from os import access, X_OK
        from mccode_antlr.run.runner import mccode_compile

        if directory is None:
            import tempfile
            self._tmpdir = tempfile.TemporaryDirectory(prefix=f'{self.instr.name}_mccode_')
            directory = Path(self._tmpdir.name)
        else:
            self._tmpdir = None
            if not isinstance(directory, Path):
                directory = Path(directory)

        binary_path = directory / self.instr.name
        if binary_path.exists() and access(binary_path, X_OK) and not force:
            self._binary = binary_path
            # Reconstruct a default target so run/scan work without knowing the original settings.
            self._target = CBinaryTarget(mpi=parallel, acc=gpu, count=process_count, nexus=False)
        else:
            target = {'mpi': parallel, 'acc': gpu, 'count': process_count, 'nexus': False}
            config = {'enable_trace': trace, 'embed_instrument_file': source, 'verbose': verbose}
            self._binary, self._target = mccode_compile(
                self.instr, binary_path, self.flavor, target=target, config=config, replace=True
            )

        self._compile_dir = directory
        return self

    def _check_compiled(self):
        if self._binary is None or self._target is None:
            raise RuntimeError(
                "Instrument has not been compiled. Call compile() before run() or scan()."
            )

    def _default_output_dir(self, suffix: str = '') -> Path:
        from datetime import datetime
        base = self._compile_dir if self._compile_dir is not None else Path('.')
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        return base / f'{self.instr.name}{timestamp}{suffix}'

    @staticmethod
    def _build_runtime_kwargs(
        ncount: int | None,
        seed: int | None,
        trace: bool,
        gravitation: bool | None,
        bufsiz: int | None,
        fmt: str | None,
    ) -> dict:
        return dict(
            ncount=ncount,
            seed=seed,
            trace=trace,
            gravitation=gravitation,
            bufsiz=bufsiz,
            format=fmt,
        )

    def run(
        self,
        parameters: dict | None = None,
        *,
        output_dir: str | Path | None = None,
        ncount: int | None = None,
        seed: int | None = None,
        trace: bool = False,
        gravitation: bool | None = None,
        bufsiz: int | None = None,
        fmt: str | None = None,
        dry_run: bool = False,
        capture: bool = True,
    ) -> tuple:
        """Run a single simulation point.

        :param parameters: Dict mapping instrument parameter names to scalar values
            (int, float, or str).  Range specifications are not accepted here; use
            :meth:`scan` for multi-point runs.  When *None* or an empty dict, all
            instrument parameters use their default values (equivalent to ``mcrun -y``).
        :param output_dir: Directory for output files.  Defaults to a timestamped
            subdirectory inside the compile directory.
        :param ncount: Number of particles to simulate.
        :param seed: RNG seed.
        :param trace: Enable trace mode at runtime.
        :param gravitation: Enable gravitation.
        :param bufsiz: Monitor buffer size.
        :param fmt: Output data format.
        :param dry_run: Print the command without executing it.
        :param capture: Capture subprocess output.
        :returns: ``(result, dats)`` where *result* is the subprocess result and
            *dats* is a dict mapping monitor stem names to loaded data objects.
        :raises ValueError: If any parameter value resolves to more than one point.
        :raises RuntimeError: If :meth:`compile` has not been called first.
        """
        from mccode_antlr.run.runner import mccode_run_compiled, mccode_runtime_parameters, regular_mccode_runtime_dict
        from mccode_antlr.run.range import _make_scanned_parameter, Singular

        self._check_compiled()

        if parameters is None:
            parameters = {}

        # Pass --yes when no explicit parameter values are given so the binary
        # uses its compiled-in defaults rather than entering interactive mode.
        use_defaults = not parameters

        concrete: dict[str, object] = {}
        for k, v in parameters.items():
            if isinstance(v, str) and any(c in v for c in (':', ',')):
                parsed = _make_scanned_parameter(v)
                if len(parsed) != 1:
                    raise ValueError(
                        f"Parameter '{k}={v}' resolves to {len(parsed)} points. "
                        "Use scan() for multi-point runs."
                    )
                concrete[k] = parsed[0]
            else:
                concrete[k] = v

        if output_dir is None:
            output_dir = self._default_output_dir()
        output_dir = Path(output_dir)
        output_dir.parent.mkdir(parents=True, exist_ok=True)

        runtime_kwargs = self._build_runtime_kwargs(ncount, seed, trace, gravitation, bufsiz, fmt)
        args = regular_mccode_runtime_dict(runtime_kwargs)
        pars = mccode_runtime_parameters(args, concrete)
        return mccode_run_compiled(self._binary, self._target, output_dir, pars, capture=capture, dry_run=dry_run, use_defaults=use_defaults)

    def scan(
        self,
        parameters: dict | None = None,
        *,
        output_dir: str | Path | None = None,
        grid: bool = False,
        ncount: int | None = None,
        seed: int | None = None,
        trace: bool = False,
        gravitation: bool | None = None,
        bufsiz: int | None = None,
        fmt: str | None = None,
        dry_run: bool = False,
        capture: bool = True,
    ) -> list:
        """Run a parameter scan.

        :param parameters: Dict mapping instrument parameter names to range specifications.
            Accepted value types:

            * **str** — MATLAB-style range ``'start:step:stop'``, explicit list
              ``'v1,v2,v3'``, or single value ``'1.5'``.
            * **list / tuple** — explicit sequence of values.
            * :class:`~mccode_antlr.run.range.MRange`, :class:`~mccode_antlr.run.range.EList`,
              :class:`~mccode_antlr.run.range.Singular` — pre-constructed range objects.
            * **scalar** (int / float) — held constant across the scan.

            When *None* or an empty dict, the instrument is run once with all default
            parameter values (equivalent to ``mcrun -y``).
        :param output_dir: Root directory for scan output.  Each scan point is written to a
            numbered subdirectory (``0/``, ``1/``, …).  Defaults to a timestamped directory
            inside the compile directory.
        :param grid: When *True*, run the Cartesian product of all parameter ranges; when
            *False* (default) zip the ranges together.
        :param ncount: Number of particles per simulation point.
        :param seed: RNG seed.
        :param trace: Enable trace mode at runtime.
        :param gravitation: Enable gravitation.
        :param bufsiz: Monitor buffer size.
        :param fmt: Output data format.
        :param dry_run: Print commands without executing.
        :param capture: Capture subprocess output.
        :returns: List of ``(result, dats)`` tuples, one per scan point.
        :raises RuntimeError: If :meth:`compile` has not been called first.
        """
        from mccode_antlr.run.runner import mccode_run_scan

        self._check_compiled()

        if parameters is None:
            parameters = {}

        # Pass --yes when no parameters are given so the binary uses its
        # compiled-in defaults rather than entering interactive mode.
        use_defaults = not parameters

        normalized = self._normalize_scan_parameters(parameters)

        if output_dir is None:
            output_dir = self._default_output_dir()
        output_dir = Path(output_dir)

        runtime_kwargs = self._build_runtime_kwargs(ncount, seed, trace, gravitation, bufsiz, fmt)
        return mccode_run_scan(
            self.instr.name,
            self._binary,
            self._target,
            normalized,
            output_dir,
            grid,
            capture=capture,
            dry_run=dry_run,
            use_defaults=use_defaults,
            **runtime_kwargs,
        )

    @staticmethod
    def _normalize_scan_parameters(parameters: dict) -> dict:
        """Convert heterogeneous parameter values to range objects understood by the runner."""
        from mccode_antlr.run.range import MRange, EList, Singular, _make_scanned_parameter, has_len

        normalized: dict = {}
        for k, v in parameters.items():
            if isinstance(v, (MRange, EList, Singular)):
                normalized[k] = v
            elif isinstance(v, str):
                normalized[k] = _make_scanned_parameter(v)
            elif isinstance(v, (list, tuple)):
                normalized[k] = EList(list(v))
            else:
                normalized[k] = Singular(v)

        # Set maximum on unbounded Singular objects so zip terminates.
        max_length = max(
            (len(v) for v in normalized.values() if has_len(v) and not isinstance(v, Singular)),
            default=1,
        )
        for k, v in normalized.items():
            if isinstance(v, Singular) and v.maximum is None:
                normalized[k] = Singular(v.value, max_length)

        return normalized

__init__(instr, flavor)

Source code in src/mccode_antlr/run/simulation.py
def __init__(self, instr: Instr, flavor: Flavor):
    self.instr = instr
    self.flavor = flavor
    self._binary: Path | None = None
    self._target: CBinaryTarget | None = None
    self._compile_dir: Path | None = None
    self._tmpdir = None  # TemporaryDirectory when compile() owns the dir

compile(directory=None, *, trace=False, source=False, verbose=False, parallel=False, gpu=False, process_count=0, force=False)

Compile the instrument to a binary.

:param directory: Directory in which to place the compiled binary and C source. When None a temporary directory is created automatically and its lifetime is tied to this :class:Simulation instance — it is cleaned up when the instance is garbage collected. :param trace: Enable trace mode in the compiled binary. :param source: Embed the instrument source in the binary. :param verbose: Verbose compiler output. :param parallel: Compile with MPI support. :param gpu: Compile with OpenACC GPU support. :param process_count: MPI process count (0 = system default). :param force: Re-compile even if the binary already exists. :returns: self, to allow method chaining.

Source code in src/mccode_antlr/run/simulation.py
def compile(
    self,
    directory: str | Path | None = None,
    *,
    trace: bool = False,
    source: bool = False,
    verbose: bool = False,
    parallel: bool = False,
    gpu: bool = False,
    process_count: int = 0,
    force: bool = False,
) -> 'Simulation':
    """Compile the instrument to a binary.

    :param directory: Directory in which to place the compiled binary and C source.
        When *None* a temporary directory is created automatically and its
        lifetime is tied to this :class:`Simulation` instance — it is cleaned
        up when the instance is garbage collected.
    :param trace: Enable trace mode in the compiled binary.
    :param source: Embed the instrument source in the binary.
    :param verbose: Verbose compiler output.
    :param parallel: Compile with MPI support.
    :param gpu: Compile with OpenACC GPU support.
    :param process_count: MPI process count (0 = system default).
    :param force: Re-compile even if the binary already exists.
    :returns: self, to allow method chaining.
    """
    from os import access, X_OK
    from mccode_antlr.run.runner import mccode_compile

    if directory is None:
        import tempfile
        self._tmpdir = tempfile.TemporaryDirectory(prefix=f'{self.instr.name}_mccode_')
        directory = Path(self._tmpdir.name)
    else:
        self._tmpdir = None
        if not isinstance(directory, Path):
            directory = Path(directory)

    binary_path = directory / self.instr.name
    if binary_path.exists() and access(binary_path, X_OK) and not force:
        self._binary = binary_path
        # Reconstruct a default target so run/scan work without knowing the original settings.
        self._target = CBinaryTarget(mpi=parallel, acc=gpu, count=process_count, nexus=False)
    else:
        target = {'mpi': parallel, 'acc': gpu, 'count': process_count, 'nexus': False}
        config = {'enable_trace': trace, 'embed_instrument_file': source, 'verbose': verbose}
        self._binary, self._target = mccode_compile(
            self.instr, binary_path, self.flavor, target=target, config=config, replace=True
        )

    self._compile_dir = directory
    return self

run(parameters=None, *, output_dir=None, ncount=None, seed=None, trace=False, gravitation=None, bufsiz=None, fmt=None, dry_run=False, capture=True)

Run a single simulation point.

:param parameters: Dict mapping instrument parameter names to scalar values (int, float, or str). Range specifications are not accepted here; use :meth:scan for multi-point runs. When None or an empty dict, all instrument parameters use their default values (equivalent to mcrun -y). :param output_dir: Directory for output files. Defaults to a timestamped subdirectory inside the compile directory. :param ncount: Number of particles to simulate. :param seed: RNG seed. :param trace: Enable trace mode at runtime. :param gravitation: Enable gravitation. :param bufsiz: Monitor buffer size. :param fmt: Output data format. :param dry_run: Print the command without executing it. :param capture: Capture subprocess output. :returns: (result, dats) where result is the subprocess result and dats is a dict mapping monitor stem names to loaded data objects. :raises ValueError: If any parameter value resolves to more than one point. :raises RuntimeError: If :meth:compile has not been called first.

Source code in src/mccode_antlr/run/simulation.py
def run(
    self,
    parameters: dict | None = None,
    *,
    output_dir: str | Path | None = None,
    ncount: int | None = None,
    seed: int | None = None,
    trace: bool = False,
    gravitation: bool | None = None,
    bufsiz: int | None = None,
    fmt: str | None = None,
    dry_run: bool = False,
    capture: bool = True,
) -> tuple:
    """Run a single simulation point.

    :param parameters: Dict mapping instrument parameter names to scalar values
        (int, float, or str).  Range specifications are not accepted here; use
        :meth:`scan` for multi-point runs.  When *None* or an empty dict, all
        instrument parameters use their default values (equivalent to ``mcrun -y``).
    :param output_dir: Directory for output files.  Defaults to a timestamped
        subdirectory inside the compile directory.
    :param ncount: Number of particles to simulate.
    :param seed: RNG seed.
    :param trace: Enable trace mode at runtime.
    :param gravitation: Enable gravitation.
    :param bufsiz: Monitor buffer size.
    :param fmt: Output data format.
    :param dry_run: Print the command without executing it.
    :param capture: Capture subprocess output.
    :returns: ``(result, dats)`` where *result* is the subprocess result and
        *dats* is a dict mapping monitor stem names to loaded data objects.
    :raises ValueError: If any parameter value resolves to more than one point.
    :raises RuntimeError: If :meth:`compile` has not been called first.
    """
    from mccode_antlr.run.runner import mccode_run_compiled, mccode_runtime_parameters, regular_mccode_runtime_dict
    from mccode_antlr.run.range import _make_scanned_parameter, Singular

    self._check_compiled()

    if parameters is None:
        parameters = {}

    # Pass --yes when no explicit parameter values are given so the binary
    # uses its compiled-in defaults rather than entering interactive mode.
    use_defaults = not parameters

    concrete: dict[str, object] = {}
    for k, v in parameters.items():
        if isinstance(v, str) and any(c in v for c in (':', ',')):
            parsed = _make_scanned_parameter(v)
            if len(parsed) != 1:
                raise ValueError(
                    f"Parameter '{k}={v}' resolves to {len(parsed)} points. "
                    "Use scan() for multi-point runs."
                )
            concrete[k] = parsed[0]
        else:
            concrete[k] = v

    if output_dir is None:
        output_dir = self._default_output_dir()
    output_dir = Path(output_dir)
    output_dir.parent.mkdir(parents=True, exist_ok=True)

    runtime_kwargs = self._build_runtime_kwargs(ncount, seed, trace, gravitation, bufsiz, fmt)
    args = regular_mccode_runtime_dict(runtime_kwargs)
    pars = mccode_runtime_parameters(args, concrete)
    return mccode_run_compiled(self._binary, self._target, output_dir, pars, capture=capture, dry_run=dry_run, use_defaults=use_defaults)

scan(parameters=None, *, output_dir=None, grid=False, ncount=None, seed=None, trace=False, gravitation=None, bufsiz=None, fmt=None, dry_run=False, capture=True)

Run a parameter scan.

:param parameters: Dict mapping instrument parameter names to range specifications. Accepted value types:

* **str** — MATLAB-style range ``'start:step:stop'``, explicit list
  ``'v1,v2,v3'``, or single value ``'1.5'``.
* **list / tuple** — explicit sequence of values.
* :class:`~mccode_antlr.run.range.MRange`, :class:`~mccode_antlr.run.range.EList`,
  :class:`~mccode_antlr.run.range.Singular` — pre-constructed range objects.
* **scalar** (int / float) — held constant across the scan.

When *None* or an empty dict, the instrument is run once with all default
parameter values (equivalent to ``mcrun -y``).

:param output_dir: Root directory for scan output. Each scan point is written to a numbered subdirectory (0/, 1/, …). Defaults to a timestamped directory inside the compile directory. :param grid: When True, run the Cartesian product of all parameter ranges; when False (default) zip the ranges together. :param ncount: Number of particles per simulation point. :param seed: RNG seed. :param trace: Enable trace mode at runtime. :param gravitation: Enable gravitation. :param bufsiz: Monitor buffer size. :param fmt: Output data format. :param dry_run: Print commands without executing. :param capture: Capture subprocess output. :returns: List of (result, dats) tuples, one per scan point. :raises RuntimeError: If :meth:compile has not been called first.

Source code in src/mccode_antlr/run/simulation.py
def scan(
    self,
    parameters: dict | None = None,
    *,
    output_dir: str | Path | None = None,
    grid: bool = False,
    ncount: int | None = None,
    seed: int | None = None,
    trace: bool = False,
    gravitation: bool | None = None,
    bufsiz: int | None = None,
    fmt: str | None = None,
    dry_run: bool = False,
    capture: bool = True,
) -> list:
    """Run a parameter scan.

    :param parameters: Dict mapping instrument parameter names to range specifications.
        Accepted value types:

        * **str** — MATLAB-style range ``'start:step:stop'``, explicit list
          ``'v1,v2,v3'``, or single value ``'1.5'``.
        * **list / tuple** — explicit sequence of values.
        * :class:`~mccode_antlr.run.range.MRange`, :class:`~mccode_antlr.run.range.EList`,
          :class:`~mccode_antlr.run.range.Singular` — pre-constructed range objects.
        * **scalar** (int / float) — held constant across the scan.

        When *None* or an empty dict, the instrument is run once with all default
        parameter values (equivalent to ``mcrun -y``).
    :param output_dir: Root directory for scan output.  Each scan point is written to a
        numbered subdirectory (``0/``, ``1/``, …).  Defaults to a timestamped directory
        inside the compile directory.
    :param grid: When *True*, run the Cartesian product of all parameter ranges; when
        *False* (default) zip the ranges together.
    :param ncount: Number of particles per simulation point.
    :param seed: RNG seed.
    :param trace: Enable trace mode at runtime.
    :param gravitation: Enable gravitation.
    :param bufsiz: Monitor buffer size.
    :param fmt: Output data format.
    :param dry_run: Print commands without executing.
    :param capture: Capture subprocess output.
    :returns: List of ``(result, dats)`` tuples, one per scan point.
    :raises RuntimeError: If :meth:`compile` has not been called first.
    """
    from mccode_antlr.run.runner import mccode_run_scan

    self._check_compiled()

    if parameters is None:
        parameters = {}

    # Pass --yes when no parameters are given so the binary uses its
    # compiled-in defaults rather than entering interactive mode.
    use_defaults = not parameters

    normalized = self._normalize_scan_parameters(parameters)

    if output_dir is None:
        output_dir = self._default_output_dir()
    output_dir = Path(output_dir)

    runtime_kwargs = self._build_runtime_kwargs(ncount, seed, trace, gravitation, bufsiz, fmt)
    return mccode_run_scan(
        self.instr.name,
        self._binary,
        self._target,
        normalized,
        output_dir,
        grid,
        capture=capture,
        dry_run=dry_run,
        use_defaults=use_defaults,
        **runtime_kwargs,
    )

SimulationOutput

mccode_antlr.run.output.SimulationOutput

Bases: Mapping

All files written to disk by a single simulation run.

:class:SimulationOutput implements :class:~collections.abc.Mapping so that existing code written against the old dict[str, DatFile] return value continues to work without modification::

result, out = sim.run({'x': 1.5}, ncount=1000)
# Old-style access still works:
print(out['m0']['I'])
print(len(out))

Additional properties expose the full picture of what was written:

  • :attr:dats — McCode-format files (any extension)
  • :attr:other — Files loaded by registered custom filters
  • :attr:loaded — Union of dats and other keyed by stem
  • :attr:unrecognized — Files that could not be loaded
  • :attr:sim_file — Parsed mccode.sim metadata (or None)
  • :attr:directory — Output directory :class:~pathlib.Path
Source code in src/mccode_antlr/run/output.py
class SimulationOutput(Mapping):
    """All files written to disk by a single simulation run.

    :class:`SimulationOutput` implements :class:`~collections.abc.Mapping` so
    that existing code written against the old ``dict[str, DatFile]`` return
    value continues to work without modification::

        result, out = sim.run({'x': 1.5}, ncount=1000)
        # Old-style access still works:
        print(out['m0']['I'])
        print(len(out))

    Additional properties expose the full picture of what was written:

    * :attr:`dats`         — McCode-format files (any extension)
    * :attr:`other`        — Files loaded by registered custom filters
    * :attr:`loaded`       — Union of *dats* and *other* keyed by stem
    * :attr:`unrecognized` — Files that could not be loaded
    * :attr:`sim_file`     — Parsed ``mccode.sim`` metadata (or ``None``)
    * :attr:`directory`    — Output directory :class:`~pathlib.Path`
    """

    def __init__(
        self,
        directory: Path,
        dats: dict[str, Any],
        other: dict[str, Any],
        unrecognized: list[Path],
        sim_file: Any | None,
    ):
        self._dats = dats
        self._other = other
        self._unrecognized = list(unrecognized)
        self._sim_file = sim_file
        self.directory = directory

    # ------------------------------------------------------------------
    # Mapping interface (proxies to dats for backward compatibility)
    # ------------------------------------------------------------------

    def __getitem__(self, key: str) -> Any:
        return self._dats[key]

    def __len__(self) -> int:
        return len(self._dats)

    def __iter__(self) -> Iterator[str]:
        return iter(self._dats)

    # ------------------------------------------------------------------
    # Additional properties
    # ------------------------------------------------------------------

    @property
    def dats(self) -> dict[str, Any]:
        """Files loaded as McCode dat format, keyed by file stem."""
        return dict(self._dats)

    @property
    def other(self) -> dict[str, Any]:
        """Files loaded by registered custom filters, keyed by file stem."""
        return dict(self._other)

    @property
    def loaded(self) -> dict[str, Any]:
        """All successfully loaded files (dats + other), keyed by stem."""
        return {**self._dats, **self._other}

    @property
    def unrecognized(self) -> list[Path]:
        """Paths of files that could not be loaded by any filter."""
        return list(self._unrecognized)

    @property
    def sim_file(self) -> Any | None:
        """Parsed McCode ``.sim`` metadata file, or ``None`` if not found."""
        return self._sim_file

    def __repr__(self) -> str:
        parts = [f"directory={self.directory!r}", f"dats={list(self._dats)!r}"]
        if self._other:
            parts.append(f"other={list(self._other)!r}")
        if self._unrecognized:
            parts.append(f"unrecognized={[p.name for p in self._unrecognized]!r}")
        return f"SimulationOutput({', '.join(parts)})"

dats property

Files loaded as McCode dat format, keyed by file stem.

other property

Files loaded by registered custom filters, keyed by file stem.

loaded property

All successfully loaded files (dats + other), keyed by stem.

unrecognized property

Paths of files that could not be loaded by any filter.

sim_file property

Parsed McCode .sim metadata file, or None if not found.

directory = directory instance-attribute


register_output_filter

mccode_antlr.run.output.register_output_filter(extension, loader)

Register a custom output-file loader for files with the given extension.

The extension must include the leading dot (e.g. '.h5'). The loader receives the :class:~pathlib.Path of the file and must return the loaded object, raising any exception if loading fails (the file will then appear in :attr:SimulationOutput.unrecognized).

Registering a loader for an extension that already has one replaces the existing entry. This allows user code to override the built-in '.dat' loader if needed.

Example::

import h5py
from mccode_antlr.run import register_output_filter
register_output_filter('.h5', h5py.File)
Source code in src/mccode_antlr/run/output.py
def register_output_filter(extension: str, loader: Callable[[Path], Any]) -> None:
    """Register a custom output-file loader for files with the given *extension*.

    The *extension* must include the leading dot (e.g. ``'.h5'``).  The *loader*
    receives the :class:`~pathlib.Path` of the file and must return the loaded
    object, raising any exception if loading fails (the file will then appear in
    :attr:`SimulationOutput.unrecognized`).

    Registering a loader for an extension that already has one replaces the
    existing entry.  This allows user code to override the built-in ``'.dat'``
    loader if needed.

    Example::

        import h5py
        from mccode_antlr.run import register_output_filter
        register_output_filter('.h5', h5py.File)
    """
    _LOAD_FILTERS[extension.lower()] = loader