The Simulation Method Interface#
Each simulation method interface in CHORAS is implemented in the format of a Python package which also specifies a command line interface (CLI) for running the method.
The CHORAS backend shares information and data with the simulation methods via JSON files. When a simulation is scheduled, the backend writes a JSON file with all required configurations and input data. The simulation methodβs interface executes the simulation based on the provided configuration and extends the same JSON file with progress updates and finally the results, which are then read by the backend.
Creating the Scaffolding#
To simplify the process of adding a new method, we provide a template using the copier tool.
Copier allows you to generate a customized template based on input you provide.
You can install copier with pip:
pip install copier
Or with uv:
uv tool install copier
From the simulation-backend directory, run:
copier copy https://github.com/choras-org/template_simulation_method ./ --trust
Note that the --trust flag is required. No code will be executed except for printing additional instructions.
Warning
Make sure to run the above command from the simulation-backend directory or that you point the output path correctly, otherwise the generated files will be created in the wrong location.
Questions for Customization#
Copier will ask you several questions to customize your interface. These questions cover information about the author, the implemented method, and dependencies.
Note
The template always includes *.msh and *.geo files (named
test_room_<method_name_lower>.*) and gmsh as a dependency.
Simply remove the .msh file if your method automatically generates
mesh files based on the geometry data defined in the *.geo file.
About gmsh initialization: If your method requires gmsh.initialize() and
gmsh.finalize(), add them in your simulation method implementation (not in the CLI).
This keeps the CLI simple and allows better control over gmsh lifecycle.
Example#
$ copier copy https://github.com/choras-org/template_simulation_method ./
π€ Author name
Your Name
π€ Author email
your.email@example.com
π€ What is the name of your simulation method (e.g., "DG", "DE", "Pyroomacoustics")?
MyMethod
π€ What is the lowercase (snake_case) version of your method name (used for package naming)?
mymethod
π€ Brief description of your simulation method
Python package for acoustic simulation using MyMethod
...
This will create a directory structure like:
mymethod_method/
βββ Dockerfile
βββ pyproject.toml
βββ mymethod_interface/
β βββ __init__.py
β βββ __main__.py
β βββ __cli__.py
β βββ definition.py
β βββ mymethod_interface.py
βββ tests/
βββ conftest.py
βββ test_definition.py
βββ test_mymethod_cli.py
βββ test_fixtures.py
βββ test_input_mymethod.json
βββ test_room_mymethod.geo
Next Steps After Generation#
Navigate to your new package:
cd <method_name_lower>_method
Implement your simulation logic: Edit
<method_name_lower>_interface/<method_name_lower>_interface.pyand implement the_<method_name_lower>_method()function.Update dependencies: Add any specific dependencies your method needs to
pyproject.toml.Update test data: Modify the test JSON file in
tests/test_input_<test_room_name>.jsonto match your methodβs expected input structure.Add specific tests: Update the CLI test in
tests/test_<method_name_lower>_cli.pywith assertions specific to your method.Install and test:
pip install -e . pytest tests/
Build Docker image:
docker build -t <method_name_lower>_method . --platform linux/amd64
Dependencies#
Include your methodβs dependencies with explicit version numbers.
Prefer libraries that are installable via pip
If your package is only hosted in a git repository, you can install it using:
"package_name @ git+https://gitprovider.com/user/project.git@{version}"If your package does not have a version number, you can use the commit hash:
"package_name @ git+https://gitprovider.com/user/project.git@{commit_hash}"
Refer to the Python Packaging Authority guide for more options.
Note that providing a specific version number (or hash) is important to ensure reproducibility (and stability) of the results and CHORAS as a platform. If your method is not yet installable via pip, please refer to packaging guidelines provided by the Python Packaging Authority.
Structure Explanation#
definition.py: Contains theSimulationMethodabstract base class.Specifies the required methods for any simulation method implementation.
Checks for None/empty paths and file existence.
Provides a standardized way to export results to the backend and database.
<method>_interface.py: Your main implementation fileImplements the simulation logic into the standardized interface defined by
SimulationMethod.
__init__.py: Exports the main class and CLI entry point__main__.py: Allows running as a module (python -m <package>)__cli__.py: CLI implementation that readsJSON_PATHenv variabletests/: Contains tests for your methodtest_definition.py: Tests the base classtest_<method>_cli.py: Tests the CLI with mockstest_fixtures.py: Tests fixture functionalityconftest.py: Shared fixtures for all tests
Customization Tips#
Mesh Files#
The template includes .msh and .geo files by default. To remove them:
Delete
tests/test_room_*.mshandtests/test_room_*.geoRemove
gmshfrompyproject.tomldependenciesRemove
gmshfromDockerfileUpdate
tests/conftest.pyto not copy these filesUpdate
tests/test_input_*.jsonto removegeo_pathandmsh_path
Gmsh Initialization#
Gmsh requires explicit initialization and finalization.
Try to keep gmsh-related code as compact as possible and make sure that gmsh.finalize() is always called, even if errors occur.
This can for example be achieved by wrapping corresponding code in a try-finally block:
def run_simulation(self) -> None:
"""Run the simulation."""
import gmsh
gmsh.initialize()
try:
self._mymethod_method(self.input_json_path)
finally:
gmsh.finalize()
This approach:
Allows better control over gmsh lifecycle
Makes it easier to test without gmsh
Adding Custom Simulation Settings#
Edit the test JSON template to include your method-specific settings in the
simulationSettings section:
"simulationSettings": {
"your_setting_1": value1,
"your_setting_2": value2
}
Common Patterns#
Reading Input Data#
with open(json_file_path, "r") as json_file:
result_container = json.load(json_file)
Extracting Source/Receiver Positions#
source_coords = [
result_container["results"][0]["sourceX"],
result_container["results"][0]["sourceY"],
result_container["results"][0]["sourceZ"],
]
receiver_coords = [
result_container["results"][0]["responses"][0]["x"],
result_container["results"][0]["responses"][0]["y"],
result_container["results"][0]["responses"][0]["z"],
]
Writing Results#
result_container["results"][0]["responses"][0]["receiverResults"] = results.tolist()
with open(json_file_path, "w") as json_output:
json_output.write(json.dumps(result_container, indent=4))