Skip to content
Draft
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
33 changes: 33 additions & 0 deletions .github/workflows/simulation-core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CHORAS simulation core CI

on:
push:
branches:
- '**'

jobs:
uv-test:
name: Pytest for the simulation core
runs-on: ubuntu-latest
continue-on-error: true

strategy:
matrix:
python-version:
- "3.11"
- "3.12"
- "3.13"
- "3.14"

steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}

- name: Run test for pyroomacoustics method
run: |
cd choras-simulation-core
uv run --extra tests pytest tests/
61 changes: 61 additions & 0 deletions choras-simulation-core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
*.manifest
*.spec

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db
18 changes: 18 additions & 0 deletions choras-simulation-core/choras_simulation_core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""CHORAS Simulation Core Package.

This package provides shared base classes and utilities for all CHORAS
simulation methods. It defines the common interface that all simulation
methods must implement and provides structured exception types for
meaningful error reporting.

"""

from choras_simulation_core.base import SimulationMethod
from choras_simulation_core import exceptions

__version__ = "0.1.0"

__all__ = [
"SimulationMethod",
"exceptions",
]
141 changes: 141 additions & 0 deletions choras-simulation-core/choras_simulation_core/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Base class for CHORAS simulation methods.

This module provides the abstract base class that all simulation methods
must inherit from. It handles common functionality like JSON file validation
and result transmission back to the backend.
"""

import time
from abc import ABC, abstractmethod
from pathlib import Path

import requests


class SimulationMethod(ABC):
"""Abstract base class for simulation methods.

This class serves as a template for methods required to run a simulation
and return results to the simulation service executor.

Parameters
----------
input_json_path : str | Path | None
The path to the input JSON file containing simulation configuration.

Raises
------
FileNotFoundError
If the input JSON file does not exist or is None/empty.

Examples
--------
>>> class MySimulationMethod(SimulationMethod):
... def run_simulation(self):
... # Implementation here
... pass
>>> method = MySimulationMethod("/path/to/config.json")
>>> method.run_simulation()
>>> method.save_results()

"""

def __init__(self, input_json_path: str | Path | None):
"""Initialize the simulation method.

Parameters
----------
input_json_path : str | Path | None
The path to the input JSON file. Cannot be None or empty.

Raises
------
FileNotFoundError
If the input JSON file does not exist or path is None/empty.

"""
if input_json_path is None or (
isinstance(input_json_path, str) and input_json_path == ""
):
raise FileNotFoundError("input_json_path cannot be None or empty")

input_path = Path(input_json_path)
if not input_path.exists():
raise FileNotFoundError(
f"Input JSON file not found: {input_json_path}"
)

self._input_json_path = input_json_path

@property
def input_json_path(self) -> str | Path:
"""Get the input JSON file path.

Returns
-------
str | Path
The path to the input JSON configuration file.

"""
return self._input_json_path

@abstractmethod
def run_simulation(self):
"""Run the simulation for the given JSON configuration file.

This method must be implemented by all subclasses to perform
the actual simulation computation.

"""
pass

def save_results(
self,
url="http://host.docker.internal:5001/receive",
max_retries=5,
delay=2,
):
"""Return the results back to the simulation service executor.

This method sends the simulation results stored in the input JSON file
back to the backend service via HTTP POST request.

Parameters
----------
url : str, optional
The URL of the results server. Default is
"http://host.docker.internal:5001/receive" which is the
standard address for local execution via Docker.
max_retries : int, optional
The maximum number of retries if the request fails.
Default is 5.
delay : int, optional
The delay in seconds between retries. Default is 2.

Returns
-------
bool
True if results were successfully sent, False otherwise.

"""
json_tmp_file = self.input_json_path
for attempt in range(1, max_retries + 1):
try:
with open(json_tmp_file, "rb") as f:
response = requests.post(url, files={"file": f})

if response.status_code == 200:
print("Successfully sent file.")
return True

print(
f"Attempt {attempt}: ",
f"Server returned {response.status_code}",
)
except requests.RequestException as exc:
print(f"Attempt {attempt}: Request failed - {exc}")

time.sleep(delay)

print("Max retries reached. Giving up.")
return False
108 changes: 108 additions & 0 deletions choras-simulation-core/choras_simulation_core/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Exception hierarchy for CHORAS simulation methods.

This module defines a structured exception hierarchy that simulation methods
can use to provide meaningful, user-friendly error messages that will be
propagated to the frontend.
"""


class SimulationError(Exception):
"""Base exception for all simulation-related errors.

This is the base class for all custom exceptions raised by simulation
methods. It ensures that errors can be caught generically while still
maintaining specific error types.

Parameters
----------
message : str
A user-friendly description of what went wrong.

"""

def __init__(self, message: str):
"""Initialize the simulation error.

Parameters
----------
message : str
A user-friendly error message.

"""
self.message = message
super().__init__(self.message)


class ConfigurationError(SimulationError):
"""Exception raised for configuration-related errors.

Use this exception when the simulation fails due to invalid configuration,
missing parameters, invalid JSON structure, or incorrect settings.

Examples
--------
>>> raise ConfigurationError(
... f"Absorption coefficient format for {material_id} incorrect - "
... "Check material assignments"
... )

"""

pass


class GeometryError(SimulationError):
"""Exception raised for geometry-related errors.

Use this exception when the simulation fails due to geometry issues
such as missing mesh files, invalid geometry, malformed meshes, or
geometry processing errors.

Examples
--------
>>> raise GeometryError(
... "The provided geometry is invalid - "
... "Please verify the geometry file was uploaded correctly"
... )

"""

pass


class ComputationError(SimulationError):
"""Exception raised for computation/solver errors.

Use this exception when the simulation fails during the actual
computation phase, such as solver divergence, numerical instability,
or algorithm-specific failures.

Examples
--------
>>> raise ComputationError(
... "Solver diverged after 1000 iterations - "
... "Try reducing time step or increasing damping"
... )

"""

pass


class ResourceError(SimulationError):
"""Exception raised for resource-related errors.

Use this exception when the simulation fails due to insufficient
resources such as memory, disk space, file access permissions, or
other system resource issues.

Examples
--------
>>> raise ResourceError(
... "Insufficient memory to allocate resources "
... "Try reducing mesh density or use cloud resources"
... )

"""

pass
Loading