# Architecture Rules These rules apply to new and changed code. Reviewers refer to them. ## Layers and imports The package is organized in layers. A module may import only from its own or a lower layer. Library code never imports `simtools.applications`, and production code never imports test helpers. Import-linter enforces the package layers, and Ruff checks private cross-module imports. The `ignore_imports` list of `[tool.importlinter]` in `pyproject.toml` is a ratchet: reduce it as the architecture cleanup proceeds and do not add entries. ```text 5 applications/ thin CLI wrappers 4 workflows simulate, production, science_tests, reporting, derive_* 3 simulation engines corsika, simtel, ray_tracing, camera, job_execution 2 model model (site/telescope/array), model_repository, layout 1 data io, data_model (schemas, metadata), sim_events readers 0 foundation utils, constants, names, units visualization may import 0-2 only; receives data, never computes it ``` Plotting modules receive arrays, tables or dataclasses and draw them. Rebinning, statistics and comparison metrics belong in the module that computes them, so they can be tested without matplotlib. ## Adding is data, not code Prefer data over code for anything that is expected to grow: - A new model parameter is a schema in `schemas/model_parameters/` plus model data. Simulator-specific names and formats belong in schema fields (`simulation_software`, `internal_parameter_name`). Add code only for a new output format. - A shared file product (suffix, default name) is an entry in `resources/file_types.yml`. - A simple line or histogram plot is a `plot_tables` configuration. ## Extension points Each axis where simtools grows has one extension point: | Axis | Extension point | | --- | --- | | Model parameter | schema YAML in `schemas/model_parameters/` | | Simulation configuration writer and result reader | see [Simulation formats](simulation_formats.md) | | Ray-tracing backend | `RayTracingBackend` Protocol in `ray_tracing/backends/interface.py`; see [Add a ray-tracing backend](extending/ray-tracing-backend.md) | | Event-file format | `EventReader` Protocol in `sim_events/formats/readers.py` | | Model-repository storage | `ModelStorage` Protocol in `model_repository/storage.py` | | Execution backend | `job_execution/backends/registry.py` | | Plot | `plot_tables` and `plot_configuration.metaschema.yml` | | Application | `ApplicationDefinition` plus the `[project.scripts]` entry | New extension points use a small `typing.Protocol` and a module-level dict mapping names to implementations (with lazy imports for optional dependencies). Do not add `register_*` functions, factories or plug-in machinery outside these axes. Engine-specific vocabulary (CORSIKA keywords, sim_telarray option names) stays inside the engine's package; other code uses physics terms. ### Adding a simple plot Use `plot_tables.plot` to render a line or histogram plot from library code. The configuration follows `plot_configuration.metaschema.yml`; table data can come from an ECSV file or model parameters. For example, with `response.ecsv` in the data directory: ```python from pathlib import Path from simtools.visualization import plot_tables config = { "type": "example_response", "title": "Detector response", "xtitle": "Wavelength [nm]", "ytitle": "Response", "tables": [ { "file_name": "response.ecsv", "label": "Response", "column_x": "wavelength", "column_y": "efficiency", } ], } plot_tables.plot(config, Path("response.png"), data_path=Path("data")) ``` The same configuration can be stored as `plot_configuration` in a model parameter schema for plots generated by simtools applications. ## Public API A name is public when it is documented in the API reference and does not start with an underscore. A leading underscore means private to its module: do not import it from another module; make it public or move it instead. Use `__all__` only in package `__init__.py` files that re-export names; do not define per-module `__all__` lists. Before v1.0, API changes do not keep compatibility aliases. Remove or rename names directly and list each change in the changelog. ## Test policy - Unit tests do not use checked-in or downloaded test resources. Generate inputs in `tmp_test_directory` from the packaged schemas; reduced-event and trigger-histogram tables come from `tests/unit_tests/builders/tables.py`, and in-memory model repositories from `MemoryStorage`. - Mock only true external boundaries: external programs (CORSIKA, sim_telarray, container engines, HTCondor), network access, model sources (through the shared model-reader fixtures in `tests/unit_tests/conftest.py`), and time, UUID or randomness when determinism is needed. Use `monkeypatch.setenv` for environment variables. - Do not add patches of other `simtools.*` internals. Test public behavior; shape code as plain functions on arrays, tables or dataclasses so that it needs no mocks. - File-format compatibility with real CORSIKA and sim_telarray output belongs in integration tests.