From 3278cd0e4d9fa4b25867a3d83c3788caa09641db Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 11 May 2026 13:52:31 +0200 Subject: [PATCH 01/40] Ran copier --- modart_method/Dockerfile | 22 + modart_method/LICENSE | 21 + modart_method/modart_interface/__cli__.py | 19 + modart_method/modart_interface/__init__.py | 8 + modart_method/modart_interface/__main__.py | 5 + modart_method/modart_interface/definition.py | 92 + .../modart_interface/modart_interface.py | 73 + modart_method/pyproject.toml | 88 + modart_method/tests/conftest.py | 68 + modart_method/tests/test_definition.py | 37 + modart_method/tests/test_fixtures.py | 23 + modart_method/tests/test_input_modart.json | 55 + modart_method/tests/test_modart_cli.py | 37 + modart_method/tests/test_room_modart.geo | 51 + modart_method/tests/test_room_modart.msh | 2116 +++++++++++++++++ 15 files changed, 2715 insertions(+) create mode 100644 modart_method/Dockerfile create mode 100644 modart_method/LICENSE create mode 100644 modart_method/modart_interface/__cli__.py create mode 100644 modart_method/modart_interface/__init__.py create mode 100644 modart_method/modart_interface/__main__.py create mode 100644 modart_method/modart_interface/definition.py create mode 100644 modart_method/modart_interface/modart_interface.py create mode 100644 modart_method/pyproject.toml create mode 100644 modart_method/tests/conftest.py create mode 100644 modart_method/tests/test_definition.py create mode 100644 modart_method/tests/test_fixtures.py create mode 100644 modart_method/tests/test_input_modart.json create mode 100644 modart_method/tests/test_modart_cli.py create mode 100644 modart_method/tests/test_room_modart.geo create mode 100644 modart_method/tests/test_room_modart.msh diff --git a/modart_method/Dockerfile b/modart_method/Dockerfile new file mode 100644 index 0000000..b766d96 --- /dev/null +++ b/modart_method/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11.13-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies for mesh generation and scientific computing +RUN apt-get update && apt-get install -y \ + git \ + build-essential \ + gmsh \ + && rm -rf /var/lib/apt/lists/* + +# Copy method package directory +COPY modart_method /app/modart_method + +# Install the method package +RUN pip install --no-cache-dir /app/modart_method + +WORKDIR /app/modart_method + +# Default command to run the containerized MoDART method +CMD ["python", "-m", "modart_interface"] diff --git a/modart_method/LICENSE b/modart_method/LICENSE new file mode 100644 index 0000000..a8e20f6 --- /dev/null +++ b/modart_method/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025, Matteo Scerbo + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/modart_method/modart_interface/__cli__.py b/modart_method/modart_interface/__cli__.py new file mode 100644 index 0000000..b0dcc6b --- /dev/null +++ b/modart_method/modart_interface/__cli__.py @@ -0,0 +1,19 @@ +"""CLI module for MoDART method.""" +import os +from .modart_interface import MoDARTMethod + + +def main() -> None: + """Run the MoDART method simulation.""" + # JSON path in the uploads folder. This variable is set for the + # container when it is started up. + json_file_path = os.environ.get("JSON_PATH") + + print(f"Running MoDART method with JSON_PATH={json_file_path}") + modart_method_object = MoDARTMethod(json_file_path) + modart_method_object.run_simulation() + + # Save the results to a separate file + modart_method_object.save_results() + + print("MoDART container finished.") diff --git a/modart_method/modart_interface/__init__.py b/modart_method/modart_interface/__init__.py new file mode 100644 index 0000000..6937cdd --- /dev/null +++ b/modart_method/modart_interface/__init__.py @@ -0,0 +1,8 @@ +"""MoDARTMethod package.""" +from .__main__ import main +from .modart_interface import MoDARTMethod + +__all__ = [ + "main", + "MoDARTMethod" +] diff --git a/modart_method/modart_interface/__main__.py b/modart_method/modart_interface/__main__.py new file mode 100644 index 0000000..fe0bf36 --- /dev/null +++ b/modart_method/modart_interface/__main__.py @@ -0,0 +1,5 @@ +"""Main module for MoDART method.""" +from .__cli__ import main + +if __name__ == "__main__": + main() diff --git a/modart_method/modart_interface/definition.py b/modart_method/modart_interface/definition.py new file mode 100644 index 0000000..0031729 --- /dev/null +++ b/modart_method/modart_interface/definition.py @@ -0,0 +1,92 @@ +"""Base class implementation of the SimulationMethod interface class.""" +from abc import ABC, abstractmethod +from pathlib import Path +import time + +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. + + """ + + def __init__(self, input_json_path: str | Path | None): + """Initialize the simulation method. + + Parameters + ---------- + input_json_path : str | Path | None, optional + The path to the input JSON file, by default None + + Raises + ------ + FileNotFoundError + If the input JSON file does not exist. + + """ + 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: + """The input JSON file.""" + return self._input_json_path + + @abstractmethod + def run_simulation(self): + """Run the simulation for the given a JSON file.""" + 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. + + Parameters + ---------- + url : str, optional + The URL of the results server, + by default "http://host.docker.internal:5001/receive" which + is the default address for local executrion via Docker. + max_retries : int, optional + The maximum number of retries if the request fails, by default 5 + delay : int, optional + The delay in seconds between retries, by default 2 + + """ + + 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 diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py new file mode 100644 index 0000000..af23201 --- /dev/null +++ b/modart_method/modart_interface/modart_interface.py @@ -0,0 +1,73 @@ +"""Module implementing a CHORAS interface for MoDART. +""" +import json +from pathlib import Path + +from .definition import SimulationMethod + + +class MoDARTMethod(SimulationMethod): + """Interface class to run the MoDART method. + + The class implements method to run the calculations for the + MoDART simulation method. All required configuration parameters + are expected to be provided in the input JSON file passed during + initialization. + + """ + + def __init__(self, input_json_path: str | Path | None = None): + """Initialize the MoDART method interface for the given JSON file.""" + super().__init__(input_json_path) + + def run_simulation(self) -> None: + """Run the simulation. + + Parameters + ---------- + json_file_path : str | Path | None, optional + Path to the JSON file. If not provided, uses the path from initialization. + """ + self._modart_method(self.input_json_path) + + def _modart_method(self, json_file_path: str | Path) -> None: + """ + Run MoDART simulation for acoustic wave propagation. + + Args: + json_file_path: Path to the JSON configuration file + """ + # Load the input JSON file + with open(json_file_path, "r") as json_file: + result_container = json.load(json_file) + + # TODO: Implement your simulation logic here + # 1. Extract simulation parameters from result_container + # 2. Run your simulation + # 3. Process results + # 4. Write results back to result_container + + # Example structure (modify based on your needs): + # simulation_settings = result_container["simulationSettings"] + # 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"], + # ] + + # Run your simulation here + # results = your_simulation_function(...) + + # Write results back to JSON + # result_container["results"][0]["responses"][0]["receiverResults"] = results.tolist() + + # Save the updated JSON + with open(json_file_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + print("MoDART simulation completed successfully!") diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml new file mode 100644 index 0000000..42c4e6f --- /dev/null +++ b/modart_method/pyproject.toml @@ -0,0 +1,88 @@ +[project] +name = "modart_interface" +version = "0.1.0" +description = "Python package implementing Acoustic Radiance Transfer and its Modal Decomposition." +requires-python = ">=3.11" +authors = [ + { name = "Matteo Scerbo", email = "matteo.scerbo@gmail.com" }, +] +keywords = [ + + "acoustic simulation", + + "geometrical acoustics", + + "acoustic radiance transfer", + +] + +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + + "Programming Language :: Python :: 3.11", + + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + + "Programming Language :: Python :: 3.14", + +] +dependencies = [ + + "numpy>=2.2.5", + + "matplotlib>=3.8.4", + + "scipy>=1.14.1", + + "tqdm>=4.67.1", + + "requests", + "gmsh", +] + +[project.optional-dependencies] +deploy = [ + "twine", + "wheel", + "build", + "setuptools", + "bump-my-version", +] + +tests = [ + "pytest", + "pytest-cov", + "watchdog", + "ruff", + "coverage", +] + +docs = [ + "sphinx", + "autodocsumm>=0.2.14", + "pydata-sphinx-theme", + "sphinx_mdinclude", + "sphinx-design", + "sphinx-favicon", + "sphinx-reredirects", +] + +dev = ["modart_interface[deploy,tests,docs]"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["modart_interface"] + +[project.scripts] +modart_interface = "modart_interface:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/modart_method/tests/conftest.py b/modart_method/tests/conftest.py new file mode 100644 index 0000000..0164de3 --- /dev/null +++ b/modart_method/tests/conftest.py @@ -0,0 +1,68 @@ +import json +import os +import pytest +import shutil +import tempfile +from pathlib import Path +from unittest.mock import patch, MagicMock + + +def default_data_path(): + """Get the path to the default data folder.""" + return os.path.join( + os.path.dirname(os.path.abspath(__file__))) + + +def load_default_input_data(): + """Load the example input data.""" + with open(os.path.join( + default_data_path(), + "test_input_modart.json"), 'r') as f: + data = json.load(f) + + return data + + +@pytest.fixture +def default_input_data(): + """Fixture to load the example input data.""" + return load_default_input_data() + + +@pytest.fixture +def create_temporary_input_file(): + """Fixture to create a temporary input JSON file which can be reused to + write results to.""" + input_tmp = load_default_input_data() + geo_file = os.path.join( + default_data_path(), "test_room_modart.geo") + msh_file = os.path.join( + default_data_path(), "test_room_modart.msh") + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) / "temp_input.json" + shutil.copy(geo_file, Path(tmpdirname)) + shutil.copy(msh_file, Path(tmpdirname)) + input_tmp['geo_path'] = os.path.join( + tmpdirname, "test_room_modart.geo") + input_tmp['msh_path'] = os.path.join( + tmpdirname, "test_room_modart.msh") + with open(tmp_path, 'w') as f: + json.dump(input_tmp, f) + + yield str(tmp_path) + + return str(tmp_path) + + +@pytest.fixture +def mock_requests_post(): + """Fixture to mock requests.post for CLI tests. + + Returns the mock object so tests can make assertions on it. + """ + with patch("modart_interface.definition.requests.post") as mock_post: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + yield mock_post diff --git a/modart_method/tests/test_definition.py b/modart_method/tests/test_definition.py new file mode 100644 index 0000000..d7797ec --- /dev/null +++ b/modart_method/tests/test_definition.py @@ -0,0 +1,37 @@ +"""Test the SimulationMethod base class for modart method.""" +import pytest +from unittest.mock import patch +from pathlib import Path + +from modart_interface.definition import SimulationMethod + + +@patch.multiple(SimulationMethod, __abstractmethods__=set()) +def test_simulation_method_with_valid_file(create_temporary_input_file): + """Test SimulationMethod initialization with a valid file.""" + method = SimulationMethod(create_temporary_input_file) + assert method.input_json_path == create_temporary_input_file + + +@pytest.mark.parametrize("empty_path", [None, ""]) +@patch.multiple(SimulationMethod, __abstractmethods__=set()) +def test_simulation_method_with_none_path(empty_path): + """Test SimulationMethod initialization with None path.""" + with pytest.raises(FileNotFoundError, match="input_json_path cannot be None or empty"): + SimulationMethod(empty_path) + + +@patch.multiple(SimulationMethod, __abstractmethods__=set()) +def test_simulation_method_with_nonexistent_file(): + """Test SimulationMethod initialization with a non-existent file.""" + nonexistent_path = "/tmp/nonexistent_file_that_does_not_exist.json" + with pytest.raises(FileNotFoundError, match="Input JSON file not found"): + SimulationMethod(nonexistent_path) + + +@patch.multiple(SimulationMethod, __abstractmethods__=set()) +def test_simulation_method_with_path_object(create_temporary_input_file): + """Test SimulationMethod initialization with a Path object.""" + path_obj = Path(create_temporary_input_file) + method = SimulationMethod(path_obj) + assert method.input_json_path == path_obj diff --git a/modart_method/tests/test_fixtures.py b/modart_method/tests/test_fixtures.py new file mode 100644 index 0000000..fb4dc33 --- /dev/null +++ b/modart_method/tests/test_fixtures.py @@ -0,0 +1,23 @@ +"""Test fixtures for MoDART method tests.""" +import pytest + + +def test_default_input_data_structure(default_input_data): + """Test that the default input data has the expected structure.""" + assert "results" in default_input_data + assert len(default_input_data["results"]) > 0 + assert "sourceX" in default_input_data["results"][0] + assert "sourceY" in default_input_data["results"][0] + assert "sourceZ" in default_input_data["results"][0] + assert "responses" in default_input_data["results"][0] + assert len(default_input_data["results"][0]["responses"]) > 0 + assert "geo_path" in default_input_data + assert "msh_path" in default_input_data + assert "absorption_coefficients" in default_input_data + + +def test_create_temporary_input_file_fixture(create_temporary_input_file): + """Test that the temporary input file fixture works correctly.""" + import os + assert os.path.exists(create_temporary_input_file) + assert create_temporary_input_file.endswith(".json") diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json new file mode 100644 index 0000000..ee1707b --- /dev/null +++ b/modart_method/tests/test_input_modart.json @@ -0,0 +1,55 @@ +{ + "geo_path": "test_room_modart.geo", + "msh_path": "test_room_modart.msh", + "absorption_coefficients": { + "floor": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", + "wall1": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", + "ceiling": "0.4, 0.5, 0.6, 0.7, 0.8, 0.9", + "wall2": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", + "wall3": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", + "wall4": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15" + }, + "simulationSettings": { + "sim_len_type": "edt", + "edt": 30, + "de_ir_length": 0.5, + "de_lc": 0.5 + }, + "results": [ + { + "simulationMethodId": 1, + "resultType": "IR", + "simulationId": 1, + "sourceX": 1.0, + "sourceY": 2.0, + "sourceZ": 1.5, + "frequencies": [ + 125, + 250, + 500, + 1000, + 2000, + 4000 + ], + "percentage": 0, + "responses": [ + { + "responseId": 1, + "x": 5.0, + "y": 3.5, + "z": 1.5, + "parameters": { + "edt": [], + "t20": [], + "t30": [], + "c80": [], + "d50": [], + "ts": [], + "spl_t0_freq": [] + }, + "receiverResults": [] + } + ] + } + ] +} diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py new file mode 100644 index 0000000..cd09e54 --- /dev/null +++ b/modart_method/tests/test_modart_cli.py @@ -0,0 +1,37 @@ +"""Test the MoDART method CLI.""" +import os +import json +import pytest + +from modart_interface import main + + +def test_modart_method_cli(mock_requests_post, create_temporary_input_file): + """Test the MoDART method CLI.""" + # Set JSON_PATH environment variable and call main() directly + os.environ["JSON_PATH"] = create_temporary_input_file + main() + + with open(create_temporary_input_file, 'r') as f: + data = json.load(f) + + # TODO: Add assertions specific to your simulation method + # For example, check that results were written to the JSON file + # assert "receiverResults" in data['results'][0]['responses'][0] + # results = data['results'][0]['responses'][0]['receiverResults'] + # assert results is not None + # assert len(results) > 0 + + # Verify that requests.post was called (save_results was executed) + mock_requests_post.assert_called_once() + + +def test_modart_method_cli_missing_json_path(mock_requests_post): + """Test the MoDART method CLI with missing JSON_PATH.""" + # Clear JSON_PATH environment variable + if "JSON_PATH" in os.environ: + del os.environ["JSON_PATH"] + + # Expect FileNotFoundError from SimulationMethod.__init__ + with pytest.raises(FileNotFoundError, match="input_json_path cannot be None or empty"): + main() diff --git a/modart_method/tests/test_room_modart.geo b/modart_method/tests/test_room_modart.geo new file mode 100644 index 0000000..db900ed --- /dev/null +++ b/modart_method/tests/test_room_modart.geo @@ -0,0 +1,51 @@ +Point(1) = { 0.000000, 5.100000, 0.000000, 1.0 }; +Point(2) = { 6.210000, 4.000000, 0.000000, 1.0 }; +Point(3) = { 5.520000, 0.000000, 0.000000, 1.0 }; +Point(4) = { 0.000000, 0.000000, 0.000000, 1.0 }; +Point(5) = { 0.000000, 5.100000, 3.300000, 1.0 }; +Point(6) = { 6.210000, 4.000000, 3.300000, 1.0 }; +Point(7) = { 0.000000, 0.000000, 3.300000, 1.0 }; +Point(8) = { 5.520000, 0.000000, 3.300000, 1.0 }; + +Line(1) = { 1, 2 }; +Line(2) = { 1, 4 }; +Line(3) = { 1, 5 }; +Line(4) = { 2, 3 }; +Line(5) = { 2, 6 }; +Line(6) = { 3, 4 }; +Line(7) = { 3, 8 }; +Line(8) = { 4, 7 }; +Line(9) = { 5, 6 }; +Line(10) = { 5, 7 }; +Line(11) = { 6, 8 }; +Line(12) = { 7, 8 }; + +Line Loop(1) = { 6, -2, 1, 4 }; +Line Loop(2) = { -1, 3, 9, -5 }; +Line Loop(3) = { -9, 10, 12, -11 }; +Line Loop(4) = { -6, 7, -12, -8 }; +Line Loop(5) = { 2, 8, -10, -3 }; +Line Loop(6) = { 11, -7, -4, 5 }; + +Plane Surface(1) = { 1 }; +Plane Surface(2) = { 2 }; +Plane Surface(3) = { 3 }; +Plane Surface(4) = { 4 }; +Plane Surface(5) = { 5 }; +Plane Surface(6) = { 6 }; + +Surface Loop(1) = { 1, 2, 3, 4, 5, 6 }; +Physical Surface("floor") = { 1 }; +Physical Surface("wall1") = { 2 }; +Physical Surface("ceiling") = { 3 }; +Physical Surface("wall2") = { 4 }; +Physical Surface("wall3") = { 5 }; +Physical Surface("wall4") = { 6 }; +Volume( 1 ) = { 1 }; +Physical Volume("RoomVolume") = { 1 }; +Physical Line ("default") = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; +Mesh.Algorithm = 6; +Mesh.Algorithm3D = 1; // Delaunay3D, works for boundary layer insertion. +Mesh.Optimize = 1; // Gmsh smoother, works with boundary layers (netgen version does not). +Mesh.CharacteristicLengthFromPoints = 1; +// Recombine Surface "*"; diff --git a/modart_method/tests/test_room_modart.msh b/modart_method/tests/test_room_modart.msh new file mode 100644 index 0000000..bbcdc1f --- /dev/null +++ b/modart_method/tests/test_room_modart.msh @@ -0,0 +1,2116 @@ +$MeshFormat +4.1 0 8 +$EndMeshFormat +$PhysicalNames +8 +1 8 "default" +2 1 "floor" +2 2 "wall1" +2 3 "ceiling" +2 4 "wall2" +2 5 "wall3" +2 6 "wall4" +3 7 "RoomVolume" +$EndPhysicalNames +$Entities +8 12 6 1 +1 0 5.1 0 0 +2 6.21 4 0 0 +3 5.52 0 0 0 +4 0 0 0 0 +5 0 5.1 3.3 0 +6 6.21 4 3.3 0 +7 0 0 3.3 0 +8 5.52 0 3.3 0 +1 0 4 0 6.21 5.1 0 1 8 2 1 -2 +2 0 0 0 0 5.1 0 1 8 2 1 -4 +3 0 5.1 0 0 5.1 3.3 1 8 2 1 -5 +4 5.52 0 0 6.21 4 0 1 8 2 2 -3 +5 6.21 4 0 6.21 4 3.3 1 8 2 2 -6 +6 0 0 0 5.52 0 0 1 8 2 3 -4 +7 5.52 0 0 5.52 0 3.3 1 8 2 3 -8 +8 0 0 0 0 0 3.3 1 8 2 4 -7 +9 0 4 3.3 6.21 5.1 3.3 1 8 2 5 -6 +10 0 0 3.3 0 5.1 3.3 1 8 2 5 -7 +11 5.52 0 3.3 6.21 4 3.3 1 8 2 6 -8 +12 0 0 3.3 5.52 0 3.3 1 8 2 7 -8 +1 0 0 0 6.21 5.1 0 1 1 4 6 -2 1 4 +2 0 4 0 6.21 5.1 3.3 1 2 4 -1 3 9 -5 +3 0 0 3.3 6.21 5.1 3.3 1 3 4 -9 10 12 -11 +4 0 0 0 5.52 0 3.3 1 4 4 -6 7 -12 -8 +5 0 0 0 0 5.1 3.3 1 5 4 2 8 -10 -3 +6 5.52 0 0 6.21 4 3.3 1 6 4 11 -7 -4 5 +1 0 0 0 6.21 5.1 3.3 1 7 6 1 2 3 4 5 6 +$EndEntities +$Nodes +27 286 1 286 +0 1 0 1 +1 +0 5.1 0 +0 2 0 1 +2 +6.21 4 0 +0 3 0 1 +3 +5.52 0 0 +0 4 0 1 +4 +0 0 0 +0 5 0 1 +5 +0 5.1 3.3 +0 6 0 1 +6 +6.21 4 3.3 +0 7 0 1 +7 +0 0 3.3 +0 8 0 1 +8 +5.52 0 3.3 +1 1 0 7 +9 +10 +11 +12 +13 +14 +15 +0.7762499999987194 4.962500000000227 0 +1.552499999996365 4.825000000000643 0 +2.32874999999396 4.687500000001069 0 +3.104999999991609 4.550000000001486 0 +3.881249999993475 4.412500000001156 0 +4.657499999996126 4.275000000000686 0 +5.433749999997627 4.13750000000042 0 +1 2 0 5 +16 +17 +18 +19 +20 +0 4.25000000000211 0 +0 3.400000000004221 0 +0 2.550000000006418 0 +0 1.70000000000445 0 +0 0.8500000000019954 0 +1 3 0 3 +21 +22 +23 +0 5.1 0.8249999999980804 +0 5.1 1.649999999995743 +0 5.1 2.474999999997828 +1 4 0 4 +24 +25 +26 +27 +6.072000000000003 3.200000000000018 0 +5.93400000000094 2.400000000005448 0 +5.79600000000112 1.600000000006494 0 +5.658000000000555 0.8000000000032195 0 +1 5 0 3 +28 +29 +30 +6.21 4 0.8249999999980804 +6.21 4 1.649999999995743 +6.21 4 2.474999999997828 +1 6 0 6 +31 +32 +33 +34 +35 +36 +4.731428571430031 0 0 +3.942857142860572 0 0 +3.154285714291769 0 0 +2.365714285720427 0 0 +1.577142857146942 0 0 +0.7885714285734089 0 0 +1 7 0 3 +37 +38 +39 +5.52 0 0.8249999999980804 +5.52 0 1.649999999995743 +5.52 0 2.474999999997828 +1 8 0 3 +40 +41 +42 +0 0 0.8249999999980804 +0 0 1.649999999995743 +0 0 2.474999999997828 +1 9 0 7 +43 +44 +45 +46 +47 +48 +49 +0.7762499999987194 4.962500000000227 3.3 +1.552499999996365 4.825000000000643 3.3 +2.32874999999396 4.687500000001069 3.3 +3.104999999991609 4.550000000001486 3.3 +3.881249999993475 4.412500000001156 3.3 +4.657499999996126 4.275000000000686 3.3 +5.433749999997627 4.13750000000042 3.3 +1 10 0 5 +50 +51 +52 +53 +54 +0 4.25000000000211 3.3 +0 3.400000000004221 3.3 +0 2.550000000006418 3.3 +0 1.70000000000445 3.3 +0 0.8500000000019954 3.3 +1 11 0 4 +55 +56 +57 +58 +6.072000000000003 3.200000000000018 3.3 +5.93400000000094 2.400000000005448 3.3 +5.79600000000112 1.600000000006494 3.3 +5.658000000000555 0.8000000000032195 3.3 +1 12 0 6 +59 +60 +61 +62 +63 +64 +0.7885714285697485 0 3.3 +1.577142857138913 0 3.3 +2.365714285708011 0 3.3 +3.154285714279408 0 3.3 +3.942857142852948 0 3.3 +4.731428571426536 0 3.3 +2 1 0 41 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +2.702246367247137 0.6278844212046903 0 +2.606042587526941 3.994689295288387 0 +4.135501471387311 3.688925114670039 0 +0.7255309746239729 2.136165699760757 0 +4.902674462051395 1.252630494334912 0 +1.20047813245132 0.6104954181138812 0 +5.269159873184588 2.92986357870479 0 +0.6515713233657827 3.679057086664729 0 +4.373071687051047 0.6141996761085335 0 +0.713879408774184 2.951516656175773 0 +1.445202806081749 2.549401318616725 0 +1.4661307299318 1.769306549349747 0 +2.160856254496739 2.13997549312561 0 +2.153388815033426 2.910769260569127 0 +2.858826860697639 2.527420539366687 0 +2.883103284483341 1.78179324994803 0 +3.555295546448354 2.151167921494717 0 +3.55538599452136 1.347703574723207 0 +3.642404054441497 3.018695082077782 0 +4.143451559114237 2.528430844542227 0 +1.445042169812366 3.407415605569577 0 +5.161746439014879 2.08728091321036 0 +2.255048869344842 1.323748656822932 0 +3.374880227047213 3.823234457155831 0 +4.298587786259919 1.843190706691889 0 +1.830781621157491 4.101449621592511 0 +0.7683875600501459 1.274723232763478 0 +4.91237749292892 3.6129436942932 0 +1.971428571433684 0.6229082830041432 0 +3.548622212624968 0.7121086994479576 0 +2.79381059071493 3.305830370074237 0 +5.019055347763423 0.5854986388992025 0 +1.051585836382507 4.255182029687647 0 +5.540980365877392 3.531097221289941 0 +4.488329658859804 3.061315687642965 0 +0.5584260501118248 0.5651366727068572 0 +2.127320999542864 3.560173585987611 0 +1.577361304461205 1.127741821563272 0 +4.672255063286686 2.490016346158446 0 +3.0012588298784 1.181109857586796 0 +4.135668428501737 1.1539666302613 0 +2 2 0 31 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +1.983496193715434 4.748656068746058 2.642353555724199 +3.524861074614583 4.475628473095646 0.6469532362317842 +4.170689869483857 4.36123045790141 2.617641854477065 +1.981024688072751 4.749093855574875 0.6368379189250895 +5.529889823872385 4.120470401568499 1.272491748558482 +0.6444333288960226 4.985849168794585 2.05927767325974 +5.570072844942467 4.11335263616156 2.064242586280364 +4.893356454539051 4.233221884059105 1.674949874422897 +4.924879178751135 4.227638148691425 0.7701082756560119 +4.135560251077941 4.367453095622265 1.249948278105242 +0.6343656858248634 4.987632487212987 1.24104712499307 +1.20202905091593 4.88708020032085 1.649139535744951 +3.432667366830957 4.491959081559734 2.622829972307688 +2.728897627109878 4.616620388112582 0.6492714764780272 +3.10499999999319 4.550000000001206 1.365434340236661 +2.703261743501124 4.621161365885469 2.638722571453489 +2.343935826619011 4.684810079020787 1.922301438399384 +3.688841588058679 4.446582005335822 1.925311476061128 +4.96989686064763 4.219664002139711 2.553109812715744 +1.202302782053134 4.887031713323921 0.7163966354029625 +1.210159247866509 4.885640068815916 2.561144913134303 +2.385376420429541 4.677469555157408 1.159758465332143 +3.054741305000592 4.558902506360604 2.094919959691671 +4.224810100886651 4.351643943482236 0.5334019579986077 +4.312565322594148 4.336099540281229 1.991227115787539 +5.618616525137388 4.104753916642331 0.6097483995752218 +0.5776833462350431 4.997672837220845 2.70354663497231 +0.5762063749332704 4.997934458546442 0.5950200657474403 +5.63404965774177 4.102020189449928 2.705228396391119 +1.723999555027303 4.794621656919479 2.019411175964083 +1.795467360471973 4.78196230329804 1.314728373340517 +2 3 0 41 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +2.702246367239102 0.627884421204691 3.3 +2.606042587526935 3.994689295288329 3.3 +4.135501471387224 3.688925114669808 3.3 +0.725530974623923 2.136165699760805 3.3 +4.902674462049536 1.252630494335985 3.3 +1.200478132446673 0.610495418113386 3.3 +5.269159873184508 2.929863578704726 3.3 +0.6515713233657568 3.679057086664713 3.3 +4.373071687046156 0.6141996761093222 3.3 +0.7138794087741396 2.951516656175771 3.3 +1.445202806081554 2.549401318616729 3.3 +1.466130729931171 1.769306549349424 3.3 +2.160856254496093 2.13997549312568 3.3 +2.15338881503317 2.910769260569019 3.3 +2.858826860697112 2.527420539366371 3.3 +2.883103284482265 1.781793249947509 3.3 +3.555295546447529 2.151167921494006 3.3 +3.555385994520122 1.347703574722567 3.3 +3.64240405444118 3.018695082077328 3.3 +4.143451559113692 2.528430844541685 3.3 +1.445042169812269 3.407415605569536 3.3 +5.161746439014308 2.087280913210382 3.3 +2.255048869341225 1.323748656822352 3.3 +3.374880227047138 3.823234457155672 3.3 +4.298587786258741 1.843190706691747 3.3 +1.830781621157481 4.101449621592469 3.3 +0.7683875600489698 1.274723232762874 3.3 +4.912377492928875 3.61294369429304 3.3 +1.971428571423462 0.6229082830067182 3.3 +3.548622212619272 0.7121086994479118 3.3 +2.793810590714735 3.30583037007404 3.3 +5.019055347760613 0.5854986389009575 3.3 +1.051585836382491 4.255182029687636 3.3 +5.540980365877386 3.531097221289929 3.3 +4.488329658859553 3.061315687642745 3.3 +0.5584260501090383 0.5651366727055279 3.3 +2.127320999542876 3.560173585987518 3.3 +1.577361304458576 1.127741821560872 3.3 +4.672255063286161 2.490016346158257 3.3 +3.00125882987532 1.181109857585027 3.3 +4.135668428498765 1.153966630261506 3.3 +2 4 0 28 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +3.505459795894285 0 0.658032377106032 +2.01454020410447 0 2.641967622893278 +3.507053838499056 0 2.663309771093026 +2.012946161501398 0 0.6366902289065148 +4.865401979309599 0 2.05921375528795 +0.6545980206904399 0 1.24078624470752 +4.875602516131682 0 1.241094456535543 +4.298871526490614 0 1.651488430344355 +0.6443974838671406 0 2.058905543460367 +1.221128473508714 0 1.648511569653169 +2.760265673763017 0 2.648836058929298 +2.384072622735947 0 1.92338884153312 +2.75973432623673 0 0.6511639410704287 +3.136610066657712 0 1.376210827548336 +1.22738113669884 0 2.560606383438317 +4.292618863299248 0 0.7393936165603563 +4.296350786159673 0 2.584228126142046 +1.223649213841326 0 0.715771873856837 +2.417904882314414 0 1.162541000761508 +3.102368193443461 0 2.137298866870632 +0.5869333018814119 0 0.5966410707398072 +4.933066698120168 0 2.703358929260585 +4.934555941642836 0 0.5952175860213638 +0.5854440583546006 0 2.704782413979891 +1.751447634802106 0 2.019299358032363 +3.768649892252999 0 1.280643451835007 +1.824039994209592 0 1.315095912873981 +3.696085397720391 0 1.98483055695618 +2 5 0 22 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +0 2.975000000005319 0.7361215932148703 +0 2.133874682074826 2.613113855916287 +0 3.817679962353616 2.61760765757847 +0 1.281886275606401 0.6820170777442269 +0 0.8612986076400723 2.035771238118329 +0 4.238698811323031 1.264060753882836 +0 2.125794453959565 0.6807297793938323 +0 1.690398848941836 1.432404491573377 +0 2.9752591074082 2.582039386289344 +0 3.563364762682421 1.92085905061594 +0 2.57296682209647 1.863891573970136 +0 1.309795077452145 2.674067092312445 +0 3.788013766143618 0.6555925894110306 +0 4.405657901755993 2.027635791990685 +0 0.7191813219154064 1.239127261499417 +0 4.47542187234981 2.681874050298682 +0 0.6245781276490048 0.6181259496979481 +0 0.6048716833449455 2.740832855243506 +0 4.49481492559346 0.5633802410014497 +0 2.551410583650405 1.18354020885267 +0 1.713666807641069 2.123849650378115 +0 3.281575790983544 1.27067762832458 +2 6 0 18 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +5.86504015491614 2.000232782122551 2.753274856069659 +5.867830910298883 2.016411074196425 0.5360075278748611 +5.637260150785848 0.6797689900628874 1.241778785205294 +6.09276160492423 3.320357129995534 1.241630517347858 +6.093189791309344 3.322839369909241 2.057600205069194 +5.987287937489962 2.708915579651955 1.640940440356776 +5.636903658445092 0.6777023677976404 2.058585057068526 +5.74749001496069 1.318782695424291 1.641218812814056 +5.992924077649551 2.741588855939429 2.570447607394167 +5.737509580613327 1.260925105004799 2.570866003043504 +5.73608444090115 1.252663425513916 0.7028856333450937 +5.992997453982559 2.742014225985852 0.7398057616850354 +5.866671077855663 2.009687407858918 2.007813954164472 +6.107400876806572 3.405222474240992 2.6930046065353 +6.107550781131361 3.406091484819482 0.6061276416337821 +5.622449218869113 0.5939085151832642 2.693872358363222 +5.616606147060705 0.5600356351345229 0.5688456716878525 +5.875832781331978 2.062798732359293 1.289268727873359 +3 1 0 41 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +3.982748395016723 2.131866902494136 1.649999999999908 +1.905001571868004 2.522929832411618 1.649999999999942 +2.783809050303439 1.379359660610481 1.894049905953509 +3.105235241577535 3.18471758150762 1.886406261975826 +1.447295988181111 1.305498849181051 1.97708543851478 +4.523319666724902 1.161399387875922 1.153392657073833 +1.87102264981235 3.605615646780535 1.160033125701817 +4.843491511925335 2.86040094787313 1.159889058803643 +1.757757297908341 3.70957514665298 2.261620215216739 +3.974032600267527 1.102001581995885 2.162311504171539 +4.256596207974636 3.167131397587323 2.14716125977807 +2.93509507365237 2.280750044950089 1.189456430932815 +3.42563054064633 1.031436021518221 1.058711176608252 +2.079698879421366 1.066651901090252 1.078148983359485 +1.088438455019048 1.937812665252411 1.078967168932442 +1.046797986343716 2.866301854554071 2.251872303608898 +4.856350181630246 2.280809082357493 2.260543890093401 +3.619199073662395 3.457726940313969 0.9817434735267551 +3.191764896308864 2.239857304993485 2.324304096386526 +1.021657408142361 2.991891967645576 1.011780356152358 +0.9553033612611703 3.853164463551219 1.604038917597632 +5.185592196431918 3.287766957375741 2.396163653348566 +1.066049658648649 0.9216779577124237 1.115851322635183 +2.111357440549796 1.89801080086422 2.362239546773829 +2.170233150211595 0.9018085990384976 2.423753598319347 +2.325974508817458 2.962854438232422 2.372499034842639 +3.75669844547966 2.484280640661297 0.8322020639774979 +3.171670189850378 0.826084207416367 2.489813911820977 +4.874400316607696 1.364296427022081 2.012209510457316 +2.711173798751009 3.187294244662575 1.023782385116788 +2.634843387789958 3.808749344129123 2.298460373602619 +0.861830452322895 0.8372499722850986 2.48890077077935 +2.096277343598294 1.983359873401345 0.9156131131644956 +4.560917135263096 2.035107710641359 0.8624320557041605 +0.8968064261381468 1.946977374457191 2.393097558816704 +4.033500082767937 2.210167091732712 2.506719542441852 +3.586321646640657 3.639902497162266 2.465755143136407 +1.847800900905321 2.810200118357459 0.8083406404637883 +2.758573749830245 1.445335653420554 0.7596196862816191 +4.507730337136908 3.485203150259042 0.7192062927872336 +0.9071392624803817 3.702990021572242 2.464111603443851 +$EndNodes +$Elements +19 1448 1 1448 +1 1 1 8 +1 1 9 +2 9 10 +3 10 11 +4 11 12 +5 12 13 +6 13 14 +7 14 15 +8 15 2 +1 2 1 6 +9 1 16 +10 16 17 +11 17 18 +12 18 19 +13 19 20 +14 20 4 +1 3 1 4 +15 1 21 +16 21 22 +17 22 23 +18 23 5 +1 4 1 5 +19 2 24 +20 24 25 +21 25 26 +22 26 27 +23 27 3 +1 5 1 4 +24 2 28 +25 28 29 +26 29 30 +27 30 6 +1 6 1 7 +28 3 31 +29 31 32 +30 32 33 +31 33 34 +32 34 35 +33 35 36 +34 36 4 +1 7 1 4 +35 3 37 +36 37 38 +37 38 39 +38 39 8 +1 8 1 4 +39 4 40 +40 40 41 +41 41 42 +42 42 7 +1 9 1 8 +43 5 43 +44 43 44 +45 44 45 +46 45 46 +47 46 47 +48 47 48 +49 48 49 +50 49 6 +1 10 1 6 +51 5 50 +52 50 51 +53 51 52 +54 52 53 +55 53 54 +56 54 7 +1 11 1 5 +57 6 55 +58 55 56 +59 56 57 +60 57 58 +61 58 8 +1 12 1 7 +62 7 59 +63 59 60 +64 60 61 +65 61 62 +66 62 63 +67 63 64 +68 64 8 +2 1 2 106 +69 16 1 9 +70 15 2 98 +71 2 24 98 +72 27 3 96 +73 3 31 96 +74 4 20 100 +75 36 4 100 +76 9 10 97 +77 16 9 97 +78 10 11 90 +79 10 90 97 +80 11 12 66 +81 11 66 90 +82 12 13 88 +83 66 12 88 +84 13 14 67 +85 13 67 88 +86 14 15 92 +87 67 14 92 +88 92 15 98 +89 17 16 72 +90 72 16 97 +91 18 17 74 +92 17 72 74 +93 19 18 68 +94 68 18 74 +95 20 19 91 +96 19 68 91 +97 20 91 100 +98 24 25 71 +99 24 71 98 +100 25 26 86 +101 71 25 86 +102 26 27 69 +103 26 69 86 +104 69 27 96 +105 31 32 73 +106 31 73 96 +107 32 33 94 +108 73 32 94 +109 33 34 65 +110 33 65 94 +111 34 35 93 +112 65 34 93 +113 35 36 70 +114 35 70 93 +115 70 36 100 +116 87 65 93 +117 65 87 104 +118 94 65 104 +119 66 88 95 +120 90 66 101 +121 66 95 101 +122 67 83 88 +123 83 67 99 +124 67 92 99 +125 68 74 75 +126 68 75 76 +127 68 76 91 +128 73 69 96 +129 69 73 105 +130 86 69 89 +131 89 69 105 +132 91 70 100 +133 70 91 102 +134 93 70 102 +135 71 86 103 +136 71 92 98 +137 92 71 99 +138 99 71 103 +139 74 72 85 +140 85 72 97 +141 73 94 105 +142 75 74 85 +143 76 75 77 +144 77 75 78 +145 78 75 85 +146 76 77 87 +147 76 87 102 +148 91 76 102 +149 77 78 79 +150 77 79 80 +151 77 80 87 +152 79 78 95 +153 78 85 101 +154 95 78 101 +155 80 79 81 +156 81 79 83 +157 83 79 95 +158 80 81 82 +159 80 82 104 +160 87 80 104 +161 82 81 89 +162 81 83 84 +163 81 84 89 +164 82 89 105 +165 82 94 104 +166 94 82 105 +167 84 83 99 +168 88 83 95 +169 89 84 103 +170 84 99 103 +171 90 85 97 +172 85 90 101 +173 86 89 103 +174 87 93 102 +2 2 2 84 +175 1 133 9 +176 21 133 1 +177 15 131 2 +178 2 131 28 +179 5 132 23 +180 43 132 5 +181 30 134 6 +182 6 134 49 +183 9 125 10 +184 9 133 125 +185 10 109 11 +186 10 125 109 +187 11 119 12 +188 109 119 11 +189 12 107 13 +190 12 119 107 +191 13 129 14 +192 107 129 13 +193 14 114 15 +194 14 129 114 +195 114 131 15 +196 22 116 21 +197 116 133 21 +198 23 111 22 +199 111 116 22 +200 23 132 111 +201 28 110 29 +202 28 131 110 +203 29 112 30 +204 110 112 29 +205 112 134 30 +206 44 126 43 +207 126 132 43 +208 45 106 44 +209 106 126 44 +210 46 121 45 +211 45 121 106 +212 47 118 46 +213 118 121 46 +214 48 108 47 +215 108 118 47 +216 49 124 48 +217 48 124 108 +218 49 134 124 +219 121 122 106 +220 122 135 106 +221 106 135 126 +222 107 120 115 +223 115 129 107 +224 119 120 107 +225 108 123 118 +226 108 130 123 +227 124 130 108 +228 109 127 119 +229 125 136 109 +230 109 136 127 +231 110 113 112 +232 110 114 113 +233 110 131 114 +234 111 117 116 +235 111 126 117 +236 111 132 126 +237 113 124 112 +238 124 134 112 +239 114 115 113 +240 115 130 113 +241 113 130 124 +242 114 129 115 +243 120 123 115 +244 123 130 115 +245 117 125 116 +246 125 133 116 +247 117 136 125 +248 126 135 117 +249 135 136 117 +250 118 128 121 +251 123 128 118 +252 119 127 120 +253 120 127 122 +254 122 128 120 +255 120 128 123 +256 121 128 122 +257 127 136 122 +258 122 136 135 +2 3 2 106 +259 43 5 50 +260 49 170 6 +261 6 170 55 +262 7 172 54 +263 59 172 7 +264 58 168 8 +265 8 168 64 +266 43 169 44 +267 50 169 43 +268 44 162 45 +269 44 169 162 +270 45 138 46 +271 45 162 138 +272 46 160 47 +273 138 160 46 +274 47 139 48 +275 47 160 139 +276 48 164 49 +277 139 164 48 +278 164 170 49 +279 51 144 50 +280 144 169 50 +281 52 146 51 +282 51 146 144 +283 53 140 52 +284 140 146 52 +285 54 163 53 +286 53 163 140 +287 54 172 163 +288 55 143 56 +289 55 170 143 +290 56 158 57 +291 143 158 56 +292 57 141 58 +293 57 158 141 +294 141 168 58 +295 60 142 59 +296 142 172 59 +297 61 165 60 +298 60 165 142 +299 62 137 61 +300 137 165 61 +301 63 166 62 +302 62 166 137 +303 64 145 63 +304 145 166 63 +305 64 168 145 +306 159 165 137 +307 137 176 159 +308 166 176 137 +309 138 167 160 +310 162 173 138 +311 138 173 167 +312 139 160 155 +313 155 171 139 +314 139 171 164 +315 140 147 146 +316 140 148 147 +317 140 163 148 +318 145 168 141 +319 141 177 145 +320 158 161 141 +321 161 177 141 +322 163 172 142 +323 142 174 163 +324 165 174 142 +325 143 175 158 +326 143 170 164 +327 164 171 143 +328 171 175 143 +329 146 157 144 +330 157 169 144 +331 145 177 166 +332 147 157 146 +333 148 149 147 +334 149 150 147 +335 150 157 147 +336 148 159 149 +337 148 174 159 +338 163 174 148 +339 149 151 150 +340 149 152 151 +341 149 159 152 +342 151 167 150 +343 150 173 157 +344 167 173 150 +345 152 153 151 +346 153 155 151 +347 155 167 151 +348 152 154 153 +349 152 176 154 +350 159 176 152 +351 154 161 153 +352 153 156 155 +353 153 161 156 +354 154 177 161 +355 154 176 166 +356 166 177 154 +357 156 171 155 +358 160 167 155 +359 161 175 156 +360 156 175 171 +361 162 169 157 +362 157 173 162 +363 158 175 161 +364 159 174 165 +2 4 2 76 +365 3 200 31 +366 37 200 3 +367 36 198 4 +368 4 198 40 +369 42 201 7 +370 7 201 59 +371 8 199 39 +372 64 199 8 +373 31 193 32 +374 31 200 193 +375 32 178 33 +376 32 193 178 +377 33 190 34 +378 178 190 33 +379 34 181 35 +380 34 190 181 +381 35 195 36 +382 181 195 35 +383 195 198 36 +384 38 184 37 +385 184 200 37 +386 39 182 38 +387 182 184 38 +388 39 199 182 +389 40 183 41 +390 40 198 183 +391 41 186 42 +392 183 186 41 +393 186 201 42 +394 59 192 60 +395 59 201 192 +396 60 179 61 +397 60 192 179 +398 61 188 62 +399 179 188 61 +400 62 180 63 +401 62 188 180 +402 63 194 64 +403 180 194 63 +404 194 199 64 +405 178 191 190 +406 178 203 191 +407 193 203 178 +408 179 189 188 +409 179 202 189 +410 192 202 179 +411 188 197 180 +412 180 205 194 +413 197 205 180 +414 190 196 181 +415 181 204 195 +416 196 204 181 +417 182 185 184 +418 182 194 185 +419 182 199 194 +420 183 187 186 +421 183 195 187 +422 183 198 195 +423 185 193 184 +424 193 200 184 +425 185 203 193 +426 194 205 185 +427 185 205 203 +428 187 192 186 +429 192 201 186 +430 187 202 192 +431 195 204 187 +432 187 204 202 +433 189 197 188 +434 189 196 191 +435 191 197 189 +436 189 204 196 +437 202 204 189 +438 191 196 190 +439 191 205 197 +440 203 205 191 +2 5 2 62 +441 1 16 224 +442 21 1 224 +443 20 4 222 +444 4 40 222 +445 5 23 221 +446 50 5 221 +447 42 7 223 +448 7 54 223 +449 16 17 218 +450 16 218 224 +451 17 18 206 +452 17 206 218 +453 18 19 212 +454 206 18 212 +455 19 20 209 +456 19 209 212 +457 209 20 222 +458 22 21 211 +459 211 21 224 +460 23 22 219 +461 22 211 219 +462 23 219 221 +463 40 41 220 +464 40 220 222 +465 41 42 210 +466 41 210 220 +467 210 42 223 +468 51 50 208 +469 208 50 221 +470 52 51 214 +471 51 208 214 +472 53 52 207 +473 207 52 214 +474 54 53 217 +475 53 207 217 +476 54 217 223 +477 206 212 225 +478 218 206 227 +479 206 225 227 +480 207 214 216 +481 207 216 226 +482 217 207 226 +483 214 208 215 +484 215 208 219 +485 219 208 221 +486 212 209 213 +487 213 209 220 +488 220 209 222 +489 210 213 220 +490 213 210 226 +491 217 210 223 +492 210 217 226 +493 211 215 219 +494 215 211 227 +495 218 211 224 +496 211 218 227 +497 212 213 225 +498 213 216 225 +499 216 213 226 +500 214 215 216 +501 216 215 227 +502 225 216 227 +2 6 2 52 +503 24 2 242 +504 2 28 242 +505 3 27 244 +506 37 3 244 +507 30 6 241 +508 6 55 241 +509 8 39 243 +510 58 8 243 +511 25 24 239 +512 239 24 242 +513 26 25 229 +514 229 25 239 +515 27 26 238 +516 26 229 238 +517 27 238 244 +518 28 29 231 +519 28 231 242 +520 29 30 232 +521 231 29 232 +522 232 30 241 +523 38 37 230 +524 230 37 244 +525 39 38 234 +526 38 230 234 +527 39 234 243 +528 55 56 236 +529 55 236 241 +530 56 57 228 +531 56 228 236 +532 57 58 237 +533 228 57 237 +534 237 58 243 +535 236 228 240 +536 228 237 240 +537 238 229 245 +538 229 239 245 +539 234 230 235 +540 235 230 238 +541 238 230 244 +542 231 232 233 +543 231 233 239 +544 231 239 242 +545 233 232 236 +546 236 232 241 +547 233 236 240 +548 239 233 245 +549 233 240 245 +550 234 235 237 +551 234 237 243 +552 237 235 240 +553 235 238 245 +554 240 235 245 +3 1 4 894 +555 168 199 145 274 +556 110 253 231 267 +557 125 224 218 266 +558 105 258 251 279 +559 82 272 258 279 +560 224 211 218 266 +561 232 110 231 267 +562 82 258 105 279 +563 66 88 119 275 +564 145 194 255 274 +565 112 110 232 267 +566 203 255 251 258 +567 145 199 194 274 +568 97 265 252 266 +569 196 248 191 259 +570 90 109 125 252 +571 258 272 246 279 +572 168 243 199 274 +573 218 265 72 266 +574 234 230 184 274 +575 184 182 234 274 +576 113 253 110 267 +577 131 231 110 253 +578 131 98 242 253 +579 230 251 184 274 +580 119 252 66 275 +581 119 127 252 275 +582 258 272 82 284 +583 122 252 127 276 +584 184 251 182 274 +585 224 218 97 125 +586 73 251 105 258 +587 66 109 90 252 +588 131 242 231 253 +589 246 256 253 272 +590 72 265 97 266 +591 119 109 66 252 +592 97 85 252 265 +593 131 98 253 285 +594 125 211 224 266 +595 66 90 101 252 +596 249 264 167 271 +597 89 82 105 279 +598 82 89 272 279 +599 189 248 196 259 +600 81 82 272 284 +601 218 72 97 266 +602 168 145 141 274 +603 185 251 203 255 +604 248 258 191 259 +605 231 253 233 267 +606 218 97 125 266 +607 255 258 205 273 +608 194 199 182 274 +609 251 258 246 279 +610 256 263 253 272 +611 189 191 196 248 +612 194 182 255 274 +613 202 259 250 270 +614 88 263 119 275 +615 123 263 256 282 +616 245 274 262 279 +617 248 246 255 258 +618 71 242 98 253 +619 252 247 261 265 +620 246 258 257 272 +621 247 254 252 261 +622 232 231 233 267 +623 147 269 261 271 +624 66 252 101 275 +625 107 119 88 263 +626 152 176 159 270 +627 97 72 85 265 +628 154 264 255 273 +629 97 252 125 266 +630 264 281 256 282 +631 123 130 256 263 +632 257 272 258 284 +633 204 250 202 259 +634 251 274 235 279 +635 193 251 73 258 +636 152 269 248 270 +637 110 131 253 285 +638 248 259 269 278 +639 246 248 257 258 +640 245 262 253 279 +641 113 256 253 267 +642 249 264 256 282 +643 244 96 200 251 +644 94 73 105 258 +645 250 269 259 278 +646 113 110 112 267 +647 119 127 109 252 +648 205 255 203 258 +649 93 102 70 268 +650 216 261 260 265 +651 249 120 275 276 +652 197 191 248 273 +653 197 205 191 273 +654 234 182 243 274 +655 255 264 154 281 +656 182 199 243 274 +657 205 258 191 273 +658 113 253 256 285 +659 155 281 264 282 +660 190 259 258 284 +661 254 271 252 276 +662 202 189 259 270 +663 252 265 261 266 +664 71 253 98 285 +665 191 258 248 273 +666 246 257 249 272 +667 123 249 263 282 +668 252 261 254 266 +669 216 260 261 280 +670 152 248 264 273 +671 257 264 249 271 +672 116 211 125 266 +673 97 90 125 252 +674 248 255 246 264 +675 184 185 182 251 +676 185 182 251 274 +677 159 269 152 270 +678 248 259 250 269 +679 247 264 257 271 +680 128 276 249 282 +681 251 255 246 258 +682 252 271 247 275 +683 246 256 249 264 +684 226 277 250 280 +685 247 257 264 269 +686 197 191 189 248 +687 133 224 97 125 +688 151 167 264 271 +689 152 264 154 273 +690 256 263 249 282 +691 123 256 130 282 +692 147 261 269 280 +693 93 195 181 259 +694 81 82 89 272 +695 164 139 124 256 +696 128 249 123 282 +697 86 229 253 279 +698 247 261 260 280 +699 230 244 200 251 +700 119 120 127 275 +701 152 154 176 273 +702 109 136 125 252 +703 185 251 255 274 +704 93 70 195 268 +705 250 247 269 278 +706 200 96 73 251 +707 247 257 249 271 +708 94 193 73 258 +709 204 202 189 259 +710 185 255 182 274 +711 136 254 135 266 +712 187 202 204 250 +713 238 251 235 279 +714 155 256 281 282 +715 99 84 103 253 +716 136 125 252 266 +717 86 229 239 253 +718 184 230 200 251 +719 239 242 71 253 +720 260 261 247 265 +721 93 259 102 268 +722 197 248 189 273 +723 261 269 247 271 +724 210 250 226 277 +725 155 151 167 264 +726 103 253 84 279 +727 255 264 248 273 +728 253 262 233 267 +729 203 251 193 258 +730 189 248 259 270 +731 84 253 99 272 +732 152 264 248 269 +733 141 177 161 274 +734 248 269 247 278 +735 168 141 243 274 +736 164 256 124 267 +737 248 264 257 269 +738 247 257 248 278 +739 262 274 246 279 +740 210 268 250 277 +741 250 260 213 280 +742 225 216 260 265 +743 213 260 250 268 +744 245 235 274 279 +745 226 250 213 280 +746 202 270 250 277 +747 247 269 264 271 +748 93 195 259 268 +749 248 258 255 273 +750 250 259 204 268 +751 178 193 94 258 +752 213 250 210 268 +753 247 248 257 269 +754 247 260 250 280 +755 244 69 96 251 +756 190 258 65 284 +757 97 85 90 252 +758 253 272 84 279 +759 86 239 71 253 +760 136 252 254 266 +761 81 272 257 284 +762 38 184 234 230 +763 184 234 182 38 +764 190 65 259 284 +765 256 264 246 281 +766 249 276 167 282 +767 157 147 261 271 +768 239 231 242 253 +769 108 124 139 256 +770 122 254 252 276 +771 120 122 127 276 +772 128 120 249 276 +773 110 253 113 285 +774 120 123 128 249 +775 252 254 247 271 +776 136 122 135 254 +777 149 269 147 271 +778 249 271 167 276 +779 262 274 161 281 +780 249 257 246 264 +781 210 213 226 250 +782 171 175 156 281 +783 253 263 256 285 +784 248 246 257 264 +785 192 142 165 270 +786 120 119 263 275 +787 186 268 210 277 +788 71 98 92 285 +789 157 261 254 271 +790 123 115 130 263 +791 86 71 103 253 +792 202 192 270 277 +793 126 106 162 254 +794 131 92 98 285 +795 142 174 270 277 +796 250 277 174 280 +797 95 88 66 275 +798 164 171 139 256 +799 186 183 220 268 +800 185 203 205 255 +801 187 250 204 268 +802 177 255 154 281 +803 247 250 260 278 +804 142 174 165 270 +805 212 260 68 265 +806 212 68 74 265 +807 192 142 270 277 +808 206 212 74 265 +809 175 262 171 267 +810 135 266 254 286 +811 253 262 246 279 +812 136 252 122 254 +813 261 265 227 266 +814 247 269 261 280 +815 247 249 257 275 +816 249 247 271 275 +817 245 253 229 279 +818 157 150 147 271 +819 93 181 65 259 +820 237 158 228 262 +821 194 180 166 255 +822 186 187 268 277 +823 180 166 255 273 +824 162 106 138 276 +825 190 65 181 259 +826 103 86 253 279 +827 158 262 237 274 +828 209 68 212 260 +829 107 120 119 263 +830 91 68 209 260 +831 123 249 120 263 +832 162 138 173 276 +833 205 203 191 258 +834 141 161 262 274 +835 249 120 263 275 +836 257 263 249 272 +837 165 179 192 270 +838 174 250 269 270 +839 150 149 147 271 +840 135 254 126 286 +841 135 126 266 286 +842 246 264 255 281 +843 193 200 73 251 +844 81 257 80 284 +845 164 171 256 267 +846 233 253 245 262 +847 247 250 269 280 +848 166 145 194 255 +849 256 262 253 267 +850 110 114 131 285 +851 148 250 174 280 +852 187 202 250 277 +853 240 262 245 274 +854 257 79 278 283 +855 141 262 158 274 +856 171 262 256 267 +857 245 238 235 279 +858 162 254 106 276 +859 248 270 189 273 +860 106 121 138 276 +861 166 177 145 255 +862 102 76 91 268 +863 162 173 254 276 +864 140 207 146 261 +865 253 256 246 262 +866 174 250 148 269 +867 209 91 260 268 +868 257 275 79 283 +869 79 272 83 275 +870 178 203 193 258 +871 216 213 260 280 +872 140 207 261 280 +873 251 246 274 279 +874 147 269 148 280 +875 227 261 216 265 +876 132 221 111 286 +877 258 259 248 284 +878 146 207 214 261 +879 72 218 206 265 +880 196 190 181 259 +881 253 99 272 285 +882 259 260 250 278 +883 76 260 91 268 +884 127 122 136 252 +885 264 269 151 271 +886 111 221 219 286 +887 79 257 272 275 +888 141 158 237 274 +889 218 227 265 266 +890 257 258 248 284 +891 250 268 187 277 +892 68 260 75 265 +893 117 135 126 266 +894 108 256 139 282 +895 175 143 262 267 +896 86 238 229 279 +897 250 270 174 277 +898 249 263 257 275 +899 246 255 251 274 +900 153 154 264 281 +901 69 73 96 251 +902 157 254 173 271 +903 185 193 203 251 +904 177 154 161 281 +905 254 261 247 271 +906 229 245 239 253 +907 104 65 258 284 +908 111 266 126 286 +909 83 272 263 275 +910 151 264 152 269 +911 195 204 181 259 +912 245 229 238 279 +913 149 151 269 271 +914 250 259 248 270 +915 65 94 104 258 +916 101 275 252 283 +917 219 266 111 286 +918 238 69 251 279 +919 141 161 158 262 +920 204 189 196 259 +921 79 77 278 283 +922 66 101 95 275 +923 186 187 183 268 +924 215 261 227 266 +925 80 257 278 284 +926 149 151 152 269 +927 186 220 210 268 +928 81 80 82 284 +929 124 256 113 267 +930 132 111 126 286 +931 238 69 244 251 +932 246 255 274 281 +933 253 272 263 285 +934 150 157 173 271 +935 116 111 219 266 +936 185 194 182 255 +937 225 260 212 265 +938 236 233 262 267 +939 250 148 269 280 +940 238 244 230 251 +941 79 80 257 278 +942 75 260 278 283 +943 85 101 90 252 +944 222 100 91 268 +945 236 262 143 267 +946 263 272 257 275 +947 175 171 143 267 +948 233 245 240 262 +949 110 113 114 285 +950 153 154 152 264 +951 126 135 106 254 +952 193 184 200 251 +953 74 72 206 265 +954 113 124 130 256 +955 117 126 111 266 +956 206 225 212 265 +957 179 202 192 270 +958 83 84 99 272 +959 160 138 118 276 +960 75 265 260 283 +961 138 121 118 276 +962 195 204 259 268 +963 72 218 97 224 +964 108 130 124 256 +965 100 198 70 268 +966 79 77 80 278 +967 74 68 75 265 +968 257 248 278 284 +969 80 81 79 257 +970 75 260 76 278 +971 78 79 275 283 +972 136 109 127 252 +973 250 260 259 268 +974 232 112 29 110 +975 209 222 91 268 +976 79 78 77 283 +977 155 139 256 282 +978 243 141 237 274 +979 174 269 159 270 +980 173 271 254 276 +981 238 86 69 279 +982 79 83 95 275 +983 247 265 252 283 +984 171 155 139 256 +985 78 275 101 283 +986 259 278 248 284 +987 136 135 117 266 +988 231 232 29 110 +989 99 103 71 253 +990 235 251 230 274 +991 180 255 205 273 +992 213 225 216 260 +993 237 228 240 262 +994 105 73 69 251 +995 209 19 212 68 +996 222 198 100 268 +997 95 263 88 275 +998 246 274 262 281 +999 209 260 213 268 +1000 79 257 81 272 +1001 227 215 216 261 +1002 194 205 180 255 +1003 187 192 202 277 +1004 19 209 91 68 +1005 238 230 235 251 +1006 83 272 99 285 +1007 136 117 125 266 +1008 248 269 250 270 +1009 85 101 252 283 +1010 193 185 184 251 +1011 175 143 158 262 +1012 70 91 100 268 +1013 160 276 118 282 +1014 65 87 93 259 +1015 240 245 235 274 +1016 177 166 154 255 +1017 216 226 213 280 +1018 18 212 74 206 +1019 129 88 67 263 +1020 231 239 233 253 +1021 252 265 85 283 +1022 148 147 149 269 +1023 114 92 131 285 +1024 140 261 147 280 +1025 116 219 211 266 +1026 246 272 253 279 +1027 247 275 257 283 +1028 155 153 264 281 +1029 167 271 173 276 +1030 140 146 147 261 +1031 188 197 189 273 +1032 119 66 12 88 +1033 18 212 68 74 +1034 116 125 117 266 +1035 102 91 70 268 +1036 236 233 240 262 +1037 83 263 95 275 +1038 218 227 206 265 +1039 52 146 140 207 +1040 33 65 190 178 +1041 252 275 247 283 +1042 107 88 129 263 +1043 247 260 265 283 +1044 217 277 226 280 +1045 220 213 210 268 +1046 138 45 106 162 +1047 146 52 214 207 +1048 188 189 270 273 +1049 247 257 278 283 +1050 83 263 272 285 +1051 178 190 191 258 +1052 237 262 240 274 +1053 227 216 225 265 +1054 106 44 126 162 +1055 160 167 276 282 +1056 176 137 270 273 +1057 11 90 66 109 +1058 162 254 157 286 +1059 87 259 65 284 +1060 33 94 65 178 +1061 32 193 94 178 +1062 154 255 166 273 +1063 82 94 105 258 +1064 116 117 111 266 +1065 10 90 109 125 +1066 221 144 208 286 +1067 56 228 158 143 +1068 106 121 45 138 +1069 132 169 221 286 +1070 60 192 142 165 +1071 254 261 157 286 +1072 128 122 120 276 +1073 207 53 217 280 +1074 217 163 277 280 +1075 175 158 161 262 +1076 174 148 159 269 +1077 32 94 193 73 +1078 195 70 198 268 +1079 130 256 108 282 +1080 119 11 66 109 +1081 164 139 48 124 +1082 246 262 256 281 +1083 180 166 63 194 +1084 228 56 236 143 +1085 121 46 138 118 +1086 76 68 91 260 +1087 160 167 138 276 +1088 198 222 183 268 +1089 60 179 192 165 +1090 185 205 194 255 +1091 107 119 12 88 +1092 78 95 101 275 +1093 71 99 253 285 +1094 35 70 195 93 +1095 181 35 195 93 +1096 161 175 262 281 +1097 146 261 214 286 +1098 140 53 207 280 +1099 261 266 215 286 +1100 160 138 46 118 +1101 76 75 68 260 +1102 216 261 207 280 +1103 145 63 166 194 +1104 106 254 122 276 +1105 235 230 234 274 +1106 155 153 151 264 +1107 137 179 165 270 +1108 209 212 213 260 +1109 138 167 173 276 +1110 149 150 151 271 +1111 72 74 85 265 +1112 214 207 216 261 +1113 75 76 77 278 +1114 126 169 132 286 +1115 75 278 77 283 +1116 108 48 139 124 +1117 179 137 188 270 +1118 247 278 260 283 +1119 240 228 236 262 +1120 219 221 208 286 +1121 144 146 214 286 +1122 137 159 176 270 +1123 146 157 147 261 +1124 218 211 227 266 +1125 245 233 239 253 +1126 106 135 122 254 +1127 155 156 153 281 +1128 188 270 137 273 +1129 84 272 89 279 +1130 254 266 261 286 +1131 41 183 220 186 +1132 159 149 152 269 +1133 181 204 196 259 +1134 214 261 215 286 +1135 209 213 220 268 +1136 93 87 102 259 +1137 67 129 13 88 +1138 243 237 234 274 +1139 129 263 67 285 +1140 234 182 39 243 +1141 118 276 128 282 +1142 208 144 214 286 +1143 169 144 221 286 +1144 173 157 162 254 +1145 157 261 146 286 +1146 218 17 72 206 +1147 199 39 182 243 +1148 85 265 75 283 +1149 129 107 13 88 +1150 95 83 88 263 +1151 165 61 179 137 +1152 81 89 84 272 +1153 93 181 34 65 +1154 215 214 216 261 +1155 85 74 75 265 +1156 220 183 222 268 +1157 95 78 79 275 +1158 153 161 154 281 +1159 172 223 217 277 +1160 124 113 112 267 +1161 17 74 72 206 +1162 210 226 217 277 +1163 115 123 120 263 +1164 236 232 233 267 +1165 178 191 203 258 +1166 61 188 179 137 +1167 121 106 122 276 +1168 144 208 214 51 +1169 170 241 143 267 +1170 104 258 82 284 +1171 190 34 181 65 +1172 219 215 266 286 +1173 44 169 126 162 +1174 164 134 170 267 +1175 212 225 213 260 +1176 162 157 169 286 +1177 94 82 104 258 +1178 139 47 108 282 +1179 87 278 259 284 +1180 160 47 139 282 +1181 186 192 187 277 +1182 172 217 163 277 +1183 62 180 188 273 +1184 158 57 237 228 +1185 112 232 241 267 +1186 87 77 76 278 +1187 47 118 108 282 +1188 186 223 201 277 +1189 90 10 97 125 +1190 83 67 263 285 +1191 47 160 118 282 +1192 164 143 171 267 +1193 71 92 99 285 +1194 140 147 148 280 +1195 160 155 167 282 +1196 83 79 81 272 +1197 214 144 51 146 +1198 159 148 149 269 +1199 164 124 134 267 +1200 167 150 173 271 +1201 241 134 112 267 +1202 166 180 62 273 +1203 62 188 137 273 +1204 172 54 217 223 +1205 219 208 215 286 +1206 83 99 67 285 +1207 118 128 123 282 +1208 85 78 101 283 +1209 209 220 222 268 +1210 143 241 236 267 +1211 137 166 62 273 +1212 220 41 186 210 +1213 128 118 121 276 +1214 218 72 16 224 +1215 54 217 163 172 +1216 80 104 82 284 +1217 237 141 57 158 +1218 26 229 86 238 +1219 189 179 188 270 +1220 88 83 67 263 +1221 89 69 86 279 +1222 186 210 223 277 +1223 113 115 114 285 +1224 151 153 152 264 +1225 148 163 140 280 +1226 195 187 204 268 +1227 22 116 111 219 +1228 104 87 65 284 +1229 208 221 50 144 +1230 27 244 69 96 +1231 28 231 110 131 +1232 241 55 170 143 +1233 163 174 142 277 +1234 180 205 197 273 +1235 242 24 71 98 +1236 129 115 263 285 +1237 142 201 172 277 +1238 161 156 175 281 +1239 167 151 150 271 +1240 107 115 120 263 +1241 103 84 89 279 +1242 208 214 215 286 +1243 216 207 226 280 +1244 131 28 231 242 +1245 115 107 129 263 +1246 179 189 202 270 +1247 170 134 241 267 +1248 30 241 112 232 +1249 97 16 72 224 +1250 238 86 26 69 +1251 198 183 195 268 +1252 144 157 146 286 +1253 239 24 71 242 +1254 221 169 50 144 +1255 141 168 243 58 +1256 30 241 134 112 +1257 137 165 159 270 +1258 67 14 129 285 +1259 207 217 226 280 +1260 237 240 235 274 +1261 55 241 236 143 +1262 223 201 42 186 +1263 215 211 219 266 +1264 201 142 192 277 +1265 174 159 165 270 +1266 78 75 77 283 +1267 40 198 222 183 +1268 225 206 227 265 +1269 116 22 211 219 +1270 143 164 170 267 +1271 123 130 108 282 +1272 195 183 187 268 +1273 103 89 86 279 +1274 132 221 169 43 +1275 67 92 14 285 +1276 36 70 100 198 +1277 155 160 139 282 +1278 243 237 141 58 +1279 83 81 84 272 +1280 224 97 9 133 +1281 27 244 238 69 +1282 142 59 172 201 +1283 227 211 215 266 +1284 14 114 129 285 +1285 170 164 49 134 +1286 223 172 201 277 +1287 78 85 75 283 +1288 186 42 223 210 +1289 124 49 164 134 +1290 14 92 114 285 +1291 118 123 108 282 +1292 237 235 234 274 +1293 124 112 134 267 +1294 39 182 234 38 +1295 145 199 168 64 +1296 137 176 166 273 +1297 230 38 184 37 +1298 74 17 18 206 +1299 122 128 121 276 +1300 142 59 201 192 +1301 220 40 222 183 +1302 15 131 92 98 +1303 188 180 197 273 +1304 70 36 195 198 +1305 96 200 73 31 +1306 25 24 71 239 +1307 29 28 231 110 +1308 243 199 8 168 +1309 131 114 15 92 +1310 154 166 176 273 +1311 200 244 3 96 +1312 241 232 236 267 +1313 129 114 115 285 +1314 236 56 55 143 +1315 163 142 172 277 +1316 221 132 5 43 +1317 52 214 51 146 +1318 209 20 19 91 +1319 33 32 94 178 +1320 50 169 221 43 +1321 112 29 30 232 +1322 153 156 161 281 +1323 44 106 45 162 +1324 166 63 62 180 +1325 90 11 10 109 +1326 97 9 16 224 +1327 200 193 73 31 +1328 47 160 46 118 +1329 46 45 121 138 +1330 223 210 217 277 +1331 157 144 169 286 +1332 219 111 23 221 +1333 207 140 52 53 +1334 132 23 111 221 +1335 133 21 224 116 +1336 145 194 199 64 +1337 211 224 21 116 +1338 237 141 58 57 +1339 212 19 18 68 +1340 53 217 163 54 +1341 242 131 2 98 +1342 66 12 11 119 +1343 241 170 6 134 +1344 9 1 224 133 +1345 60 61 179 165 +1346 201 192 186 277 +1347 139 48 108 47 +1348 34 35 181 93 +1349 223 172 7 201 +1350 222 4 100 198 +1351 14 15 114 92 +1352 49 164 48 124 +1353 80 87 104 284 +1354 107 12 13 88 +1355 142 59 192 60 +1356 41 42 186 210 +1357 199 8 39 243 +1358 193 32 73 31 +1359 195 36 70 35 +1360 238 26 27 69 +1361 190 34 65 33 +1362 137 61 188 62 +1363 21 22 211 116 +1364 3 37 244 200 +1365 50 221 5 43 +1366 9 1 16 224 +1367 194 145 63 64 +1368 129 13 14 67 +1369 131 2 28 242 +1370 99 92 67 285 +1371 241 6 30 134 +1372 198 222 4 40 +1373 23 22 111 219 +1374 183 220 40 41 +1375 168 199 8 64 +1376 3 200 96 31 +1377 168 8 243 58 +1378 55 170 6 241 +1379 50 208 144 51 +1380 2 24 242 98 +1381 244 27 3 96 +1382 20 4 100 222 +1383 126 132 169 43 +1384 72 16 17 218 +1385 5 23 132 221 +1386 172 59 7 201 +1387 36 100 4 198 +1388 6 170 49 134 +1389 133 97 9 125 +1390 172 7 54 223 +1391 131 15 2 98 +1392 228 158 57 56 +1393 7 223 201 42 +1394 229 25 26 86 +1395 224 1 21 133 +1396 43 169 126 44 +1397 10 9 97 125 +1398 191 259 190 196 +1399 190 259 191 258 +1400 272 256 249 246 +1401 272 249 256 263 +1402 275 276 271 249 +1403 271 276 275 252 +1404 264 282 167 155 +1405 167 282 264 249 +1406 275 127 276 120 +1407 275 276 127 252 +1408 273 152 270 176 +1409 273 270 152 248 +1410 274 177 145 141 +1411 274 145 177 255 +1412 239 86 25 229 +1413 25 86 239 71 +1414 285 263 113 115 +1415 285 113 263 256 +1416 130 113 263 115 +1417 130 263 113 256 +1418 262 171 281 175 +1419 262 281 171 256 +1420 178 65 258 94 +1421 178 258 65 190 +1422 281 177 274 161 +1423 281 274 177 255 +1424 174 280 163 148 +1425 163 280 174 277 +1426 268 76 259 102 +1427 268 259 76 260 +1428 278 76 259 260 +1429 116 224 125 211 +1430 116 125 224 133 +1431 281 171 155 156 +1432 281 155 171 256 +1433 230 200 37 184 +1434 37 200 230 244 +1435 143 228 262 236 +1436 143 262 228 158 +1437 105 279 69 89 +1438 69 279 105 251 +1439 286 162 126 169 +1440 286 126 162 254 +1441 280 53 163 140 +1442 280 163 53 217 +1443 278 80 87 77 +1444 278 87 80 284 +1445 222 91 20 209 +1446 20 91 222 100 +1447 76 259 87 278 +1448 76 87 259 102 +$EndElements From 82822c3ad153f6331c494b3ec7668e157e4d181a Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 11 May 2026 14:12:06 +0200 Subject: [PATCH 02/40] Added RAVES dependency --- modart_method/pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index 42c4e6f..13a647f 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -41,6 +41,8 @@ dependencies = [ "tqdm>=4.67.1", + "raves @ git+https://github.com/IoSR-Surrey/MoD-ART.git@eb047d21ddf406859ea51b74e9d47e5dfe21365a", + "requests", "gmsh", ] From e1bfb9dea13259d661f32be616cd479ea899d746 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 11 May 2026 14:12:29 +0200 Subject: [PATCH 03/40] It works! --- modart_method/modart_interface/modart_interface.py | 2 ++ modart_method/tests/test_modart_cli.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index af23201..f9a1f58 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -41,6 +41,8 @@ def _modart_method(self, json_file_path: str | Path) -> None: with open(json_file_path, "r") as json_file: result_container = json.load(json_file) + print('\tDEBUG MESSAGE: running _modart_method') + # TODO: Implement your simulation logic here # 1. Extract simulation parameters from result_container # 2. Run your simulation diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index cd09e54..cf6fc8a 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -10,6 +10,9 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): """Test the MoDART method CLI.""" # Set JSON_PATH environment variable and call main() directly os.environ["JSON_PATH"] = create_temporary_input_file + + print('\tDEBUG MESSAGE: running test_modart_method_cli') + main() with open(create_temporary_input_file, 'r') as f: From c06459341b58955266379f28b3fdd7b2143e2372 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 11 May 2026 15:27:06 +0200 Subject: [PATCH 04/40] Wavefront conversion work, but materials are not included --- .../modart_interface/modart_interface.py | 48 ++++++++++++++++++- modart_method/tests/test_modart_cli.py | 4 +- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index f9a1f58..0735f8d 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -2,6 +2,7 @@ """ import json from pathlib import Path +from pprint import pprint from .definition import SimulationMethod @@ -28,6 +29,39 @@ def run_simulation(self) -> None: json_file_path : str | Path | None, optional Path to the JSON file. If not provided, uses the path from initialization. """ + # Load the input JSON file + with open(self.input_json_path, "r") as json_file: + result_container = json.load(json_file) + + temp_subfolder = Path(result_container['msh_path']).parent / 'MoDART_data' + result_container['MoDART_data_subfolder'] = str(temp_subfolder) + + print('\n\tDEBUG MESSAGE: creating temp subfolder:') + print('\t', temp_subfolder, '\n') + if not Path.is_dir(temp_subfolder): + Path.mkdir(temp_subfolder) + + obj_path = str(temp_subfolder / 'mesh.obj') + print('\n\tDEBUG MESSAGE: will write to temp file:') + print('\t', obj_path, '\n') + + import gmsh + gmsh.initialize() + try: + print('\n\tDEBUG MESSAGE: converting mesh to Wavefront format; step 1: load .msh\n') + + gmsh.open(result_container['msh_path']) + + print('\n\tDEBUG MESSAGE: converting mesh to Wavefront format; step 2: save .obj\n') + + gmsh.write(obj_path) + finally: + gmsh.finalize() + + # Save the updated JSON (with the added MoDART_data_subfolder field) + with open(self.input_json_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + self._modart_method(self.input_json_path) def _modart_method(self, json_file_path: str | Path) -> None: @@ -41,7 +75,17 @@ def _modart_method(self, json_file_path: str | Path) -> None: with open(json_file_path, "r") as json_file: result_container = json.load(json_file) - print('\tDEBUG MESSAGE: running _modart_method') + print('\n\tDEBUG MESSAGE: starting _modart_method\n') + + obj_path = str(Path(result_container['MoDART_data_subfolder']) / 'mesh.obj') + + print('\n\tDEBUG MESSAGE: reading .obj file at path:') + print('\t', obj_path, '\n') + + with open(obj_path, "r") as obj_file: + for line in obj_file: + print(line[:-1]) + print() # TODO: Implement your simulation logic here # 1. Extract simulation parameters from result_container @@ -68,6 +112,8 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Write results back to JSON # result_container["results"][0]["responses"][0]["receiverResults"] = results.tolist() + print('\n\tDEBUG MESSAGE: ending _modart_method\n') + # Save the updated JSON with open(json_file_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index cf6fc8a..07906f5 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -11,13 +11,13 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): # Set JSON_PATH environment variable and call main() directly os.environ["JSON_PATH"] = create_temporary_input_file - print('\tDEBUG MESSAGE: running test_modart_method_cli') - main() with open(create_temporary_input_file, 'r') as f: data = json.load(f) + print('\n\tDEBUG MESSAGE: assessing modart_method results\n') + # TODO: Add assertions specific to your simulation method # For example, check that results were written to the JSON file # assert "receiverResults" in data['results'][0]['responses'][0] From 165075f0f7997ce7176fcb19e42f4799d756d77c Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 11 May 2026 16:27:53 +0200 Subject: [PATCH 05/40] Test file didn't have materials. That wasn't the (only) problem. --- .../modart_interface/modart_interface.py | 8 + modart_method/tests/test_room_modart.msh | 3109 ++++++----------- 2 files changed, 1019 insertions(+), 2098 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 0735f8d..c9cfd96 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -36,6 +36,14 @@ def run_simulation(self) -> None: temp_subfolder = Path(result_container['msh_path']).parent / 'MoDART_data' result_container['MoDART_data_subfolder'] = str(temp_subfolder) + print('\n\tDEBUG MESSAGE: reading .msh file at path:') + print('\t', result_container['msh_path'], '\n') + + with open(result_container['msh_path'], "r") as msh_file: + for line in msh_file: + print(line[:-1]) + print() + print('\n\tDEBUG MESSAGE: creating temp subfolder:') print('\t', temp_subfolder, '\n') if not Path.is_dir(temp_subfolder): diff --git a/modart_method/tests/test_room_modart.msh b/modart_method/tests/test_room_modart.msh index bbcdc1f..e575648 100644 --- a/modart_method/tests/test_room_modart.msh +++ b/modart_method/tests/test_room_modart.msh @@ -1,5 +1,5 @@ $MeshFormat -4.1 0 8 +2.2 0 8 $EndMeshFormat $PhysicalNames 8 @@ -12,2105 +12,1018 @@ $PhysicalNames 2 6 "wall4" 3 7 "RoomVolume" $EndPhysicalNames -$Entities -8 12 6 1 -1 0 5.1 0 0 -2 6.21 4 0 0 -3 5.52 0 0 0 -4 0 0 0 0 -5 0 5.1 3.3 0 -6 6.21 4 3.3 0 -7 0 0 3.3 0 -8 5.52 0 3.3 0 -1 0 4 0 6.21 5.1 0 1 8 2 1 -2 -2 0 0 0 0 5.1 0 1 8 2 1 -4 -3 0 5.1 0 0 5.1 3.3 1 8 2 1 -5 -4 5.52 0 0 6.21 4 0 1 8 2 2 -3 -5 6.21 4 0 6.21 4 3.3 1 8 2 2 -6 -6 0 0 0 5.52 0 0 1 8 2 3 -4 -7 5.52 0 0 5.52 0 3.3 1 8 2 3 -8 -8 0 0 0 0 0 3.3 1 8 2 4 -7 -9 0 4 3.3 6.21 5.1 3.3 1 8 2 5 -6 -10 0 0 3.3 0 5.1 3.3 1 8 2 5 -7 -11 5.52 0 3.3 6.21 4 3.3 1 8 2 6 -8 -12 0 0 3.3 5.52 0 3.3 1 8 2 7 -8 -1 0 0 0 6.21 5.1 0 1 1 4 6 -2 1 4 -2 0 4 0 6.21 5.1 3.3 1 2 4 -1 3 9 -5 -3 0 0 3.3 6.21 5.1 3.3 1 3 4 -9 10 12 -11 -4 0 0 0 5.52 0 3.3 1 4 4 -6 7 -12 -8 -5 0 0 0 0 5.1 3.3 1 5 4 2 8 -10 -3 -6 5.52 0 0 6.21 4 3.3 1 6 4 11 -7 -4 5 -1 0 0 0 6.21 5.1 3.3 1 7 6 1 2 3 4 5 6 -$EndEntities $Nodes -27 286 1 286 -0 1 0 1 -1 -0 5.1 0 -0 2 0 1 -2 -6.21 4 0 -0 3 0 1 -3 -5.52 0 0 -0 4 0 1 -4 -0 0 0 -0 5 0 1 -5 -0 5.1 3.3 -0 6 0 1 -6 -6.21 4 3.3 -0 7 0 1 -7 -0 0 3.3 -0 8 0 1 -8 -5.52 0 3.3 -1 1 0 7 -9 -10 -11 -12 -13 -14 -15 -0.7762499999987194 4.962500000000227 0 -1.552499999996365 4.825000000000643 0 -2.32874999999396 4.687500000001069 0 -3.104999999991609 4.550000000001486 0 -3.881249999993475 4.412500000001156 0 -4.657499999996126 4.275000000000686 0 -5.433749999997627 4.13750000000042 0 -1 2 0 5 -16 -17 -18 -19 -20 -0 4.25000000000211 0 -0 3.400000000004221 0 -0 2.550000000006418 0 -0 1.70000000000445 0 -0 0.8500000000019954 0 -1 3 0 3 -21 -22 -23 -0 5.1 0.8249999999980804 -0 5.1 1.649999999995743 -0 5.1 2.474999999997828 -1 4 0 4 -24 -25 -26 -27 -6.072000000000003 3.200000000000018 0 -5.93400000000094 2.400000000005448 0 -5.79600000000112 1.600000000006494 0 -5.658000000000555 0.8000000000032195 0 -1 5 0 3 -28 -29 -30 -6.21 4 0.8249999999980804 -6.21 4 1.649999999995743 -6.21 4 2.474999999997828 -1 6 0 6 -31 -32 -33 -34 -35 -36 -4.731428571430031 0 0 -3.942857142860572 0 0 -3.154285714291769 0 0 -2.365714285720427 0 0 -1.577142857146942 0 0 -0.7885714285734089 0 0 -1 7 0 3 -37 -38 -39 -5.52 0 0.8249999999980804 -5.52 0 1.649999999995743 -5.52 0 2.474999999997828 -1 8 0 3 -40 -41 -42 -0 0 0.8249999999980804 -0 0 1.649999999995743 -0 0 2.474999999997828 -1 9 0 7 -43 -44 -45 -46 -47 -48 -49 -0.7762499999987194 4.962500000000227 3.3 -1.552499999996365 4.825000000000643 3.3 -2.32874999999396 4.687500000001069 3.3 -3.104999999991609 4.550000000001486 3.3 -3.881249999993475 4.412500000001156 3.3 -4.657499999996126 4.275000000000686 3.3 -5.433749999997627 4.13750000000042 3.3 -1 10 0 5 -50 -51 -52 -53 -54 -0 4.25000000000211 3.3 -0 3.400000000004221 3.3 -0 2.550000000006418 3.3 -0 1.70000000000445 3.3 -0 0.8500000000019954 3.3 -1 11 0 4 -55 -56 -57 -58 -6.072000000000003 3.200000000000018 3.3 -5.93400000000094 2.400000000005448 3.3 -5.79600000000112 1.600000000006494 3.3 -5.658000000000555 0.8000000000032195 3.3 -1 12 0 6 -59 -60 -61 -62 -63 -64 -0.7885714285697485 0 3.3 -1.577142857138913 0 3.3 -2.365714285708011 0 3.3 -3.154285714279408 0 3.3 -3.942857142852948 0 3.3 -4.731428571426536 0 3.3 -2 1 0 41 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -2.702246367247137 0.6278844212046903 0 -2.606042587526941 3.994689295288387 0 -4.135501471387311 3.688925114670039 0 -0.7255309746239729 2.136165699760757 0 -4.902674462051395 1.252630494334912 0 -1.20047813245132 0.6104954181138812 0 -5.269159873184588 2.92986357870479 0 -0.6515713233657827 3.679057086664729 0 -4.373071687051047 0.6141996761085335 0 -0.713879408774184 2.951516656175773 0 -1.445202806081749 2.549401318616725 0 -1.4661307299318 1.769306549349747 0 -2.160856254496739 2.13997549312561 0 -2.153388815033426 2.910769260569127 0 -2.858826860697639 2.527420539366687 0 -2.883103284483341 1.78179324994803 0 -3.555295546448354 2.151167921494717 0 -3.55538599452136 1.347703574723207 0 -3.642404054441497 3.018695082077782 0 -4.143451559114237 2.528430844542227 0 -1.445042169812366 3.407415605569577 0 -5.161746439014879 2.08728091321036 0 -2.255048869344842 1.323748656822932 0 -3.374880227047213 3.823234457155831 0 -4.298587786259919 1.843190706691889 0 -1.830781621157491 4.101449621592511 0 -0.7683875600501459 1.274723232763478 0 -4.91237749292892 3.6129436942932 0 -1.971428571433684 0.6229082830041432 0 -3.548622212624968 0.7121086994479576 0 -2.79381059071493 3.305830370074237 0 -5.019055347763423 0.5854986388992025 0 -1.051585836382507 4.255182029687647 0 -5.540980365877392 3.531097221289941 0 -4.488329658859804 3.061315687642965 0 -0.5584260501118248 0.5651366727068572 0 -2.127320999542864 3.560173585987611 0 -1.577361304461205 1.127741821563272 0 -4.672255063286686 2.490016346158446 0 -3.0012588298784 1.181109857586796 0 -4.135668428501737 1.1539666302613 0 -2 2 0 31 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -1.983496193715434 4.748656068746058 2.642353555724199 -3.524861074614583 4.475628473095646 0.6469532362317842 -4.170689869483857 4.36123045790141 2.617641854477065 -1.981024688072751 4.749093855574875 0.6368379189250895 -5.529889823872385 4.120470401568499 1.272491748558482 -0.6444333288960226 4.985849168794585 2.05927767325974 -5.570072844942467 4.11335263616156 2.064242586280364 -4.893356454539051 4.233221884059105 1.674949874422897 -4.924879178751135 4.227638148691425 0.7701082756560119 -4.135560251077941 4.367453095622265 1.249948278105242 -0.6343656858248634 4.987632487212987 1.24104712499307 -1.20202905091593 4.88708020032085 1.649139535744951 -3.432667366830957 4.491959081559734 2.622829972307688 -2.728897627109878 4.616620388112582 0.6492714764780272 -3.10499999999319 4.550000000001206 1.365434340236661 -2.703261743501124 4.621161365885469 2.638722571453489 -2.343935826619011 4.684810079020787 1.922301438399384 -3.688841588058679 4.446582005335822 1.925311476061128 -4.96989686064763 4.219664002139711 2.553109812715744 -1.202302782053134 4.887031713323921 0.7163966354029625 -1.210159247866509 4.885640068815916 2.561144913134303 -2.385376420429541 4.677469555157408 1.159758465332143 -3.054741305000592 4.558902506360604 2.094919959691671 -4.224810100886651 4.351643943482236 0.5334019579986077 -4.312565322594148 4.336099540281229 1.991227115787539 -5.618616525137388 4.104753916642331 0.6097483995752218 -0.5776833462350431 4.997672837220845 2.70354663497231 -0.5762063749332704 4.997934458546442 0.5950200657474403 -5.63404965774177 4.102020189449928 2.705228396391119 -1.723999555027303 4.794621656919479 2.019411175964083 -1.795467360471973 4.78196230329804 1.314728373340517 -2 3 0 41 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -2.702246367239102 0.627884421204691 3.3 -2.606042587526935 3.994689295288329 3.3 -4.135501471387224 3.688925114669808 3.3 -0.725530974623923 2.136165699760805 3.3 -4.902674462049536 1.252630494335985 3.3 -1.200478132446673 0.610495418113386 3.3 -5.269159873184508 2.929863578704726 3.3 -0.6515713233657568 3.679057086664713 3.3 -4.373071687046156 0.6141996761093222 3.3 -0.7138794087741396 2.951516656175771 3.3 -1.445202806081554 2.549401318616729 3.3 -1.466130729931171 1.769306549349424 3.3 -2.160856254496093 2.13997549312568 3.3 -2.15338881503317 2.910769260569019 3.3 -2.858826860697112 2.527420539366371 3.3 -2.883103284482265 1.781793249947509 3.3 -3.555295546447529 2.151167921494006 3.3 -3.555385994520122 1.347703574722567 3.3 -3.64240405444118 3.018695082077328 3.3 -4.143451559113692 2.528430844541685 3.3 -1.445042169812269 3.407415605569536 3.3 -5.161746439014308 2.087280913210382 3.3 -2.255048869341225 1.323748656822352 3.3 -3.374880227047138 3.823234457155672 3.3 -4.298587786258741 1.843190706691747 3.3 -1.830781621157481 4.101449621592469 3.3 -0.7683875600489698 1.274723232762874 3.3 -4.912377492928875 3.61294369429304 3.3 -1.971428571423462 0.6229082830067182 3.3 -3.548622212619272 0.7121086994479118 3.3 -2.793810590714735 3.30583037007404 3.3 -5.019055347760613 0.5854986389009575 3.3 -1.051585836382491 4.255182029687636 3.3 -5.540980365877386 3.531097221289929 3.3 -4.488329658859553 3.061315687642745 3.3 -0.5584260501090383 0.5651366727055279 3.3 -2.127320999542876 3.560173585987518 3.3 -1.577361304458576 1.127741821560872 3.3 -4.672255063286161 2.490016346158257 3.3 -3.00125882987532 1.181109857585027 3.3 -4.135668428498765 1.153966630261506 3.3 -2 4 0 28 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -3.505459795894285 0 0.658032377106032 -2.01454020410447 0 2.641967622893278 -3.507053838499056 0 2.663309771093026 -2.012946161501398 0 0.6366902289065148 -4.865401979309599 0 2.05921375528795 -0.6545980206904399 0 1.24078624470752 -4.875602516131682 0 1.241094456535543 -4.298871526490614 0 1.651488430344355 -0.6443974838671406 0 2.058905543460367 -1.221128473508714 0 1.648511569653169 -2.760265673763017 0 2.648836058929298 -2.384072622735947 0 1.92338884153312 -2.75973432623673 0 0.6511639410704287 -3.136610066657712 0 1.376210827548336 -1.22738113669884 0 2.560606383438317 -4.292618863299248 0 0.7393936165603563 -4.296350786159673 0 2.584228126142046 -1.223649213841326 0 0.715771873856837 -2.417904882314414 0 1.162541000761508 -3.102368193443461 0 2.137298866870632 -0.5869333018814119 0 0.5966410707398072 -4.933066698120168 0 2.703358929260585 -4.934555941642836 0 0.5952175860213638 -0.5854440583546006 0 2.704782413979891 -1.751447634802106 0 2.019299358032363 -3.768649892252999 0 1.280643451835007 -1.824039994209592 0 1.315095912873981 -3.696085397720391 0 1.98483055695618 -2 5 0 22 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -0 2.975000000005319 0.7361215932148703 -0 2.133874682074826 2.613113855916287 -0 3.817679962353616 2.61760765757847 -0 1.281886275606401 0.6820170777442269 -0 0.8612986076400723 2.035771238118329 -0 4.238698811323031 1.264060753882836 -0 2.125794453959565 0.6807297793938323 -0 1.690398848941836 1.432404491573377 -0 2.9752591074082 2.582039386289344 -0 3.563364762682421 1.92085905061594 -0 2.57296682209647 1.863891573970136 -0 1.309795077452145 2.674067092312445 -0 3.788013766143618 0.6555925894110306 -0 4.405657901755993 2.027635791990685 -0 0.7191813219154064 1.239127261499417 -0 4.47542187234981 2.681874050298682 -0 0.6245781276490048 0.6181259496979481 -0 0.6048716833449455 2.740832855243506 -0 4.49481492559346 0.5633802410014497 -0 2.551410583650405 1.18354020885267 -0 1.713666807641069 2.123849650378115 -0 3.281575790983544 1.27067762832458 -2 6 0 18 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -5.86504015491614 2.000232782122551 2.753274856069659 -5.867830910298883 2.016411074196425 0.5360075278748611 -5.637260150785848 0.6797689900628874 1.241778785205294 -6.09276160492423 3.320357129995534 1.241630517347858 -6.093189791309344 3.322839369909241 2.057600205069194 -5.987287937489962 2.708915579651955 1.640940440356776 -5.636903658445092 0.6777023677976404 2.058585057068526 -5.74749001496069 1.318782695424291 1.641218812814056 -5.992924077649551 2.741588855939429 2.570447607394167 -5.737509580613327 1.260925105004799 2.570866003043504 -5.73608444090115 1.252663425513916 0.7028856333450937 -5.992997453982559 2.742014225985852 0.7398057616850354 -5.866671077855663 2.009687407858918 2.007813954164472 -6.107400876806572 3.405222474240992 2.6930046065353 -6.107550781131361 3.406091484819482 0.6061276416337821 -5.622449218869113 0.5939085151832642 2.693872358363222 -5.616606147060705 0.5600356351345229 0.5688456716878525 -5.875832781331978 2.062798732359293 1.289268727873359 -3 1 0 41 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -3.982748395016723 2.131866902494136 1.649999999999908 -1.905001571868004 2.522929832411618 1.649999999999942 -2.783809050303439 1.379359660610481 1.894049905953509 -3.105235241577535 3.18471758150762 1.886406261975826 -1.447295988181111 1.305498849181051 1.97708543851478 -4.523319666724902 1.161399387875922 1.153392657073833 -1.87102264981235 3.605615646780535 1.160033125701817 -4.843491511925335 2.86040094787313 1.159889058803643 -1.757757297908341 3.70957514665298 2.261620215216739 -3.974032600267527 1.102001581995885 2.162311504171539 -4.256596207974636 3.167131397587323 2.14716125977807 -2.93509507365237 2.280750044950089 1.189456430932815 -3.42563054064633 1.031436021518221 1.058711176608252 -2.079698879421366 1.066651901090252 1.078148983359485 -1.088438455019048 1.937812665252411 1.078967168932442 -1.046797986343716 2.866301854554071 2.251872303608898 -4.856350181630246 2.280809082357493 2.260543890093401 -3.619199073662395 3.457726940313969 0.9817434735267551 -3.191764896308864 2.239857304993485 2.324304096386526 -1.021657408142361 2.991891967645576 1.011780356152358 -0.9553033612611703 3.853164463551219 1.604038917597632 -5.185592196431918 3.287766957375741 2.396163653348566 -1.066049658648649 0.9216779577124237 1.115851322635183 -2.111357440549796 1.89801080086422 2.362239546773829 -2.170233150211595 0.9018085990384976 2.423753598319347 -2.325974508817458 2.962854438232422 2.372499034842639 -3.75669844547966 2.484280640661297 0.8322020639774979 -3.171670189850378 0.826084207416367 2.489813911820977 -4.874400316607696 1.364296427022081 2.012209510457316 -2.711173798751009 3.187294244662575 1.023782385116788 -2.634843387789958 3.808749344129123 2.298460373602619 -0.861830452322895 0.8372499722850986 2.48890077077935 -2.096277343598294 1.983359873401345 0.9156131131644956 -4.560917135263096 2.035107710641359 0.8624320557041605 -0.8968064261381468 1.946977374457191 2.393097558816704 -4.033500082767937 2.210167091732712 2.506719542441852 -3.586321646640657 3.639902497162266 2.465755143136407 -1.847800900905321 2.810200118357459 0.8083406404637883 -2.758573749830245 1.445335653420554 0.7596196862816191 -4.507730337136908 3.485203150259042 0.7192062927872336 -0.9071392624803817 3.702990021572242 2.464111603443851 +1 0 5.1 0 +2 6.21 4 0 +3 5.52 0 0 +4 0 0 0 +5 0 5.1 3.3 +6 6.21 4 3.3 +7 0 0 3.3 +8 5.52 0 3.3 +9 1.034999999997969 4.916666666667026 0 +10 2.069999999994763 4.733333333334261 0 +11 3.10499999999161 4.550000000001486 0 +12 4.13999999999402 4.366666666667726 0 +13 5.174999999996958 4.183333333333872 0 +14 0 4.080000000002531 0 +15 0 3.060000000005065 0 +16 0 2.040000000005119 0 +17 0 1.020000000002431 0 +18 0 5.1 1.099999999997243 +19 0 5.1 2.199999999997244 +20 6.037500000000238 3.000000000001378 0 +21 5.865000000001396 2.000000000008094 0 +22 5.692500000000702 1.000000000004076 0 +23 6.21 4 1.099999999997243 +24 6.21 4 2.199999999997244 +25 4.41600000000253 0 0 +26 3.312000000005635 0 0 +27 2.208000000005758 0 0 +28 1.104000000002809 0 0 +29 5.52 0 1.099999999997243 +30 5.52 0 2.199999999997244 +31 0 0 1.099999999997243 +32 0 0 2.199999999997244 +33 1.034999999997969 4.916666666667026 3.3 +34 2.069999999994763 4.733333333334261 3.3 +35 3.10499999999161 4.550000000001486 3.3 +36 4.13999999999402 4.366666666667726 3.3 +37 5.174999999996958 4.183333333333872 3.3 +38 0 4.080000000002531 3.3 +39 0 3.060000000005065 3.3 +40 0 2.040000000005119 3.3 +41 0 1.020000000002431 3.3 +42 6.037500000000238 3.000000000001378 3.3 +43 5.865000000001396 2.000000000008094 3.3 +44 5.692500000000702 1.000000000004076 3.3 +45 1.103999999997441 0 3.3 +46 2.207999999994134 0 3.3 +47 3.311999999994089 0 3.3 +48 4.415999999997114 0 3.3 +49 2.386497302527005 3.730919978475254 0 +50 3.960392510002857 1.083598528600443 0 +51 4.432076326632258 3.3265620645578 0 +52 1.621307515214968 1.01661383527277 0 +53 0.8767230743720327 2.556581258312159 0 +54 1.167343462037464 3.719275428942924 0 +55 4.930220297964123 1.643458680208118 0 +56 2.771878283950587 0.9787751225948951 0 +57 3.251943127621877 1.976195550159624 0 +58 2.283266696299014 1.95502264736377 0 +59 3.404289098754766 3.591640835316118 0 +60 2.78600449478413 2.819098871455047 0 +61 1.826246986058131 2.800155175232311 0 +62 0.7848144778877137 1.563501256170337 0 +63 5.134455819504938 2.617910745983633 0 +64 4.212934756300453 2.240388354699605 0 +65 4.841732618044288 0.7712784380690728 0 +66 5.352673851426799 3.406841797813403 0 +67 0.7270945991708352 0.7238221488829299 0 +68 1.457646886329138 2.020032866844718 0 +69 3.581257103500146 2.793608248960197 0 +70 2.587499999993187 4.641666666667874 0.9102895601555816 +71 5.271974302055746 4.166155920730866 1.649999999997243 +72 1.560681022834067 4.823550865520535 2.469186580995072 +73 3.622499999992815 4.458333333334606 2.389710439841948 +74 3.622499999990019 4.458333333335101 0.9102895601555957 +75 1.560681022834242 4.823550865520504 0.8308134190039077 +76 2.587854931221515 4.641603796401986 2.407959536651757 +77 4.62736533843115 4.280337862757767 0.8493821351554303 +78 4.627365338433152 4.280337862757412 2.450617864843471 +79 0.8901710668505624 4.942320745002315 1.649999999997876 +80 3.104999999991593 4.550000000001489 1.649999999999781 +81 2.06999999999476 4.733333333334262 1.65 +82 4.146117496482412 4.365583052152874 1.649999999998912 +83 5.481978673881598 4.128957078700522 0.7414177774147986 +84 5.481978673882898 4.128957078700292 2.558582222584017 +85 0.6971704179365547 4.976507655437969 0.7161626837998054 +86 0.6971704179365198 4.976507655437976 2.583837316198038 +87 2.386403851037251 3.730380564715313 3.3 +88 3.965775674800577 1.083362816797341 3.3 +89 4.433132327960219 3.326072113895397 3.3 +90 1.627151092599739 1.01548622626109 3.3 +91 0.878936738206633 2.554689649487218 3.3 +92 1.142974245182026 3.742346051007342 3.3 +93 4.931150832522625 1.643333526254781 3.3 +94 2.777474348734183 0.9772965708701882 3.3 +95 3.256887919122461 1.973572264630932 3.3 +96 2.27362056338768 1.93287362800228 3.3 +97 3.404660201724602 3.590565843447886 3.3 +98 2.785534549692484 2.81477332476458 3.3 +99 1.816687742138859 2.844362833128078 3.3 +100 0.787390917524992 1.562956413140717 3.3 +101 5.135713562421214 2.617602212099839 3.3 +102 4.215502143431396 2.239392322736088 3.3 +103 4.841732618043357 0.7712784380679567 3.3 +104 5.352673851426799 3.406841797813403 3.3 +105 0.7178515418486776 0.7220002560822467 3.3 +106 1.465048282934295 2.0197830265027 3.3 +107 3.5830532113542 2.791490597479166 3.3 +108 4.48468980981603 0 1.649999999997916 +109 1.035310190183893 0 1.649999999999388 +110 2.759999999995056 0 2.37960991561047 +111 2.760000000004749 0 0.9203900843894577 +112 3.832171817007044 0 2.462506203432567 +113 1.687828182992578 0 0.8374937965665119 +114 1.687828182988288 0 2.462506203433918 +115 3.832171817011822 0 0.8374937965651579 +116 3.305991422412324 0 1.649999999996311 +117 2.210004029377576 0 1.650000000001565 +118 0.8056503301718059 0 2.495427304989771 +119 4.714349669827654 0 2.495427304990706 +120 4.714349669829664 0 0.8045726950072637 +121 0.805650330170864 0 0.8045726950063466 +122 0 2.550000000005092 2.416654088140993 +123 0 3.567406916949975 0.8676712655163928 +124 0 1.532593083057434 0.8676712655189218 +125 0 1.532593083057383 2.432328734481018 +126 0 3.567406916949724 2.432328734482401 +127 0 2.550000000004911 0.8672664086931592 +128 0 0.858221729039492 1.649999999997243 +129 0 4.241778270962218 1.649999999997244 +130 0 3.060000000004152 1.64999999999982 +131 0 2.040000000005835 1.650000000000142 +132 0 0.7315477981171267 2.544564769018438 +133 0 4.368452201887145 2.544564769021097 +134 0 0.7315477981183018 0.7554352309809245 +135 0 4.368452201886346 0.7554352309779164 +136 5.677733456182633 0.9143968474355568 1.649999999998703 +137 5.951250000000817 2.500000000004736 0.8788157870052324 +138 5.951250000000817 2.500000000004736 2.421184212994766 +139 5.778750000001049 1.500000000006085 2.454721090038176 +140 5.778750000001049 1.500000000006085 0.8452789099591339 +141 6.064307601881952 3.155406387721458 1.649999999997243 +142 5.863855733322866 1.99336656998763 1.649999999999426 +143 6.086483241547053 3.283960820562625 2.547756640661937 +144 5.643030432939424 0.7132199010981142 2.550395885654611 +145 5.643030432939587 0.7132199010990538 0.7496041143445117 +146 6.086483241546856 3.283960820561481 0.7522433593373904 +147 3.185152992809589 2.853842729420941 1.650105251411698 +148 1.848798741753221 1.785272088996948 1.649113242066087 +149 4.294795109466685 1.708491619923346 1.791678167333182 +150 1.753055973400559 3.274722774000739 1.857200121487178 +151 4.538875156952106 3.016527099416517 1.408338929901463 +152 3.094271366453881 1.597958473505834 1.25459524507098 +153 1.08724203698074 1.120888866969477 1.047102140042662 +154 1.082785338776251 1.11517678526899 2.255970139100719 +155 2.310694805362786 2.578141344200029 0.9937414242326503 +156 1.140445927663791 2.572711817267504 1.143520495002259 +157 3.943633811919474 3.384959792017217 2.288888650148183 +158 3.308816216352959 1.028420414186172 2.256405604516595 +159 2.381749043048582 2.488049082549255 2.320657494382369 +160 4.947756758381086 2.155663961686152 0.938396064409417 +161 4.88844102152302 2.469303483107215 2.264694198274357 +162 2.215565106161902 0.9688630562528956 0.9869268335752817 +163 1.032432474645413 3.893322723850346 1.225919309291406 +164 2.895167778353591 3.612966366114015 2.353749188670391 +165 2.942988247029935 3.635370012826987 0.9628371523745898 +166 4.690563675953788 1.123250848348735 0.9941834455913106 +167 2.211611067924777 0.9465777326923647 2.261932054260194 +168 1.074422649597229 2.334800898167823 2.227697036090162 $EndNodes $Elements -19 1448 1 1448 -1 1 1 8 -1 1 9 -2 9 10 -3 10 11 -4 11 12 -5 12 13 -6 13 14 -7 14 15 -8 15 2 -1 2 1 6 -9 1 16 -10 16 17 -11 17 18 -12 18 19 -13 19 20 -14 20 4 -1 3 1 4 -15 1 21 -16 21 22 -17 22 23 -18 23 5 -1 4 1 5 -19 2 24 -20 24 25 -21 25 26 -22 26 27 -23 27 3 -1 5 1 4 -24 2 28 -25 28 29 -26 29 30 -27 30 6 -1 6 1 7 -28 3 31 -29 31 32 -30 32 33 -31 33 34 -32 34 35 -33 35 36 -34 36 4 -1 7 1 4 -35 3 37 -36 37 38 -37 38 39 -38 39 8 -1 8 1 4 -39 4 40 -40 40 41 -41 41 42 -42 42 7 -1 9 1 8 -43 5 43 -44 43 44 -45 44 45 -46 45 46 -47 46 47 -48 47 48 -49 48 49 -50 49 6 -1 10 1 6 -51 5 50 -52 50 51 -53 51 52 -54 52 53 -55 53 54 -56 54 7 -1 11 1 5 -57 6 55 -58 55 56 -59 56 57 -60 57 58 -61 58 8 -1 12 1 7 -62 7 59 -63 59 60 -64 60 61 -65 61 62 -66 62 63 -67 63 64 -68 64 8 -2 1 2 106 -69 16 1 9 -70 15 2 98 -71 2 24 98 -72 27 3 96 -73 3 31 96 -74 4 20 100 -75 36 4 100 -76 9 10 97 -77 16 9 97 -78 10 11 90 -79 10 90 97 -80 11 12 66 -81 11 66 90 -82 12 13 88 -83 66 12 88 -84 13 14 67 -85 13 67 88 -86 14 15 92 -87 67 14 92 -88 92 15 98 -89 17 16 72 -90 72 16 97 -91 18 17 74 -92 17 72 74 -93 19 18 68 -94 68 18 74 -95 20 19 91 -96 19 68 91 -97 20 91 100 -98 24 25 71 -99 24 71 98 -100 25 26 86 -101 71 25 86 -102 26 27 69 -103 26 69 86 -104 69 27 96 -105 31 32 73 -106 31 73 96 -107 32 33 94 -108 73 32 94 -109 33 34 65 -110 33 65 94 -111 34 35 93 -112 65 34 93 -113 35 36 70 -114 35 70 93 -115 70 36 100 -116 87 65 93 -117 65 87 104 -118 94 65 104 -119 66 88 95 -120 90 66 101 -121 66 95 101 -122 67 83 88 -123 83 67 99 -124 67 92 99 -125 68 74 75 -126 68 75 76 -127 68 76 91 -128 73 69 96 -129 69 73 105 -130 86 69 89 -131 89 69 105 -132 91 70 100 -133 70 91 102 -134 93 70 102 -135 71 86 103 -136 71 92 98 -137 92 71 99 -138 99 71 103 -139 74 72 85 -140 85 72 97 -141 73 94 105 -142 75 74 85 -143 76 75 77 -144 77 75 78 -145 78 75 85 -146 76 77 87 -147 76 87 102 -148 91 76 102 -149 77 78 79 -150 77 79 80 -151 77 80 87 -152 79 78 95 -153 78 85 101 -154 95 78 101 -155 80 79 81 -156 81 79 83 -157 83 79 95 -158 80 81 82 -159 80 82 104 -160 87 80 104 -161 82 81 89 -162 81 83 84 -163 81 84 89 -164 82 89 105 -165 82 94 104 -166 94 82 105 -167 84 83 99 -168 88 83 95 -169 89 84 103 -170 84 99 103 -171 90 85 97 -172 85 90 101 -173 86 89 103 -174 87 93 102 -2 2 2 84 -175 1 133 9 -176 21 133 1 -177 15 131 2 -178 2 131 28 -179 5 132 23 -180 43 132 5 -181 30 134 6 -182 6 134 49 -183 9 125 10 -184 9 133 125 -185 10 109 11 -186 10 125 109 -187 11 119 12 -188 109 119 11 -189 12 107 13 -190 12 119 107 -191 13 129 14 -192 107 129 13 -193 14 114 15 -194 14 129 114 -195 114 131 15 -196 22 116 21 -197 116 133 21 -198 23 111 22 -199 111 116 22 -200 23 132 111 -201 28 110 29 -202 28 131 110 -203 29 112 30 -204 110 112 29 -205 112 134 30 -206 44 126 43 -207 126 132 43 -208 45 106 44 -209 106 126 44 -210 46 121 45 -211 45 121 106 -212 47 118 46 -213 118 121 46 -214 48 108 47 -215 108 118 47 -216 49 124 48 -217 48 124 108 -218 49 134 124 -219 121 122 106 -220 122 135 106 -221 106 135 126 -222 107 120 115 -223 115 129 107 -224 119 120 107 -225 108 123 118 -226 108 130 123 -227 124 130 108 -228 109 127 119 -229 125 136 109 -230 109 136 127 -231 110 113 112 -232 110 114 113 -233 110 131 114 -234 111 117 116 -235 111 126 117 -236 111 132 126 -237 113 124 112 -238 124 134 112 -239 114 115 113 -240 115 130 113 -241 113 130 124 -242 114 129 115 -243 120 123 115 -244 123 130 115 -245 117 125 116 -246 125 133 116 -247 117 136 125 -248 126 135 117 -249 135 136 117 -250 118 128 121 -251 123 128 118 -252 119 127 120 -253 120 127 122 -254 122 128 120 -255 120 128 123 -256 121 128 122 -257 127 136 122 -258 122 136 135 -2 3 2 106 -259 43 5 50 -260 49 170 6 -261 6 170 55 -262 7 172 54 -263 59 172 7 -264 58 168 8 -265 8 168 64 -266 43 169 44 -267 50 169 43 -268 44 162 45 -269 44 169 162 -270 45 138 46 -271 45 162 138 -272 46 160 47 -273 138 160 46 -274 47 139 48 -275 47 160 139 -276 48 164 49 -277 139 164 48 -278 164 170 49 -279 51 144 50 -280 144 169 50 -281 52 146 51 -282 51 146 144 -283 53 140 52 -284 140 146 52 -285 54 163 53 -286 53 163 140 -287 54 172 163 -288 55 143 56 -289 55 170 143 -290 56 158 57 -291 143 158 56 -292 57 141 58 -293 57 158 141 -294 141 168 58 -295 60 142 59 -296 142 172 59 -297 61 165 60 -298 60 165 142 -299 62 137 61 -300 137 165 61 -301 63 166 62 -302 62 166 137 -303 64 145 63 -304 145 166 63 -305 64 168 145 -306 159 165 137 -307 137 176 159 -308 166 176 137 -309 138 167 160 -310 162 173 138 -311 138 173 167 -312 139 160 155 -313 155 171 139 -314 139 171 164 -315 140 147 146 -316 140 148 147 -317 140 163 148 -318 145 168 141 -319 141 177 145 -320 158 161 141 -321 161 177 141 -322 163 172 142 -323 142 174 163 -324 165 174 142 -325 143 175 158 -326 143 170 164 -327 164 171 143 -328 171 175 143 -329 146 157 144 -330 157 169 144 -331 145 177 166 -332 147 157 146 -333 148 149 147 -334 149 150 147 -335 150 157 147 -336 148 159 149 -337 148 174 159 -338 163 174 148 -339 149 151 150 -340 149 152 151 -341 149 159 152 -342 151 167 150 -343 150 173 157 -344 167 173 150 -345 152 153 151 -346 153 155 151 -347 155 167 151 -348 152 154 153 -349 152 176 154 -350 159 176 152 -351 154 161 153 -352 153 156 155 -353 153 161 156 -354 154 177 161 -355 154 176 166 -356 166 177 154 -357 156 171 155 -358 160 167 155 -359 161 175 156 -360 156 175 171 -361 162 169 157 -362 157 173 162 -363 158 175 161 -364 159 174 165 -2 4 2 76 -365 3 200 31 -366 37 200 3 -367 36 198 4 -368 4 198 40 -369 42 201 7 -370 7 201 59 -371 8 199 39 -372 64 199 8 -373 31 193 32 -374 31 200 193 -375 32 178 33 -376 32 193 178 -377 33 190 34 -378 178 190 33 -379 34 181 35 -380 34 190 181 -381 35 195 36 -382 181 195 35 -383 195 198 36 -384 38 184 37 -385 184 200 37 -386 39 182 38 -387 182 184 38 -388 39 199 182 -389 40 183 41 -390 40 198 183 -391 41 186 42 -392 183 186 41 -393 186 201 42 -394 59 192 60 -395 59 201 192 -396 60 179 61 -397 60 192 179 -398 61 188 62 -399 179 188 61 -400 62 180 63 -401 62 188 180 -402 63 194 64 -403 180 194 63 -404 194 199 64 -405 178 191 190 -406 178 203 191 -407 193 203 178 -408 179 189 188 -409 179 202 189 -410 192 202 179 -411 188 197 180 -412 180 205 194 -413 197 205 180 -414 190 196 181 -415 181 204 195 -416 196 204 181 -417 182 185 184 -418 182 194 185 -419 182 199 194 -420 183 187 186 -421 183 195 187 -422 183 198 195 -423 185 193 184 -424 193 200 184 -425 185 203 193 -426 194 205 185 -427 185 205 203 -428 187 192 186 -429 192 201 186 -430 187 202 192 -431 195 204 187 -432 187 204 202 -433 189 197 188 -434 189 196 191 -435 191 197 189 -436 189 204 196 -437 202 204 189 -438 191 196 190 -439 191 205 197 -440 203 205 191 -2 5 2 62 -441 1 16 224 -442 21 1 224 -443 20 4 222 -444 4 40 222 -445 5 23 221 -446 50 5 221 -447 42 7 223 -448 7 54 223 -449 16 17 218 -450 16 218 224 -451 17 18 206 -452 17 206 218 -453 18 19 212 -454 206 18 212 -455 19 20 209 -456 19 209 212 -457 209 20 222 -458 22 21 211 -459 211 21 224 -460 23 22 219 -461 22 211 219 -462 23 219 221 -463 40 41 220 -464 40 220 222 -465 41 42 210 -466 41 210 220 -467 210 42 223 -468 51 50 208 -469 208 50 221 -470 52 51 214 -471 51 208 214 -472 53 52 207 -473 207 52 214 -474 54 53 217 -475 53 207 217 -476 54 217 223 -477 206 212 225 -478 218 206 227 -479 206 225 227 -480 207 214 216 -481 207 216 226 -482 217 207 226 -483 214 208 215 -484 215 208 219 -485 219 208 221 -486 212 209 213 -487 213 209 220 -488 220 209 222 -489 210 213 220 -490 213 210 226 -491 217 210 223 -492 210 217 226 -493 211 215 219 -494 215 211 227 -495 218 211 224 -496 211 218 227 -497 212 213 225 -498 213 216 225 -499 216 213 226 -500 214 215 216 -501 216 215 227 -502 225 216 227 -2 6 2 52 -503 24 2 242 -504 2 28 242 -505 3 27 244 -506 37 3 244 -507 30 6 241 -508 6 55 241 -509 8 39 243 -510 58 8 243 -511 25 24 239 -512 239 24 242 -513 26 25 229 -514 229 25 239 -515 27 26 238 -516 26 229 238 -517 27 238 244 -518 28 29 231 -519 28 231 242 -520 29 30 232 -521 231 29 232 -522 232 30 241 -523 38 37 230 -524 230 37 244 -525 39 38 234 -526 38 230 234 -527 39 234 243 -528 55 56 236 -529 55 236 241 -530 56 57 228 -531 56 228 236 -532 57 58 237 -533 228 57 237 -534 237 58 243 -535 236 228 240 -536 228 237 240 -537 238 229 245 -538 229 239 245 -539 234 230 235 -540 235 230 238 -541 238 230 244 -542 231 232 233 -543 231 233 239 -544 231 239 242 -545 233 232 236 -546 236 232 241 -547 233 236 240 -548 239 233 245 -549 233 240 245 -550 234 235 237 -551 234 237 243 -552 237 235 240 -553 235 238 245 -554 240 235 245 -3 1 4 894 -555 168 199 145 274 -556 110 253 231 267 -557 125 224 218 266 -558 105 258 251 279 -559 82 272 258 279 -560 224 211 218 266 -561 232 110 231 267 -562 82 258 105 279 -563 66 88 119 275 -564 145 194 255 274 -565 112 110 232 267 -566 203 255 251 258 -567 145 199 194 274 -568 97 265 252 266 -569 196 248 191 259 -570 90 109 125 252 -571 258 272 246 279 -572 168 243 199 274 -573 218 265 72 266 -574 234 230 184 274 -575 184 182 234 274 -576 113 253 110 267 -577 131 231 110 253 -578 131 98 242 253 -579 230 251 184 274 -580 119 252 66 275 -581 119 127 252 275 -582 258 272 82 284 -583 122 252 127 276 -584 184 251 182 274 -585 224 218 97 125 -586 73 251 105 258 -587 66 109 90 252 -588 131 242 231 253 -589 246 256 253 272 -590 72 265 97 266 -591 119 109 66 252 -592 97 85 252 265 -593 131 98 253 285 -594 125 211 224 266 -595 66 90 101 252 -596 249 264 167 271 -597 89 82 105 279 -598 82 89 272 279 -599 189 248 196 259 -600 81 82 272 284 -601 218 72 97 266 -602 168 145 141 274 -603 185 251 203 255 -604 248 258 191 259 -605 231 253 233 267 -606 218 97 125 266 -607 255 258 205 273 -608 194 199 182 274 -609 251 258 246 279 -610 256 263 253 272 -611 189 191 196 248 -612 194 182 255 274 -613 202 259 250 270 -614 88 263 119 275 -615 123 263 256 282 -616 245 274 262 279 -617 248 246 255 258 -618 71 242 98 253 -619 252 247 261 265 -620 246 258 257 272 -621 247 254 252 261 -622 232 231 233 267 -623 147 269 261 271 -624 66 252 101 275 -625 107 119 88 263 -626 152 176 159 270 -627 97 72 85 265 -628 154 264 255 273 -629 97 252 125 266 -630 264 281 256 282 -631 123 130 256 263 -632 257 272 258 284 -633 204 250 202 259 -634 251 274 235 279 -635 193 251 73 258 -636 152 269 248 270 -637 110 131 253 285 -638 248 259 269 278 -639 246 248 257 258 -640 245 262 253 279 -641 113 256 253 267 -642 249 264 256 282 -643 244 96 200 251 -644 94 73 105 258 -645 250 269 259 278 -646 113 110 112 267 -647 119 127 109 252 -648 205 255 203 258 -649 93 102 70 268 -650 216 261 260 265 -651 249 120 275 276 -652 197 191 248 273 -653 197 205 191 273 -654 234 182 243 274 -655 255 264 154 281 -656 182 199 243 274 -657 205 258 191 273 -658 113 253 256 285 -659 155 281 264 282 -660 190 259 258 284 -661 254 271 252 276 -662 202 189 259 270 -663 252 265 261 266 -664 71 253 98 285 -665 191 258 248 273 -666 246 257 249 272 -667 123 249 263 282 -668 252 261 254 266 -669 216 260 261 280 -670 152 248 264 273 -671 257 264 249 271 -672 116 211 125 266 -673 97 90 125 252 -674 248 255 246 264 -675 184 185 182 251 -676 185 182 251 274 -677 159 269 152 270 -678 248 259 250 269 -679 247 264 257 271 -680 128 276 249 282 -681 251 255 246 258 -682 252 271 247 275 -683 246 256 249 264 -684 226 277 250 280 -685 247 257 264 269 -686 197 191 189 248 -687 133 224 97 125 -688 151 167 264 271 -689 152 264 154 273 -690 256 263 249 282 -691 123 256 130 282 -692 147 261 269 280 -693 93 195 181 259 -694 81 82 89 272 -695 164 139 124 256 -696 128 249 123 282 -697 86 229 253 279 -698 247 261 260 280 -699 230 244 200 251 -700 119 120 127 275 -701 152 154 176 273 -702 109 136 125 252 -703 185 251 255 274 -704 93 70 195 268 -705 250 247 269 278 -706 200 96 73 251 -707 247 257 249 271 -708 94 193 73 258 -709 204 202 189 259 -710 185 255 182 274 -711 136 254 135 266 -712 187 202 204 250 -713 238 251 235 279 -714 155 256 281 282 -715 99 84 103 253 -716 136 125 252 266 -717 86 229 239 253 -718 184 230 200 251 -719 239 242 71 253 -720 260 261 247 265 -721 93 259 102 268 -722 197 248 189 273 -723 261 269 247 271 -724 210 250 226 277 -725 155 151 167 264 -726 103 253 84 279 -727 255 264 248 273 -728 253 262 233 267 -729 203 251 193 258 -730 189 248 259 270 -731 84 253 99 272 -732 152 264 248 269 -733 141 177 161 274 -734 248 269 247 278 -735 168 141 243 274 -736 164 256 124 267 -737 248 264 257 269 -738 247 257 248 278 -739 262 274 246 279 -740 210 268 250 277 -741 250 260 213 280 -742 225 216 260 265 -743 213 260 250 268 -744 245 235 274 279 -745 226 250 213 280 -746 202 270 250 277 -747 247 269 264 271 -748 93 195 259 268 -749 248 258 255 273 -750 250 259 204 268 -751 178 193 94 258 -752 213 250 210 268 -753 247 248 257 269 -754 247 260 250 280 -755 244 69 96 251 -756 190 258 65 284 -757 97 85 90 252 -758 253 272 84 279 -759 86 239 71 253 -760 136 252 254 266 -761 81 272 257 284 -762 38 184 234 230 -763 184 234 182 38 -764 190 65 259 284 -765 256 264 246 281 -766 249 276 167 282 -767 157 147 261 271 -768 239 231 242 253 -769 108 124 139 256 -770 122 254 252 276 -771 120 122 127 276 -772 128 120 249 276 -773 110 253 113 285 -774 120 123 128 249 -775 252 254 247 271 -776 136 122 135 254 -777 149 269 147 271 -778 249 271 167 276 -779 262 274 161 281 -780 249 257 246 264 -781 210 213 226 250 -782 171 175 156 281 -783 253 263 256 285 -784 248 246 257 264 -785 192 142 165 270 -786 120 119 263 275 -787 186 268 210 277 -788 71 98 92 285 -789 157 261 254 271 -790 123 115 130 263 -791 86 71 103 253 -792 202 192 270 277 -793 126 106 162 254 -794 131 92 98 285 -795 142 174 270 277 -796 250 277 174 280 -797 95 88 66 275 -798 164 171 139 256 -799 186 183 220 268 -800 185 203 205 255 -801 187 250 204 268 -802 177 255 154 281 -803 247 250 260 278 -804 142 174 165 270 -805 212 260 68 265 -806 212 68 74 265 -807 192 142 270 277 -808 206 212 74 265 -809 175 262 171 267 -810 135 266 254 286 -811 253 262 246 279 -812 136 252 122 254 -813 261 265 227 266 -814 247 269 261 280 -815 247 249 257 275 -816 249 247 271 275 -817 245 253 229 279 -818 157 150 147 271 -819 93 181 65 259 -820 237 158 228 262 -821 194 180 166 255 -822 186 187 268 277 -823 180 166 255 273 -824 162 106 138 276 -825 190 65 181 259 -826 103 86 253 279 -827 158 262 237 274 -828 209 68 212 260 -829 107 120 119 263 -830 91 68 209 260 -831 123 249 120 263 -832 162 138 173 276 -833 205 203 191 258 -834 141 161 262 274 -835 249 120 263 275 -836 257 263 249 272 -837 165 179 192 270 -838 174 250 269 270 -839 150 149 147 271 -840 135 254 126 286 -841 135 126 266 286 -842 246 264 255 281 -843 193 200 73 251 -844 81 257 80 284 -845 164 171 256 267 -846 233 253 245 262 -847 247 250 269 280 -848 166 145 194 255 -849 256 262 253 267 -850 110 114 131 285 -851 148 250 174 280 -852 187 202 250 277 -853 240 262 245 274 -854 257 79 278 283 -855 141 262 158 274 -856 171 262 256 267 -857 245 238 235 279 -858 162 254 106 276 -859 248 270 189 273 -860 106 121 138 276 -861 166 177 145 255 -862 102 76 91 268 -863 162 173 254 276 -864 140 207 146 261 -865 253 256 246 262 -866 174 250 148 269 -867 209 91 260 268 -868 257 275 79 283 -869 79 272 83 275 -870 178 203 193 258 -871 216 213 260 280 -872 140 207 261 280 -873 251 246 274 279 -874 147 269 148 280 -875 227 261 216 265 -876 132 221 111 286 -877 258 259 248 284 -878 146 207 214 261 -879 72 218 206 265 -880 196 190 181 259 -881 253 99 272 285 -882 259 260 250 278 -883 76 260 91 268 -884 127 122 136 252 -885 264 269 151 271 -886 111 221 219 286 -887 79 257 272 275 -888 141 158 237 274 -889 218 227 265 266 -890 257 258 248 284 -891 250 268 187 277 -892 68 260 75 265 -893 117 135 126 266 -894 108 256 139 282 -895 175 143 262 267 -896 86 238 229 279 -897 250 270 174 277 -898 249 263 257 275 -899 246 255 251 274 -900 153 154 264 281 -901 69 73 96 251 -902 157 254 173 271 -903 185 193 203 251 -904 177 154 161 281 -905 254 261 247 271 -906 229 245 239 253 -907 104 65 258 284 -908 111 266 126 286 -909 83 272 263 275 -910 151 264 152 269 -911 195 204 181 259 -912 245 229 238 279 -913 149 151 269 271 -914 250 259 248 270 -915 65 94 104 258 -916 101 275 252 283 -917 219 266 111 286 -918 238 69 251 279 -919 141 161 158 262 -920 204 189 196 259 -921 79 77 278 283 -922 66 101 95 275 -923 186 187 183 268 -924 215 261 227 266 -925 80 257 278 284 -926 149 151 152 269 -927 186 220 210 268 -928 81 80 82 284 -929 124 256 113 267 -930 132 111 126 286 -931 238 69 244 251 -932 246 255 274 281 -933 253 272 263 285 -934 150 157 173 271 -935 116 111 219 266 -936 185 194 182 255 -937 225 260 212 265 -938 236 233 262 267 -939 250 148 269 280 -940 238 244 230 251 -941 79 80 257 278 -942 75 260 278 283 -943 85 101 90 252 -944 222 100 91 268 -945 236 262 143 267 -946 263 272 257 275 -947 175 171 143 267 -948 233 245 240 262 -949 110 113 114 285 -950 153 154 152 264 -951 126 135 106 254 -952 193 184 200 251 -953 74 72 206 265 -954 113 124 130 256 -955 117 126 111 266 -956 206 225 212 265 -957 179 202 192 270 -958 83 84 99 272 -959 160 138 118 276 -960 75 265 260 283 -961 138 121 118 276 -962 195 204 259 268 -963 72 218 97 224 -964 108 130 124 256 -965 100 198 70 268 -966 79 77 80 278 -967 74 68 75 265 -968 257 248 278 284 -969 80 81 79 257 -970 75 260 76 278 -971 78 79 275 283 -972 136 109 127 252 -973 250 260 259 268 -974 232 112 29 110 -975 209 222 91 268 -976 79 78 77 283 -977 155 139 256 282 -978 243 141 237 274 -979 174 269 159 270 -980 173 271 254 276 -981 238 86 69 279 -982 79 83 95 275 -983 247 265 252 283 -984 171 155 139 256 -985 78 275 101 283 -986 259 278 248 284 -987 136 135 117 266 -988 231 232 29 110 -989 99 103 71 253 -990 235 251 230 274 -991 180 255 205 273 -992 213 225 216 260 -993 237 228 240 262 -994 105 73 69 251 -995 209 19 212 68 -996 222 198 100 268 -997 95 263 88 275 -998 246 274 262 281 -999 209 260 213 268 -1000 79 257 81 272 -1001 227 215 216 261 -1002 194 205 180 255 -1003 187 192 202 277 -1004 19 209 91 68 -1005 238 230 235 251 -1006 83 272 99 285 -1007 136 117 125 266 -1008 248 269 250 270 -1009 85 101 252 283 -1010 193 185 184 251 -1011 175 143 158 262 -1012 70 91 100 268 -1013 160 276 118 282 -1014 65 87 93 259 -1015 240 245 235 274 -1016 177 166 154 255 -1017 216 226 213 280 -1018 18 212 74 206 -1019 129 88 67 263 -1020 231 239 233 253 -1021 252 265 85 283 -1022 148 147 149 269 -1023 114 92 131 285 -1024 140 261 147 280 -1025 116 219 211 266 -1026 246 272 253 279 -1027 247 275 257 283 -1028 155 153 264 281 -1029 167 271 173 276 -1030 140 146 147 261 -1031 188 197 189 273 -1032 119 66 12 88 -1033 18 212 68 74 -1034 116 125 117 266 -1035 102 91 70 268 -1036 236 233 240 262 -1037 83 263 95 275 -1038 218 227 206 265 -1039 52 146 140 207 -1040 33 65 190 178 -1041 252 275 247 283 -1042 107 88 129 263 -1043 247 260 265 283 -1044 217 277 226 280 -1045 220 213 210 268 -1046 138 45 106 162 -1047 146 52 214 207 -1048 188 189 270 273 -1049 247 257 278 283 -1050 83 263 272 285 -1051 178 190 191 258 -1052 237 262 240 274 -1053 227 216 225 265 -1054 106 44 126 162 -1055 160 167 276 282 -1056 176 137 270 273 -1057 11 90 66 109 -1058 162 254 157 286 -1059 87 259 65 284 -1060 33 94 65 178 -1061 32 193 94 178 -1062 154 255 166 273 -1063 82 94 105 258 -1064 116 117 111 266 -1065 10 90 109 125 -1066 221 144 208 286 -1067 56 228 158 143 -1068 106 121 45 138 -1069 132 169 221 286 -1070 60 192 142 165 -1071 254 261 157 286 -1072 128 122 120 276 -1073 207 53 217 280 -1074 217 163 277 280 -1075 175 158 161 262 -1076 174 148 159 269 -1077 32 94 193 73 -1078 195 70 198 268 -1079 130 256 108 282 -1080 119 11 66 109 -1081 164 139 48 124 -1082 246 262 256 281 -1083 180 166 63 194 -1084 228 56 236 143 -1085 121 46 138 118 -1086 76 68 91 260 -1087 160 167 138 276 -1088 198 222 183 268 -1089 60 179 192 165 -1090 185 205 194 255 -1091 107 119 12 88 -1092 78 95 101 275 -1093 71 99 253 285 -1094 35 70 195 93 -1095 181 35 195 93 -1096 161 175 262 281 -1097 146 261 214 286 -1098 140 53 207 280 -1099 261 266 215 286 -1100 160 138 46 118 -1101 76 75 68 260 -1102 216 261 207 280 -1103 145 63 166 194 -1104 106 254 122 276 -1105 235 230 234 274 -1106 155 153 151 264 -1107 137 179 165 270 -1108 209 212 213 260 -1109 138 167 173 276 -1110 149 150 151 271 -1111 72 74 85 265 -1112 214 207 216 261 -1113 75 76 77 278 -1114 126 169 132 286 -1115 75 278 77 283 -1116 108 48 139 124 -1117 179 137 188 270 -1118 247 278 260 283 -1119 240 228 236 262 -1120 219 221 208 286 -1121 144 146 214 286 -1122 137 159 176 270 -1123 146 157 147 261 -1124 218 211 227 266 -1125 245 233 239 253 -1126 106 135 122 254 -1127 155 156 153 281 -1128 188 270 137 273 -1129 84 272 89 279 -1130 254 266 261 286 -1131 41 183 220 186 -1132 159 149 152 269 -1133 181 204 196 259 -1134 214 261 215 286 -1135 209 213 220 268 -1136 93 87 102 259 -1137 67 129 13 88 -1138 243 237 234 274 -1139 129 263 67 285 -1140 234 182 39 243 -1141 118 276 128 282 -1142 208 144 214 286 -1143 169 144 221 286 -1144 173 157 162 254 -1145 157 261 146 286 -1146 218 17 72 206 -1147 199 39 182 243 -1148 85 265 75 283 -1149 129 107 13 88 -1150 95 83 88 263 -1151 165 61 179 137 -1152 81 89 84 272 -1153 93 181 34 65 -1154 215 214 216 261 -1155 85 74 75 265 -1156 220 183 222 268 -1157 95 78 79 275 -1158 153 161 154 281 -1159 172 223 217 277 -1160 124 113 112 267 -1161 17 74 72 206 -1162 210 226 217 277 -1163 115 123 120 263 -1164 236 232 233 267 -1165 178 191 203 258 -1166 61 188 179 137 -1167 121 106 122 276 -1168 144 208 214 51 -1169 170 241 143 267 -1170 104 258 82 284 -1171 190 34 181 65 -1172 219 215 266 286 -1173 44 169 126 162 -1174 164 134 170 267 -1175 212 225 213 260 -1176 162 157 169 286 -1177 94 82 104 258 -1178 139 47 108 282 -1179 87 278 259 284 -1180 160 47 139 282 -1181 186 192 187 277 -1182 172 217 163 277 -1183 62 180 188 273 -1184 158 57 237 228 -1185 112 232 241 267 -1186 87 77 76 278 -1187 47 118 108 282 -1188 186 223 201 277 -1189 90 10 97 125 -1190 83 67 263 285 -1191 47 160 118 282 -1192 164 143 171 267 -1193 71 92 99 285 -1194 140 147 148 280 -1195 160 155 167 282 -1196 83 79 81 272 -1197 214 144 51 146 -1198 159 148 149 269 -1199 164 124 134 267 -1200 167 150 173 271 -1201 241 134 112 267 -1202 166 180 62 273 -1203 62 188 137 273 -1204 172 54 217 223 -1205 219 208 215 286 -1206 83 99 67 285 -1207 118 128 123 282 -1208 85 78 101 283 -1209 209 220 222 268 -1210 143 241 236 267 -1211 137 166 62 273 -1212 220 41 186 210 -1213 128 118 121 276 -1214 218 72 16 224 -1215 54 217 163 172 -1216 80 104 82 284 -1217 237 141 57 158 -1218 26 229 86 238 -1219 189 179 188 270 -1220 88 83 67 263 -1221 89 69 86 279 -1222 186 210 223 277 -1223 113 115 114 285 -1224 151 153 152 264 -1225 148 163 140 280 -1226 195 187 204 268 -1227 22 116 111 219 -1228 104 87 65 284 -1229 208 221 50 144 -1230 27 244 69 96 -1231 28 231 110 131 -1232 241 55 170 143 -1233 163 174 142 277 -1234 180 205 197 273 -1235 242 24 71 98 -1236 129 115 263 285 -1237 142 201 172 277 -1238 161 156 175 281 -1239 167 151 150 271 -1240 107 115 120 263 -1241 103 84 89 279 -1242 208 214 215 286 -1243 216 207 226 280 -1244 131 28 231 242 -1245 115 107 129 263 -1246 179 189 202 270 -1247 170 134 241 267 -1248 30 241 112 232 -1249 97 16 72 224 -1250 238 86 26 69 -1251 198 183 195 268 -1252 144 157 146 286 -1253 239 24 71 242 -1254 221 169 50 144 -1255 141 168 243 58 -1256 30 241 134 112 -1257 137 165 159 270 -1258 67 14 129 285 -1259 207 217 226 280 -1260 237 240 235 274 -1261 55 241 236 143 -1262 223 201 42 186 -1263 215 211 219 266 -1264 201 142 192 277 -1265 174 159 165 270 -1266 78 75 77 283 -1267 40 198 222 183 -1268 225 206 227 265 -1269 116 22 211 219 -1270 143 164 170 267 -1271 123 130 108 282 -1272 195 183 187 268 -1273 103 89 86 279 -1274 132 221 169 43 -1275 67 92 14 285 -1276 36 70 100 198 -1277 155 160 139 282 -1278 243 237 141 58 -1279 83 81 84 272 -1280 224 97 9 133 -1281 27 244 238 69 -1282 142 59 172 201 -1283 227 211 215 266 -1284 14 114 129 285 -1285 170 164 49 134 -1286 223 172 201 277 -1287 78 85 75 283 -1288 186 42 223 210 -1289 124 49 164 134 -1290 14 92 114 285 -1291 118 123 108 282 -1292 237 235 234 274 -1293 124 112 134 267 -1294 39 182 234 38 -1295 145 199 168 64 -1296 137 176 166 273 -1297 230 38 184 37 -1298 74 17 18 206 -1299 122 128 121 276 -1300 142 59 201 192 -1301 220 40 222 183 -1302 15 131 92 98 -1303 188 180 197 273 -1304 70 36 195 198 -1305 96 200 73 31 -1306 25 24 71 239 -1307 29 28 231 110 -1308 243 199 8 168 -1309 131 114 15 92 -1310 154 166 176 273 -1311 200 244 3 96 -1312 241 232 236 267 -1313 129 114 115 285 -1314 236 56 55 143 -1315 163 142 172 277 -1316 221 132 5 43 -1317 52 214 51 146 -1318 209 20 19 91 -1319 33 32 94 178 -1320 50 169 221 43 -1321 112 29 30 232 -1322 153 156 161 281 -1323 44 106 45 162 -1324 166 63 62 180 -1325 90 11 10 109 -1326 97 9 16 224 -1327 200 193 73 31 -1328 47 160 46 118 -1329 46 45 121 138 -1330 223 210 217 277 -1331 157 144 169 286 -1332 219 111 23 221 -1333 207 140 52 53 -1334 132 23 111 221 -1335 133 21 224 116 -1336 145 194 199 64 -1337 211 224 21 116 -1338 237 141 58 57 -1339 212 19 18 68 -1340 53 217 163 54 -1341 242 131 2 98 -1342 66 12 11 119 -1343 241 170 6 134 -1344 9 1 224 133 -1345 60 61 179 165 -1346 201 192 186 277 -1347 139 48 108 47 -1348 34 35 181 93 -1349 223 172 7 201 -1350 222 4 100 198 -1351 14 15 114 92 -1352 49 164 48 124 -1353 80 87 104 284 -1354 107 12 13 88 -1355 142 59 192 60 -1356 41 42 186 210 -1357 199 8 39 243 -1358 193 32 73 31 -1359 195 36 70 35 -1360 238 26 27 69 -1361 190 34 65 33 -1362 137 61 188 62 -1363 21 22 211 116 -1364 3 37 244 200 -1365 50 221 5 43 -1366 9 1 16 224 -1367 194 145 63 64 -1368 129 13 14 67 -1369 131 2 28 242 -1370 99 92 67 285 -1371 241 6 30 134 -1372 198 222 4 40 -1373 23 22 111 219 -1374 183 220 40 41 -1375 168 199 8 64 -1376 3 200 96 31 -1377 168 8 243 58 -1378 55 170 6 241 -1379 50 208 144 51 -1380 2 24 242 98 -1381 244 27 3 96 -1382 20 4 100 222 -1383 126 132 169 43 -1384 72 16 17 218 -1385 5 23 132 221 -1386 172 59 7 201 -1387 36 100 4 198 -1388 6 170 49 134 -1389 133 97 9 125 -1390 172 7 54 223 -1391 131 15 2 98 -1392 228 158 57 56 -1393 7 223 201 42 -1394 229 25 26 86 -1395 224 1 21 133 -1396 43 169 126 44 -1397 10 9 97 125 -1398 191 259 190 196 -1399 190 259 191 258 -1400 272 256 249 246 -1401 272 249 256 263 -1402 275 276 271 249 -1403 271 276 275 252 -1404 264 282 167 155 -1405 167 282 264 249 -1406 275 127 276 120 -1407 275 276 127 252 -1408 273 152 270 176 -1409 273 270 152 248 -1410 274 177 145 141 -1411 274 145 177 255 -1412 239 86 25 229 -1413 25 86 239 71 -1414 285 263 113 115 -1415 285 113 263 256 -1416 130 113 263 115 -1417 130 263 113 256 -1418 262 171 281 175 -1419 262 281 171 256 -1420 178 65 258 94 -1421 178 258 65 190 -1422 281 177 274 161 -1423 281 274 177 255 -1424 174 280 163 148 -1425 163 280 174 277 -1426 268 76 259 102 -1427 268 259 76 260 -1428 278 76 259 260 -1429 116 224 125 211 -1430 116 125 224 133 -1431 281 171 155 156 -1432 281 155 171 256 -1433 230 200 37 184 -1434 37 200 230 244 -1435 143 228 262 236 -1436 143 262 228 158 -1437 105 279 69 89 -1438 69 279 105 251 -1439 286 162 126 169 -1440 286 126 162 254 -1441 280 53 163 140 -1442 280 163 53 217 -1443 278 80 87 77 -1444 278 87 80 284 -1445 222 91 20 209 -1446 20 91 222 100 -1447 76 259 87 278 -1448 76 87 259 102 +841 +1 1 2 8 1 1 9 +2 1 2 8 1 9 10 +3 1 2 8 1 10 11 +4 1 2 8 1 11 12 +5 1 2 8 1 12 13 +6 1 2 8 1 13 2 +7 1 2 8 2 1 14 +8 1 2 8 2 14 15 +9 1 2 8 2 15 16 +10 1 2 8 2 16 17 +11 1 2 8 2 17 4 +12 1 2 8 3 1 18 +13 1 2 8 3 18 19 +14 1 2 8 3 19 5 +15 1 2 8 4 2 20 +16 1 2 8 4 20 21 +17 1 2 8 4 21 22 +18 1 2 8 4 22 3 +19 1 2 8 5 2 23 +20 1 2 8 5 23 24 +21 1 2 8 5 24 6 +22 1 2 8 6 3 25 +23 1 2 8 6 25 26 +24 1 2 8 6 26 27 +25 1 2 8 6 27 28 +26 1 2 8 6 28 4 +27 1 2 8 7 3 29 +28 1 2 8 7 29 30 +29 1 2 8 7 30 8 +30 1 2 8 8 4 31 +31 1 2 8 8 31 32 +32 1 2 8 8 32 7 +33 1 2 8 9 5 33 +34 1 2 8 9 33 34 +35 1 2 8 9 34 35 +36 1 2 8 9 35 36 +37 1 2 8 9 36 37 +38 1 2 8 9 37 6 +39 1 2 8 10 5 38 +40 1 2 8 10 38 39 +41 1 2 8 10 39 40 +42 1 2 8 10 40 41 +43 1 2 8 10 41 7 +44 1 2 8 11 6 42 +45 1 2 8 11 42 43 +46 1 2 8 11 43 44 +47 1 2 8 11 44 8 +48 1 2 8 12 7 45 +49 1 2 8 12 45 46 +50 1 2 8 12 46 47 +51 1 2 8 12 47 48 +52 1 2 8 12 48 8 +53 2 2 1 1 1 9 14 +54 2 2 1 1 13 2 66 +55 2 2 1 1 2 20 66 +56 2 2 1 1 22 3 65 +57 2 2 1 1 3 25 65 +58 2 2 1 1 4 17 67 +59 2 2 1 1 28 4 67 +60 2 2 1 1 9 10 54 +61 2 2 1 1 14 9 54 +62 2 2 1 1 10 11 49 +63 2 2 1 1 10 49 54 +64 2 2 1 1 11 12 59 +65 2 2 1 1 49 11 59 +66 2 2 1 1 12 13 51 +67 2 2 1 1 12 51 59 +68 2 2 1 1 51 13 66 +69 2 2 1 1 15 14 54 +70 2 2 1 1 16 15 53 +71 2 2 1 1 53 15 54 +72 2 2 1 1 17 16 62 +73 2 2 1 1 16 53 62 +74 2 2 1 1 17 62 67 +75 2 2 1 1 20 21 63 +76 2 2 1 1 20 63 66 +77 2 2 1 1 21 22 55 +78 2 2 1 1 21 55 63 +79 2 2 1 1 55 22 65 +80 2 2 1 1 25 26 50 +81 2 2 1 1 25 50 65 +82 2 2 1 1 26 27 56 +83 2 2 1 1 50 26 56 +84 2 2 1 1 27 28 52 +85 2 2 1 1 27 52 56 +86 2 2 1 1 52 28 67 +87 2 2 1 1 54 49 61 +88 2 2 1 1 49 59 60 +89 2 2 1 1 49 60 61 +90 2 2 1 1 55 50 64 +91 2 2 1 1 50 55 65 +92 2 2 1 1 50 56 57 +93 2 2 1 1 50 57 64 +94 2 2 1 1 59 51 69 +95 2 2 1 1 51 63 64 +96 2 2 1 1 63 51 66 +97 2 2 1 1 51 64 69 +98 2 2 1 1 56 52 58 +99 2 2 1 1 58 52 68 +100 2 2 1 1 62 52 67 +101 2 2 1 1 52 62 68 +102 2 2 1 1 53 54 61 +103 2 2 1 1 53 61 68 +104 2 2 1 1 62 53 68 +105 2 2 1 1 63 55 64 +106 2 2 1 1 57 56 58 +107 2 2 1 1 57 58 60 +108 2 2 1 1 57 60 69 +109 2 2 1 1 64 57 69 +110 2 2 1 1 60 58 61 +111 2 2 1 1 61 58 68 +112 2 2 1 1 60 59 69 +113 2 2 2 2 1 85 9 +114 2 2 2 2 18 85 1 +115 2 2 2 2 13 83 2 +116 2 2 2 2 2 83 23 +117 2 2 2 2 5 86 19 +118 2 2 2 2 33 86 5 +119 2 2 2 2 24 84 6 +120 2 2 2 2 6 84 37 +121 2 2 2 2 9 75 10 +122 2 2 2 2 9 85 75 +123 2 2 2 2 10 70 11 +124 2 2 2 2 10 75 70 +125 2 2 2 2 11 74 12 +126 2 2 2 2 70 74 11 +127 2 2 2 2 12 77 13 +128 2 2 2 2 74 77 12 +129 2 2 2 2 77 83 13 +130 2 2 2 2 19 79 18 +131 2 2 2 2 79 85 18 +132 2 2 2 2 19 86 79 +133 2 2 2 2 23 71 24 +134 2 2 2 2 23 83 71 +135 2 2 2 2 71 84 24 +136 2 2 2 2 34 72 33 +137 2 2 2 2 72 86 33 +138 2 2 2 2 35 76 34 +139 2 2 2 2 34 76 72 +140 2 2 2 2 36 73 35 +141 2 2 2 2 73 76 35 +142 2 2 2 2 37 78 36 +143 2 2 2 2 36 78 73 +144 2 2 2 2 37 84 78 +145 2 2 2 2 70 80 74 +146 2 2 2 2 75 81 70 +147 2 2 2 2 70 81 80 +148 2 2 2 2 77 82 71 +149 2 2 2 2 71 83 77 +150 2 2 2 2 71 82 78 +151 2 2 2 2 78 84 71 +152 2 2 2 2 76 81 72 +153 2 2 2 2 72 81 79 +154 2 2 2 2 79 86 72 +155 2 2 2 2 73 80 76 +156 2 2 2 2 78 82 73 +157 2 2 2 2 73 82 80 +158 2 2 2 2 74 82 77 +159 2 2 2 2 80 82 74 +160 2 2 2 2 79 81 75 +161 2 2 2 2 75 85 79 +162 2 2 2 2 80 81 76 +163 2 2 3 3 33 5 38 +164 2 2 3 3 37 104 6 +165 2 2 3 3 6 104 42 +166 2 2 3 3 7 105 41 +167 2 2 3 3 45 105 7 +168 2 2 3 3 44 103 8 +169 2 2 3 3 8 103 48 +170 2 2 3 3 33 92 34 +171 2 2 3 3 38 92 33 +172 2 2 3 3 34 87 35 +173 2 2 3 3 34 92 87 +174 2 2 3 3 35 97 36 +175 2 2 3 3 87 97 35 +176 2 2 3 3 36 89 37 +177 2 2 3 3 36 97 89 +178 2 2 3 3 89 104 37 +179 2 2 3 3 39 92 38 +180 2 2 3 3 40 91 39 +181 2 2 3 3 91 92 39 +182 2 2 3 3 41 100 40 +183 2 2 3 3 40 100 91 +184 2 2 3 3 41 105 100 +185 2 2 3 3 42 101 43 +186 2 2 3 3 42 104 101 +187 2 2 3 3 43 93 44 +188 2 2 3 3 43 101 93 +189 2 2 3 3 93 103 44 +190 2 2 3 3 46 90 45 +191 2 2 3 3 90 105 45 +192 2 2 3 3 47 94 46 +193 2 2 3 3 46 94 90 +194 2 2 3 3 48 88 47 +195 2 2 3 3 88 94 47 +196 2 2 3 3 48 103 88 +197 2 2 3 3 92 99 87 +198 2 2 3 3 87 98 97 +199 2 2 3 3 87 99 98 +200 2 2 3 3 93 102 88 +201 2 2 3 3 88 103 93 +202 2 2 3 3 88 95 94 +203 2 2 3 3 88 102 95 +204 2 2 3 3 97 107 89 +205 2 2 3 3 89 102 101 +206 2 2 3 3 101 104 89 +207 2 2 3 3 89 107 102 +208 2 2 3 3 94 96 90 +209 2 2 3 3 96 106 90 +210 2 2 3 3 100 105 90 +211 2 2 3 3 90 106 100 +212 2 2 3 3 91 99 92 +213 2 2 3 3 91 106 99 +214 2 2 3 3 100 106 91 +215 2 2 3 3 101 102 93 +216 2 2 3 3 95 96 94 +217 2 2 3 3 95 98 96 +218 2 2 3 3 95 107 98 +219 2 2 3 3 102 107 95 +220 2 2 3 3 98 99 96 +221 2 2 3 3 99 106 96 +222 2 2 3 3 98 107 97 +223 2 2 4 4 3 120 25 +224 2 2 4 4 29 120 3 +225 2 2 4 4 28 121 4 +226 2 2 4 4 4 121 31 +227 2 2 4 4 32 118 7 +228 2 2 4 4 7 118 45 +229 2 2 4 4 8 119 30 +230 2 2 4 4 48 119 8 +231 2 2 4 4 25 115 26 +232 2 2 4 4 25 120 115 +233 2 2 4 4 26 111 27 +234 2 2 4 4 26 115 111 +235 2 2 4 4 27 113 28 +236 2 2 4 4 111 113 27 +237 2 2 4 4 113 121 28 +238 2 2 4 4 30 108 29 +239 2 2 4 4 108 120 29 +240 2 2 4 4 30 119 108 +241 2 2 4 4 31 109 32 +242 2 2 4 4 31 121 109 +243 2 2 4 4 109 118 32 +244 2 2 4 4 45 114 46 +245 2 2 4 4 45 118 114 +246 2 2 4 4 46 110 47 +247 2 2 4 4 46 114 110 +248 2 2 4 4 47 112 48 +249 2 2 4 4 110 112 47 +250 2 2 4 4 112 119 48 +251 2 2 4 4 112 116 108 +252 2 2 4 4 108 119 112 +253 2 2 4 4 108 116 115 +254 2 2 4 4 115 120 108 +255 2 2 4 4 113 117 109 +256 2 2 4 4 109 121 113 +257 2 2 4 4 109 117 114 +258 2 2 4 4 114 118 109 +259 2 2 4 4 110 116 112 +260 2 2 4 4 114 117 110 +261 2 2 4 4 110 117 116 +262 2 2 4 4 111 117 113 +263 2 2 4 4 115 116 111 +264 2 2 4 4 116 117 111 +265 2 2 5 5 1 14 135 +266 2 2 5 5 18 1 135 +267 2 2 5 5 17 4 134 +268 2 2 5 5 4 31 134 +269 2 2 5 5 5 19 133 +270 2 2 5 5 38 5 133 +271 2 2 5 5 32 7 132 +272 2 2 5 5 7 41 132 +273 2 2 5 5 14 15 123 +274 2 2 5 5 14 123 135 +275 2 2 5 5 15 16 127 +276 2 2 5 5 123 15 127 +277 2 2 5 5 16 17 124 +278 2 2 5 5 16 124 127 +279 2 2 5 5 124 17 134 +280 2 2 5 5 19 18 129 +281 2 2 5 5 129 18 135 +282 2 2 5 5 19 129 133 +283 2 2 5 5 31 32 128 +284 2 2 5 5 31 128 134 +285 2 2 5 5 128 32 132 +286 2 2 5 5 39 38 126 +287 2 2 5 5 126 38 133 +288 2 2 5 5 40 39 122 +289 2 2 5 5 122 39 126 +290 2 2 5 5 41 40 125 +291 2 2 5 5 40 122 125 +292 2 2 5 5 41 125 132 +293 2 2 5 5 125 122 131 +294 2 2 5 5 122 126 130 +295 2 2 5 5 122 130 131 +296 2 2 5 5 123 127 130 +297 2 2 5 5 129 123 130 +298 2 2 5 5 123 129 135 +299 2 2 5 5 127 124 131 +300 2 2 5 5 124 128 131 +301 2 2 5 5 128 124 134 +302 2 2 5 5 128 125 131 +303 2 2 5 5 125 128 132 +304 2 2 5 5 126 129 130 +305 2 2 5 5 129 126 133 +306 2 2 5 5 130 127 131 +307 2 2 6 6 20 2 146 +308 2 2 6 6 2 23 146 +309 2 2 6 6 3 22 145 +310 2 2 6 6 29 3 145 +311 2 2 6 6 24 6 143 +312 2 2 6 6 6 42 143 +313 2 2 6 6 8 30 144 +314 2 2 6 6 44 8 144 +315 2 2 6 6 21 20 137 +316 2 2 6 6 137 20 146 +317 2 2 6 6 22 21 140 +318 2 2 6 6 21 137 140 +319 2 2 6 6 22 140 145 +320 2 2 6 6 23 24 141 +321 2 2 6 6 23 141 146 +322 2 2 6 6 141 24 143 +323 2 2 6 6 30 29 136 +324 2 2 6 6 136 29 145 +325 2 2 6 6 30 136 144 +326 2 2 6 6 42 43 138 +327 2 2 6 6 42 138 143 +328 2 2 6 6 43 44 139 +329 2 2 6 6 138 43 139 +330 2 2 6 6 139 44 144 +331 2 2 6 6 139 136 142 +332 2 2 6 6 136 139 144 +333 2 2 6 6 136 140 142 +334 2 2 6 6 140 136 145 +335 2 2 6 6 140 137 142 +336 2 2 6 6 137 141 142 +337 2 2 6 6 141 137 146 +338 2 2 6 6 138 139 142 +339 2 2 6 6 141 138 142 +340 2 2 6 6 138 141 143 +341 4 2 7 1 107 149 147 157 +342 4 2 7 1 72 126 133 163 +343 4 2 7 1 147 151 149 152 +344 4 2 7 1 72 133 86 163 +345 4 2 7 1 107 95 147 149 +346 4 2 7 1 147 64 151 152 +347 4 2 7 1 69 147 64 151 +348 4 2 7 1 64 147 69 152 +349 4 2 7 1 149 151 64 152 +350 4 2 7 1 95 147 149 158 +351 4 2 7 1 64 151 149 160 +352 4 2 7 1 107 102 149 157 +353 4 2 7 1 147 158 95 159 +354 4 2 7 1 107 102 95 149 +355 4 2 7 1 126 72 92 163 +356 4 2 7 1 144 119 103 149 +357 4 2 7 1 133 126 72 92 +358 4 2 7 1 59 151 74 165 +359 4 2 7 1 147 151 69 165 +360 4 2 7 1 72 86 133 92 +361 4 2 7 1 59 69 151 165 +362 4 2 7 1 152 158 147 159 +363 4 2 7 1 147 152 149 158 +364 4 2 7 1 144 136 119 149 +365 4 2 7 1 59 51 74 151 +366 4 2 7 1 93 144 103 149 +367 4 2 7 1 126 92 150 163 +368 4 2 7 1 74 51 77 151 +369 4 2 7 1 150 155 49 165 +370 4 2 7 1 93 139 144 149 +371 4 2 7 1 49 163 150 165 +372 4 2 7 1 49 70 163 165 +373 4 2 7 1 49 155 150 163 +374 4 2 7 1 104 78 84 161 +375 4 2 7 1 149 158 152 166 +376 4 2 7 1 79 72 86 163 +377 4 2 7 1 72 150 92 163 +378 4 2 7 1 102 149 157 161 +379 4 2 7 1 78 71 84 161 +380 4 2 7 1 79 86 133 163 +381 4 2 7 1 61 155 49 163 +382 4 2 7 1 78 151 71 161 +383 4 2 7 1 59 69 51 151 +384 4 2 7 1 57 64 69 152 +385 4 2 7 1 78 157 151 161 +386 4 2 7 1 149 152 64 166 +387 4 2 7 1 95 107 147 159 +388 4 2 7 1 64 160 149 166 +389 4 2 7 1 75 49 70 163 +390 4 2 7 1 147 157 151 165 +391 4 2 7 1 61 156 155 163 +392 4 2 7 1 103 119 88 149 +393 4 2 7 1 89 78 104 161 +394 4 2 7 1 108 119 136 149 +395 4 2 7 1 143 84 141 161 +396 4 2 7 1 108 115 116 166 +397 4 2 7 1 63 137 66 151 +398 4 2 7 1 126 129 133 163 +399 4 2 7 1 108 116 158 166 +400 4 2 7 1 108 158 149 166 +401 4 2 7 1 89 157 78 161 +402 4 2 7 1 139 136 144 149 +403 4 2 7 1 136 108 149 166 +404 4 2 7 1 146 66 137 151 +405 4 2 7 1 137 151 63 160 +406 4 2 7 1 119 88 149 158 +407 4 2 7 1 74 151 82 165 +408 4 2 7 1 111 115 56 152 +409 4 2 7 1 151 157 82 165 +410 4 2 7 1 84 71 141 161 +411 4 2 7 1 155 156 150 163 +412 4 2 7 1 115 50 152 166 +413 4 2 7 1 69 60 57 155 +414 4 2 7 1 149 151 147 157 +415 4 2 7 1 98 147 107 159 +416 4 2 7 1 104 84 143 161 +417 4 2 7 1 119 112 88 158 +418 4 2 7 1 130 126 150 163 +419 4 2 7 1 130 150 126 168 +420 4 2 7 1 126 150 92 168 +421 4 2 7 1 98 107 147 164 +422 4 2 7 1 146 83 66 151 +423 4 2 7 1 50 64 152 166 +424 4 2 7 1 72 76 87 150 +425 4 2 7 1 79 133 129 163 +426 4 2 7 1 151 157 149 161 +427 4 2 7 1 141 71 151 161 +428 4 2 7 1 98 147 159 164 +429 4 2 7 1 106 154 148 167 +430 4 2 7 1 75 70 81 163 +431 4 2 7 1 74 77 82 151 +432 4 2 7 1 77 66 83 151 +433 4 2 7 1 148 153 68 162 +434 4 2 7 1 106 148 96 167 +435 4 2 7 1 111 116 115 152 +436 4 2 7 1 50 56 115 152 +437 4 2 7 1 96 159 158 167 +438 4 2 7 1 93 103 88 149 +439 4 2 7 1 96 148 106 159 +440 4 2 7 1 93 139 149 161 +441 4 2 7 1 54 61 49 163 +442 4 2 7 1 148 152 159 167 +443 4 2 7 1 58 148 68 162 +444 4 2 7 1 158 159 152 167 +445 4 2 7 1 107 157 147 164 +446 4 2 7 1 96 95 158 159 +447 4 2 7 1 83 146 141 151 +448 4 2 7 1 157 164 80 165 +449 4 2 7 1 76 87 150 164 +450 4 2 7 1 68 148 58 155 +451 4 2 7 1 124 62 127 156 +452 4 2 7 1 77 51 66 151 +453 4 2 7 1 147 164 157 165 +454 4 2 7 1 81 164 150 165 +455 4 2 7 1 80 82 157 165 +456 4 2 7 1 148 154 106 168 +457 4 2 7 1 68 153 148 156 +458 4 2 7 1 124 153 62 156 +459 4 2 7 1 95 149 88 158 +460 4 2 7 1 81 80 164 165 +461 4 2 7 1 148 106 159 168 +462 4 2 7 1 153 154 109 167 +463 4 2 7 1 126 91 122 168 +464 4 2 7 1 148 155 68 156 +465 4 2 7 1 95 98 107 159 +466 4 2 7 1 88 95 102 149 +467 4 2 7 1 57 50 64 152 +468 4 2 7 1 126 92 91 168 +469 4 2 7 1 83 141 71 151 +470 4 2 7 1 146 137 141 151 +471 4 2 7 1 96 148 159 167 +472 4 2 7 1 87 92 72 150 +473 4 2 7 1 151 160 142 161 +474 4 2 7 1 51 63 66 151 +475 4 2 7 1 72 81 76 150 +476 4 2 7 1 141 151 137 160 +477 4 2 7 1 131 154 153 168 +478 4 2 7 1 54 156 61 163 +479 4 2 7 1 56 111 152 162 +480 4 2 7 1 69 64 51 151 +481 4 2 7 1 142 136 139 149 +482 4 2 7 1 12 77 74 51 +483 4 2 7 1 153 156 131 168 +484 4 2 7 1 148 156 153 168 +485 4 2 7 1 150 156 130 163 +486 4 2 7 1 81 150 72 163 +487 4 2 7 1 51 74 12 59 +488 4 2 7 1 62 153 68 156 +489 4 2 7 1 49 75 54 163 +490 4 2 7 1 130 156 150 168 +491 4 2 7 1 142 149 139 161 +492 4 2 7 1 106 90 154 167 +493 4 2 7 1 148 58 155 162 +494 4 2 7 1 116 158 162 167 +495 4 2 7 1 141 151 142 161 +496 4 2 7 1 116 158 152 162 +497 4 2 7 1 153 154 148 168 +498 4 2 7 1 123 127 53 156 +499 4 2 7 1 53 127 62 156 +500 4 2 7 1 111 116 152 162 +501 4 2 7 1 62 16 124 127 +502 4 2 7 1 136 149 142 166 +503 4 2 7 1 141 142 151 160 +504 4 2 7 1 130 126 122 168 +505 4 2 7 1 54 123 156 163 +506 4 2 7 1 149 160 142 166 +507 4 2 7 1 141 137 142 160 +508 4 2 7 1 92 150 99 168 +509 4 2 7 1 150 164 147 165 +510 4 2 7 1 116 162 117 167 +511 4 2 7 1 94 96 158 167 +512 4 2 7 1 52 68 153 162 +513 4 2 7 1 147 155 152 159 +514 4 2 7 1 148 154 153 167 +515 4 2 7 1 124 131 153 156 +516 4 2 7 1 148 153 162 167 +517 4 2 7 1 109 113 153 162 +518 4 2 7 1 93 149 102 161 +519 4 2 7 1 81 76 150 164 +520 4 2 7 1 106 96 90 167 +521 4 2 7 1 88 102 93 149 +522 4 2 7 1 51 63 151 160 +523 4 2 7 1 82 151 78 157 +524 4 2 7 1 53 54 123 156 +525 4 2 7 1 94 96 95 158 +526 4 2 7 1 87 159 150 164 +527 4 2 7 1 147 155 150 165 +528 4 2 7 1 152 155 148 159 +529 4 2 7 1 152 155 58 162 +530 4 2 7 1 152 162 158 167 +531 4 2 7 1 100 125 40 168 +532 4 2 7 1 99 150 87 159 +533 4 2 7 1 78 82 71 151 +534 4 2 7 1 51 151 64 160 +535 4 2 7 1 73 80 157 164 +536 4 2 7 1 141 138 143 161 +537 4 2 7 1 40 125 122 168 +538 4 2 7 1 74 82 80 165 +539 4 2 7 1 26 56 111 115 +540 4 2 7 1 16 62 53 127 +541 4 2 7 1 109 117 113 162 +542 4 2 7 1 83 71 77 151 +543 4 2 7 1 140 142 160 166 +544 4 2 7 1 43 138 139 161 +545 4 2 7 1 91 92 99 168 +546 4 2 7 1 119 144 30 136 +547 4 2 7 1 128 153 131 154 +548 4 2 7 1 114 109 154 167 +549 4 2 7 1 140 136 142 166 +550 4 2 7 1 148 155 152 162 +551 4 2 7 1 101 89 104 161 +552 4 2 7 1 139 93 43 161 +553 4 2 7 1 73 80 82 157 +554 4 2 7 1 112 108 116 158 +555 4 2 7 1 43 101 138 161 +556 4 2 7 1 52 58 68 162 +557 4 2 7 1 70 80 81 165 +558 4 2 7 1 51 64 63 160 +559 4 2 7 1 93 101 43 161 +560 4 2 7 1 149 142 160 161 +561 4 2 7 1 81 80 76 164 +562 4 2 7 1 102 157 89 161 +563 4 2 7 1 111 117 116 162 +564 4 2 7 1 114 117 109 167 +565 4 2 7 1 99 92 87 150 +566 4 2 7 1 123 130 156 163 +567 4 2 7 1 108 119 30 136 +568 4 2 7 1 98 159 87 164 +569 4 2 7 1 124 127 131 156 +570 4 2 7 1 99 150 159 168 +571 4 2 7 1 130 129 126 163 +572 4 2 7 1 81 72 79 163 +573 4 2 7 1 53 62 68 156 +574 4 2 7 1 26 50 56 115 +575 4 2 7 1 100 154 125 168 +576 4 2 7 1 58 152 57 155 +577 4 2 7 1 85 135 54 163 +578 4 2 7 1 150 155 147 159 +579 4 2 7 1 150 159 147 164 +580 4 2 7 1 98 99 87 159 +581 4 2 7 1 125 154 131 168 +582 4 2 7 1 127 53 15 123 +583 4 2 7 1 126 122 91 39 +584 4 2 7 1 114 118 105 154 +585 4 2 7 1 99 159 106 168 +586 4 2 7 1 91 100 40 168 +587 4 2 7 1 113 67 121 153 +588 4 2 7 1 110 158 116 167 +589 4 2 7 1 55 160 64 166 +590 4 2 7 1 123 130 127 156 +591 4 2 7 1 114 105 90 154 +592 4 2 7 1 56 152 58 162 +593 4 2 7 1 106 154 100 168 +594 4 2 7 1 148 155 150 159 +595 4 2 7 1 128 124 131 153 +596 4 2 7 1 113 52 67 153 +597 4 2 7 1 128 131 125 154 +598 4 2 7 1 102 107 89 157 +599 4 2 7 1 57 56 50 152 +600 4 2 7 1 110 116 117 167 +601 4 2 7 1 122 91 40 168 +602 4 2 7 1 60 49 155 165 +603 4 2 7 1 152 148 162 167 +604 4 2 7 1 82 77 71 151 +605 4 2 7 1 75 49 10 70 +606 4 2 7 1 150 155 148 156 +607 4 2 7 1 136 29 108 166 +608 4 2 7 1 50 120 65 166 +609 4 2 7 1 141 142 138 161 +610 4 2 7 1 61 68 155 156 +611 4 2 7 1 34 72 76 87 +612 4 2 7 1 85 54 75 163 +613 4 2 7 1 135 123 54 163 +614 4 2 7 1 60 69 59 165 +615 4 2 7 1 97 157 107 164 +616 4 2 7 1 105 45 118 114 +617 4 2 7 1 115 120 50 166 +618 4 2 7 1 148 159 150 168 +619 4 2 7 1 56 57 58 152 +620 4 2 7 1 67 62 134 153 +621 4 2 7 1 134 62 124 153 +622 4 2 7 1 132 100 105 154 +623 4 2 7 1 105 45 114 90 +624 4 2 7 1 132 125 100 154 +625 4 2 7 1 28 67 121 113 +626 4 2 7 1 150 156 148 168 +627 4 2 7 1 50 55 64 166 +628 4 2 7 1 98 97 107 164 +629 4 2 7 1 28 67 113 52 +630 4 2 7 1 138 42 143 101 +631 4 2 7 1 109 153 128 154 +632 4 2 7 1 61 60 49 155 +633 4 2 7 1 53 54 15 123 +634 4 2 7 1 126 91 92 39 +635 4 2 7 1 36 97 73 157 +636 4 2 7 1 104 143 42 101 +637 4 2 7 1 149 160 151 161 +638 4 2 7 1 143 84 24 141 +639 4 2 7 1 83 23 141 146 +640 4 2 7 1 58 57 60 155 +641 4 2 7 1 128 121 134 153 +642 4 2 7 1 105 41 100 132 +643 4 2 7 1 54 53 61 156 +644 4 2 7 1 134 67 17 62 +645 4 2 7 1 132 118 128 154 +646 4 2 7 1 84 104 37 78 +647 4 2 7 1 83 77 13 66 +648 4 2 7 1 124 134 17 62 +649 4 2 7 1 132 41 100 125 +650 4 2 7 1 36 73 78 157 +651 4 2 7 1 120 25 50 65 +652 4 2 7 1 101 102 89 161 +653 4 2 7 1 106 90 100 154 +654 4 2 7 1 88 48 119 103 +655 4 2 7 1 52 68 62 153 +656 4 2 7 1 10 54 49 75 +657 4 2 7 1 87 34 72 92 +658 4 2 7 1 135 54 14 85 +659 4 2 7 1 121 128 109 153 +660 4 2 7 1 44 139 144 93 +661 4 2 7 1 104 89 37 78 +662 4 2 7 1 51 13 77 66 +663 4 2 7 1 36 78 89 157 +664 4 2 7 1 145 55 140 22 +665 4 2 7 1 29 120 108 166 +666 4 2 7 1 25 50 115 120 +667 4 2 7 1 109 128 118 154 +668 4 2 7 1 55 145 140 166 +669 4 2 7 1 144 103 44 93 +670 4 2 7 1 88 48 112 119 +671 4 2 7 1 84 24 141 71 +672 4 2 7 1 23 141 71 83 +673 4 2 7 1 22 145 55 65 +674 4 2 7 1 135 85 79 163 +675 4 2 7 1 131 156 130 168 +676 4 2 7 1 136 145 29 166 +677 4 2 7 1 115 108 120 166 +678 4 2 7 1 36 89 97 157 +679 4 2 7 1 90 114 154 167 +680 4 2 7 1 113 52 153 162 +681 4 2 7 1 145 55 65 166 +682 4 2 7 1 63 21 137 160 +683 4 2 7 1 38 133 86 92 +684 4 2 7 1 99 96 106 159 +685 4 2 7 1 35 73 97 164 +686 4 2 7 1 97 73 157 164 +687 4 2 7 1 85 54 9 75 +688 4 2 7 1 21 140 137 160 +689 4 2 7 1 14 54 135 123 +690 4 2 7 1 29 108 30 136 +691 4 2 7 1 98 95 96 159 +692 4 2 7 1 90 46 114 167 +693 4 2 7 1 74 11 59 165 +694 4 2 7 1 76 73 35 164 +695 4 2 7 1 88 112 47 158 +696 4 2 7 1 110 46 94 167 +697 4 2 7 1 138 142 139 161 +698 4 2 7 1 74 70 11 165 +699 4 2 7 1 47 112 110 158 +700 4 2 7 1 33 86 72 92 +701 4 2 7 1 61 68 58 155 +702 4 2 7 1 129 130 123 163 +703 4 2 7 1 94 90 96 167 +704 4 2 7 1 21 55 140 160 +705 4 2 7 1 113 27 52 162 +706 4 2 7 1 127 130 131 156 +707 4 2 7 1 131 130 122 168 +708 4 2 7 1 128 118 132 32 +709 4 2 7 1 31 121 134 128 +710 4 2 7 1 47 110 94 158 +711 4 2 7 1 27 111 56 162 +712 4 2 7 1 38 126 133 92 +713 4 2 7 1 79 129 135 163 +714 4 2 7 1 70 49 11 165 +715 4 2 7 1 122 125 131 168 +716 4 2 7 1 67 134 121 153 +717 4 2 7 1 94 158 110 167 +718 4 2 7 1 55 140 160 166 +719 4 2 7 1 90 94 46 167 +720 4 2 7 1 110 114 46 167 +721 4 2 7 1 105 118 132 154 +722 4 2 7 1 21 63 55 160 +723 4 2 7 1 75 81 79 163 +724 4 2 7 1 101 93 102 161 +725 4 2 7 1 27 113 111 162 +726 4 2 7 1 86 19 79 133 +727 4 2 7 1 85 18 135 79 +728 4 2 7 1 128 109 118 32 +729 4 2 7 1 128 31 121 109 +730 4 2 7 1 145 65 120 166 +731 4 2 7 1 76 35 87 164 +732 4 2 7 1 94 88 47 158 +733 4 2 7 1 120 29 145 166 +734 4 2 7 1 54 9 14 85 +735 4 2 7 1 52 56 58 162 +736 4 2 7 1 27 56 52 162 +737 4 2 7 1 61 53 68 156 +738 4 2 7 1 94 95 88 158 +739 4 2 7 1 21 20 63 137 +740 4 2 7 1 138 43 42 101 +741 4 2 7 1 91 106 100 168 +742 4 2 7 1 99 106 91 168 +743 4 2 7 1 49 59 11 165 +744 4 2 7 1 121 109 113 153 +745 4 2 7 1 74 11 12 59 +746 4 2 7 1 36 97 35 73 +747 4 2 7 1 128 134 124 153 +748 4 2 7 1 109 118 114 154 +749 4 2 7 1 33 38 86 92 +750 4 2 7 1 132 128 125 154 +751 4 2 7 1 107 97 89 157 +752 4 2 7 1 145 3 120 65 +753 4 2 7 1 8 144 119 103 +754 4 2 7 1 35 97 87 164 +755 4 2 7 1 89 36 37 78 +756 4 2 7 1 12 13 77 51 +757 4 2 7 1 100 90 105 154 +758 4 2 7 1 140 145 136 166 +759 4 2 7 1 52 62 67 153 +760 4 2 7 1 60 59 49 165 +761 4 2 7 1 19 129 79 133 +762 4 2 7 1 129 135 18 79 +763 4 2 7 1 40 100 41 125 +764 4 2 7 1 124 17 16 62 +765 4 2 7 1 93 44 139 43 +766 4 2 7 1 98 96 99 159 +767 4 2 7 1 55 21 140 22 +768 4 2 7 1 85 75 79 163 +769 4 2 7 1 46 114 45 90 +770 4 2 7 1 27 28 113 52 +771 4 2 7 1 80 73 76 164 +772 4 2 7 1 135 14 1 85 +773 4 2 7 1 133 86 5 38 +774 4 2 7 1 73 82 78 157 +775 4 2 7 1 129 123 135 163 +776 4 2 7 1 26 25 50 115 +777 4 2 7 1 88 48 47 112 +778 4 2 7 1 4 67 134 121 +779 4 2 7 1 118 7 105 132 +780 4 2 7 1 66 2 83 146 +781 4 2 7 1 6 143 104 84 +782 4 2 7 1 24 141 71 23 +783 4 2 7 1 50 65 55 166 +784 4 2 7 1 109 128 31 32 +785 4 2 7 1 60 61 58 155 +786 4 2 7 1 112 116 110 158 +787 4 2 7 1 98 87 97 164 +788 4 2 7 1 111 27 56 26 +789 4 2 7 1 74 80 70 165 +790 4 2 7 1 94 46 110 47 +791 4 2 7 1 54 14 15 123 +792 4 2 7 1 140 142 137 160 +793 4 2 7 1 92 38 126 39 +794 4 2 7 1 113 117 111 162 +795 4 2 7 1 63 64 55 160 +796 4 2 7 1 9 1 14 85 +797 4 2 7 1 38 86 5 33 +798 4 2 7 1 54 10 9 75 +799 4 2 7 1 114 110 117 167 +800 4 2 7 1 34 33 72 92 +801 4 2 7 1 11 10 49 70 +802 4 2 7 1 22 3 145 65 +803 4 2 7 1 103 8 144 44 +804 4 2 7 1 146 66 2 20 +805 4 2 7 1 143 42 6 104 +806 4 2 7 1 144 30 8 119 +807 4 2 7 1 120 29 3 145 +808 4 2 7 1 34 76 35 87 +809 4 2 7 1 122 91 39 40 +810 4 2 7 1 119 48 8 103 +811 4 2 7 1 3 25 120 65 +812 4 2 7 1 79 18 129 19 +813 4 2 7 1 53 15 16 127 +814 4 2 7 1 67 17 4 134 +815 4 2 7 1 2 23 83 146 +816 4 2 7 1 6 143 84 24 +817 4 2 7 1 67 4 28 121 +818 4 2 7 1 45 7 105 118 +819 4 2 7 1 7 41 105 132 +820 4 2 7 1 132 118 7 32 +821 4 2 7 1 121 134 4 31 +822 4 2 7 1 37 6 104 84 +823 4 2 7 1 13 2 83 66 +824 4 2 7 1 135 1 18 85 +825 4 2 7 1 5 19 86 133 +826 4 2 7 1 166 116 152 115 +827 4 2 7 1 166 152 116 158 +828 4 2 7 1 69 152 155 57 +829 4 2 7 1 155 152 69 147 +830 4 2 7 1 158 119 108 149 +831 4 2 7 1 158 108 119 112 +832 4 2 7 1 155 69 165 147 +833 4 2 7 1 155 165 69 60 +834 4 2 7 1 81 165 163 70 +835 4 2 7 1 163 165 81 150 +836 4 2 7 1 162 109 167 153 +837 4 2 7 1 162 167 109 117 +838 4 2 7 1 101 143 161 104 +839 4 2 7 1 101 161 143 138 +840 4 2 7 1 66 137 20 146 +841 4 2 7 1 20 137 66 63 $EndElements From 877aff7b5591913ab38e81d299145b1c587bd047 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 12 May 2026 08:58:24 +0200 Subject: [PATCH 06/40] Found material assignments --- .../modart_interface/modart_interface.py | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index c9cfd96..3ec3338 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -2,7 +2,7 @@ """ import json from pathlib import Path -from pprint import pprint +from collections import defaultdict from .definition import SimulationMethod @@ -39,11 +39,6 @@ def run_simulation(self) -> None: print('\n\tDEBUG MESSAGE: reading .msh file at path:') print('\t', result_container['msh_path'], '\n') - with open(result_container['msh_path'], "r") as msh_file: - for line in msh_file: - print(line[:-1]) - print() - print('\n\tDEBUG MESSAGE: creating temp subfolder:') print('\t', temp_subfolder, '\n') if not Path.is_dir(temp_subfolder): @@ -53,18 +48,37 @@ def run_simulation(self) -> None: print('\n\tDEBUG MESSAGE: will write to temp file:') print('\t', obj_path, '\n') + print('\n\tDEBUG MESSAGE: loading mesh using meshio') + import meshio + mesh = meshio.read(result_container['msh_path']) + + print('\n\tDEBUG MESSAGE: contents of "mesh.cells":') + for cell in mesh.cells: + print(f'\t\ttype: {cell.type}\tshape: {cell.data.shape}') + + print('\n\tDEBUG MESSAGE: contents of "mesh.cell_data":') + for k, d in mesh.cell_data.items(): + for i, d_i in enumerate(d): + print(f'\t\tkey: {k}\tidx in key: {i}\tlen: {len(d_i)}') + print(f'\t\tcontents: {d_i}') + print() + + # mesh.cells = [cell for cell in mesh.cells + # if cell.type in ["triangle", "quad", "polygon"]] + + # This does not include the materials. + # mesh.write(obj_path) + + """ import gmsh gmsh.initialize() try: - print('\n\tDEBUG MESSAGE: converting mesh to Wavefront format; step 1: load .msh\n') - gmsh.open(result_container['msh_path']) - - print('\n\tDEBUG MESSAGE: converting mesh to Wavefront format; step 2: save .obj\n') - + # This does not include the materials. gmsh.write(obj_path) finally: gmsh.finalize() + """ # Save the updated JSON (with the added MoDART_data_subfolder field) with open(self.input_json_path, "w") as json_output: @@ -85,16 +99,6 @@ def _modart_method(self, json_file_path: str | Path) -> None: print('\n\tDEBUG MESSAGE: starting _modart_method\n') - obj_path = str(Path(result_container['MoDART_data_subfolder']) / 'mesh.obj') - - print('\n\tDEBUG MESSAGE: reading .obj file at path:') - print('\t', obj_path, '\n') - - with open(obj_path, "r") as obj_file: - for line in obj_file: - print(line[:-1]) - print() - # TODO: Implement your simulation logic here # 1. Extract simulation parameters from result_container # 2. Run your simulation From 004bc0ece7978d386d3e89bd39e638148c2a0865 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 12 May 2026 09:56:25 +0200 Subject: [PATCH 07/40] Mesh conversion works! --- .../modart_interface/modart_interface.py | 120 +++++++++++++----- 1 file changed, 89 insertions(+), 31 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 3ec3338..15fd7f1 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -1,8 +1,10 @@ """Module implementing a CHORAS interface for MoDART. """ import json +import numpy as np from pathlib import Path -from collections import defaultdict + +from raves.src.utils import visualize_mesh from .definition import SimulationMethod @@ -36,50 +38,102 @@ def run_simulation(self) -> None: temp_subfolder = Path(result_container['msh_path']).parent / 'MoDART_data' result_container['MoDART_data_subfolder'] = str(temp_subfolder) - print('\n\tDEBUG MESSAGE: reading .msh file at path:') + print('\tDEBUG MESSAGE: reading .msh file at path:') print('\t', result_container['msh_path'], '\n') - print('\n\tDEBUG MESSAGE: creating temp subfolder:') + print('\tDEBUG MESSAGE: creating temp subfolder:') print('\t', temp_subfolder, '\n') if not Path.is_dir(temp_subfolder): Path.mkdir(temp_subfolder) - obj_path = str(temp_subfolder / 'mesh.obj') - print('\n\tDEBUG MESSAGE: will write to temp file:') - print('\t', obj_path, '\n') - - print('\n\tDEBUG MESSAGE: loading mesh using meshio') - import meshio - mesh = meshio.read(result_container['msh_path']) - - print('\n\tDEBUG MESSAGE: contents of "mesh.cells":') - for cell in mesh.cells: - print(f'\t\ttype: {cell.type}\tshape: {cell.data.shape}') - - print('\n\tDEBUG MESSAGE: contents of "mesh.cell_data":') - for k, d in mesh.cell_data.items(): - for i, d_i in enumerate(d): - print(f'\t\tkey: {k}\tidx in key: {i}\tlen: {len(d_i)}') - print(f'\t\tcontents: {d_i}') - print() - - # mesh.cells = [cell for cell in mesh.cells - # if cell.type in ["triangle", "quad", "polygon"]] - - # This does not include the materials. - # mesh.write(obj_path) - """ import gmsh gmsh.initialize() try: gmsh.open(result_container['msh_path']) - # This does not include the materials. + # Doing this does not include the materials. gmsh.write(obj_path) finally: gmsh.finalize() """ + print('\tDEBUG MESSAGE: loading mesh using meshio') + import meshio + mesh = meshio.read(result_container['msh_path']) + + vertices = np.array(mesh.points).squeeze() + num_vertices = vertices.shape[0] + print('\tDEBUG MESSAGE: num vertices:', num_vertices) + print('\tDEBUG MESSAGE: first vertex:', vertices[0]) + + # TODO: Take care: if the number of triangles is the same as the number of some other type of element, the material retrieval will fail. + # TODO: Ensure that the mesh is triangulated. + + triangles = np.array([cell.data for cell in mesh.cells + if cell.type == "triangle"]).squeeze() + num_triangles = triangles.shape[0] + print('\tDEBUG MESSAGE: num triangles:', num_triangles) + print('\tDEBUG MESSAGE: first triangle:', triangles[0]) + + assert np.all(triangles < num_vertices), 'The triangle definitions include vertex indices out of range.' + + material_ids = np.array([l for l in mesh.cell_data['gmsh:physical'] + if len(l) == num_triangles]).squeeze() + print('\tDEBUG MESSAGE: materials:', material_ids) + + material_names = dict() + for k, [mat_idx, n_dims] in mesh.field_data.items(): + if n_dims == 2: + material_names[mat_idx] = k + print('\tDEBUG MESSAGE: field_data:', mat_idx, k) + print() + + # Doing this does not include the materials. + # mesh.write(obj_path) + + print('\tDEBUG MESSAGE: beginning .obj/.mtl assembly') + obj_output_lines = list() + mtl_output_lines = list() + + obj_output_lines.append('mtllib mesh.mtl\n') + + for v in vertices: + rounded_coords = [np.round(c, 3) for c in v] + line = 'v ' + ' '.join([str(c) for c in rounded_coords]) + '\n' + obj_output_lines.append(line) + for i in range(num_triangles): + patch_name = f'Patch_{i+1}_Mat_{material_names[material_ids[i]]}' + + obj_output_lines.append(f'usemtl {patch_name}\n') + obj_output_lines.append('f ' + ' '.join([str(v+1) for v in triangles[i]]) + '\n') + + mtl_output_lines.append(f'newmtl {patch_name}\n') + rand_color = np.round(np.random.uniform(size=3), 3) + mtl_output_lines.append(f'Kd {rand_color[0]} {rand_color[1]} {rand_color[2]}\n') + + # print('\n') + # for line in obj_output_lines: + # print(line[:-1]) + # print('\n') + + # print('\n') + # for line in mtl_output_lines: + # print(line[:-1]) + # print('\n') + + obj_path = str(temp_subfolder / 'mesh.obj') + mtl_path = str(temp_subfolder / 'mesh.mtl') + print('\tDEBUG MESSAGE: will write to temp files:') + print('\t\t', obj_path) + print('\t\t', mtl_path) + + with open(obj_path, mode='w') as file: + for line in obj_output_lines: + file.write(line) + with open(mtl_path, mode='w') as file: + for line in mtl_output_lines: + file.write(line) + # Save the updated JSON (with the added MoDART_data_subfolder field) with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) @@ -97,7 +151,11 @@ def _modart_method(self, json_file_path: str | Path) -> None: with open(json_file_path, "r") as json_file: result_container = json.load(json_file) - print('\n\tDEBUG MESSAGE: starting _modart_method\n') + print('\tDEBUG MESSAGE: starting _modart_method\n') + + visualize_mesh(result_container['MoDART_data_subfolder']) + + # raves(result_container['MoDART_data_subfolder']) # TODO: Implement your simulation logic here # 1. Extract simulation parameters from result_container @@ -124,7 +182,7 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Write results back to JSON # result_container["results"][0]["responses"][0]["receiverResults"] = results.tolist() - print('\n\tDEBUG MESSAGE: ending _modart_method\n') + print('\tDEBUG MESSAGE: ending _modart_method\n') # Save the updated JSON with open(json_file_path, "w") as json_output: From 472557aa095b367c9a8a3b749db4bd44986aac3c Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 12 May 2026 12:19:39 +0200 Subject: [PATCH 08/40] IT WORKS --- .../modart_interface/modart_interface.py | 219 +++++++++++++++--- modart_method/tests/conftest.py | 4 +- modart_method/tests/test_input_modart.json | 2 +- modart_method/tests/test_modart_cli.py | 13 +- .../tests/test_room_modart_simple.msh | 81 +++++++ 5 files changed, 282 insertions(+), 37 deletions(-) create mode 100644 modart_method/tests/test_room_modart_simple.msh diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 15fd7f1..f0bcfc7 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -4,8 +4,14 @@ import numpy as np from pathlib import Path +from pprint import pprint +from raves import raves, run_MoDART from raves.src.utils import visualize_mesh +from numpy.random import default_rng +from scipy.signal import butter, sosfilt +from scipy.interpolate import make_interp_spline + from .definition import SimulationMethod @@ -45,6 +51,21 @@ def run_simulation(self) -> None: print('\t', temp_subfolder, '\n') if not Path.is_dir(temp_subfolder): Path.mkdir(temp_subfolder) + + """ + import gmsh + gmsh.initialize() + try: + gmsh.open('C:/Users/matte/Desktop/CHORAS/simulation-backend/modart_method/tests/test_room_modart.geo') + gmsh.option.setNumber('Mesh.MeshSizeFactor', 6.0) + gmsh.option.setNumber('Mesh.SaveAll', 0) + gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) + gmsh.model.mesh.generate(2) + gmsh.write('C:/Users/matte/Desktop/CHORAS/simulation-backend/modart_method/tests/test_room_modart_simple.msh') + finally: + gmsh.finalize() + return + """ """ import gmsh @@ -75,6 +96,9 @@ def run_simulation(self) -> None: print('\tDEBUG MESSAGE: num triangles:', num_triangles) print('\tDEBUG MESSAGE: first triangle:', triangles[0]) + # N.B.: the triangle normals are inverted w.r.t. what MoD-ART expects. Flip them. + triangles = triangles[:, ::-1] + assert np.all(triangles < num_vertices), 'The triangle definitions include vertex indices out of range.' material_ids = np.array([l for l in mesh.cell_data['gmsh:physical'] @@ -123,7 +147,7 @@ def run_simulation(self) -> None: obj_path = str(temp_subfolder / 'mesh.obj') mtl_path = str(temp_subfolder / 'mesh.mtl') - print('\tDEBUG MESSAGE: will write to temp files:') + print('\tDEBUG MESSAGE: writing temp files:') print('\t\t', obj_path) print('\t\t', mtl_path) @@ -134,6 +158,45 @@ def run_simulation(self) -> None: for line in mtl_output_lines: file.write(line) + # TODO: For now we assume that the length of each + # result_container['absorption_coefficients'].values() + # is the same as the length of each + # result_container['results'][res_idx]['frequencies'] + # Eventually this will change. + freq_bands = None + for res in result_container['results']: + freqs = np.array(res['frequencies'], dtype=float) + if freq_bands is None: + freq_bands = freqs + else: + assert len(freq_bands) == len(freqs) + + absorptions = dict() + for material, coeff_string in result_container['absorption_coefficients'].items(): + coeffs = np.array(coeff_string.replace(',', '').split(' '), dtype=float) + if freq_bands is None: + raise RuntimeError('The frequencies should be known before the coefficients are read.') + else: + assert len(freq_bands) == len(coeffs) + absorptions[material] = coeffs + + csv_path = str(temp_subfolder / 'materials.csv') + print('\tDEBUG MESSAGE: writing temp file:') + print('\t\t', csv_path) + + with open(csv_path, mode='w') as file: + line = 'Frequencies, ' + ', '.join([str(f) for f in freq_bands]) + '\n' + file.write(line) + + for material, coeffs in absorptions.items(): + line = material + ', ' + ', '.join([str(c) for c in coeffs]) + '\n' + file.write(line) + + # TODO: For now, scattering coefficients are arbitrarily set to 0.3. + # Eventually they will be sent by the backend. + line = material + ', 0.3\n' + file.write(line) + # Save the updated JSON (with the added MoDART_data_subfolder field) with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) @@ -150,37 +213,133 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Load the input JSON file with open(json_file_path, "r") as json_file: result_container = json.load(json_file) - + print('\tDEBUG MESSAGE: starting _modart_method\n') - visualize_mesh(result_container['MoDART_data_subfolder']) - - # raves(result_container['MoDART_data_subfolder']) - - # TODO: Implement your simulation logic here - # 1. Extract simulation parameters from result_container - # 2. Run your simulation - # 3. Process results - # 4. Write results back to result_container - - # Example structure (modify based on your needs): - # simulation_settings = result_container["simulationSettings"] - # 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"], - # ] - - # Run your simulation here - # results = your_simulation_function(...) - - # Write results back to JSON - # result_container["results"][0]["responses"][0]["receiverResults"] = results.tolist() + print('\tDEBUG MESSAGE: JSON contents:') + pprint(result_container) + print() + + environment_folder = result_container['MoDART_data_subfolder'] + + # visualize_mesh(environment_folder) + + print('\tDEBUG MESSAGE: precomputation\n') + + raves(environment_folder) + + print('\tDEBUG MESSAGE: runtime computation') + + for sim_idx, sim_dict in enumerate(result_container['results']): + source_position = np.array([ + sim_dict['sourceX'], + sim_dict['sourceY'], + sim_dict['sourceZ'], + ]) + listener_positions = np.array([[pos['x'], pos['y'], pos['z']] + for pos in sim_dict['responses']]) + + # TODO: echogram_sample_rate will eventually be a parameter set by the user. + echogram_sample_rate = int(1e3) + # TODO: audio_sample_rate will eventually be a parameter set by...? + audio_sample_rate = int(44.1e3) + # TODO: this will eventually be named something else in the JSON. + response_duration = result_container['simulationSettings']['de_ir_length'] + + print('\t\tgenerating echograms') + + # Generate the echograms with MoD-ART. + MoDART_echograms, frequencies, _ = run_MoDART(environment_folder, + source_position, listener_positions, + echogram_duration=response_duration, + echogram_sample_rate=echogram_sample_rate) + + print('\t\techogram shape:', MoDART_echograms.shape) + + print('\t\tupsampling echograms') + + # Take note of the echogram energy, to compare it after upsampling. + old_energy = np.sum(MoDART_echograms, axis=-1) + + # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. + echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) + audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) + # We use a linear interpolation, because any other upsampling algorithm risks introducing negative values. + linear_spline = make_interp_spline(echogram_time_axis, MoDART_echograms, k=1, axis=-1) + upsampled_echograms = linear_spline(audio_time_axis) + + # Normalize w.r.t. the new sample rate, to preserve the energy-per-second definition of echogram values. + upsampled_echograms *= echogram_sample_rate / audio_sample_rate + + # Compare the new energy to the old one. + new_energy = np.sum(upsampled_echograms, axis=-1) + # The ratio (averaged over all frequency bands) should be close to 1 for all sources and listeners. + print('\t\techogram energy normalization:', old_energy / new_energy) + + print('\t\tamplitude modulation of noise signal') + + # Random number generator for the stochastic signal to be modulated. + rng = default_rng() + + # White noise + # noise_signal = rng.normal(size=len(audio_time_axis)) + # Poisson process + noise_signal = rng.poisson(lam=0.5, size=len(audio_time_axis)).astype(float) + + # Ensure the noise signal has unit energy per second, matching the + # convention used to generate the echograms. + noise_signal *= np.sqrt(response_duration / np.sum(noise_signal**2)) + + # Factor for octave-band boundaries. + # TODO: These may become third-octave bands at some point. + band_bound = np.sqrt(2) + # Consider the frequency band centers provided alongside the input data. + band_centers = frequencies + num_bands = len(frequencies) + + # Ensure that all frequencies support band-pass filtering. + if np.any(band_centers * band_bound >= audio_sample_rate): + print('Warning: the audio sample rate is too low for some frequency bands.') + # Select only acceptable bands. + band_centers = band_centers[band_centers * band_bound < audio_sample_rate] + # Update the number of rendered bands. + num_bands = len(band_centers) + # Drop unused bands from the echogram, to preserve the right shape. + upsampled_echograms = upsampled_echograms[:, :, :num_bands] + + # Prepare an array for the band-pass filtered signals. + filtered_noise_signals = np.zeros((num_bands, len(audio_time_axis))) + + print('\t\tfiltered_noise_signals shape:', filtered_noise_signals.shape) + + for b in range(num_bands): + # Prepare the suitable band-pass filter... + sos = butter(6, (band_centers[b] / band_bound, + band_centers[b] * band_bound), + btype='bandpass', output='sos', + fs=audio_sample_rate) + # ...and apply it to the stochastic signal. + filtered_noise_signals[b] = sosfilt(sos, noise_signal) + + # Translate the energy envelopes to amplitude envelopes. + envelopes = np.sqrt(upsampled_echograms) + + # The envelope array has shape (S, L, B, T), the noise signals have shape (B, T): + # we need to add two "leading" dimensions, which is done using [None, None]. + modulated_noise_signals = envelopes * filtered_noise_signals[None, None] + + print('\t\tmodulated_noise_signals shape:', modulated_noise_signals.shape) + + # The dimension of index 2 holds the separate frequency bands. + # Sum the array along that dimension to obtain the complete room impulse responses. + responses = np.sum(modulated_noise_signals, axis=2) + + print('\t\tresponses shape:', responses.shape) + + # Write results back to JSON. + for rec_idx in range(len(listener_positions)): + # Note that the first index of "responses" is for the single source position. + result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'] = responses[0, rec_idx].tolist() print('\tDEBUG MESSAGE: ending _modart_method\n') diff --git a/modart_method/tests/conftest.py b/modart_method/tests/conftest.py index 0164de3..84c2723 100644 --- a/modart_method/tests/conftest.py +++ b/modart_method/tests/conftest.py @@ -37,7 +37,7 @@ def create_temporary_input_file(): geo_file = os.path.join( default_data_path(), "test_room_modart.geo") msh_file = os.path.join( - default_data_path(), "test_room_modart.msh") + default_data_path(), "test_room_modart_simple.msh") with tempfile.TemporaryDirectory() as tmpdirname: tmp_path = Path(tmpdirname) / "temp_input.json" @@ -46,7 +46,7 @@ def create_temporary_input_file(): input_tmp['geo_path'] = os.path.join( tmpdirname, "test_room_modart.geo") input_tmp['msh_path'] = os.path.join( - tmpdirname, "test_room_modart.msh") + tmpdirname, "test_room_modart_simple.msh") with open(tmp_path, 'w') as f: json.dump(input_tmp, f) diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json index ee1707b..cae2fd0 100644 --- a/modart_method/tests/test_input_modart.json +++ b/modart_method/tests/test_input_modart.json @@ -1,6 +1,6 @@ { "geo_path": "test_room_modart.geo", - "msh_path": "test_room_modart.msh", + "msh_path": "test_room_modart_simple.msh", "absorption_coefficients": { "floor": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", "wall1": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index 07906f5..4823aa6 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -5,6 +5,8 @@ from modart_interface import main +import matplotlib.pyplot as plt + def test_modart_method_cli(mock_requests_post, create_temporary_input_file): """Test the MoDART method CLI.""" @@ -20,10 +22,13 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): # TODO: Add assertions specific to your simulation method # For example, check that results were written to the JSON file - # assert "receiverResults" in data['results'][0]['responses'][0] - # results = data['results'][0]['responses'][0]['receiverResults'] - # assert results is not None - # assert len(results) > 0 + assert "receiverResults" in data['results'][0]['responses'][0] + results = data['results'][0]['responses'][0]['receiverResults'] + assert results is not None + assert len(results) > 0 + + # plt.plot(results) + # plt.show() # Verify that requests.post was called (save_results was executed) mock_requests_post.assert_called_once() diff --git a/modart_method/tests/test_room_modart_simple.msh b/modart_method/tests/test_room_modart_simple.msh new file mode 100644 index 0000000..49ac2b2 --- /dev/null +++ b/modart_method/tests/test_room_modart_simple.msh @@ -0,0 +1,81 @@ +$MeshFormat +2.2 0 8 +$EndMeshFormat +$PhysicalNames +8 +1 8 "default" +2 1 "floor" +2 2 "wall1" +2 3 "ceiling" +2 4 "wall2" +2 5 "wall3" +2 6 "wall4" +3 7 "RoomVolume" +$EndPhysicalNames +$Nodes +17 +1 0 5.1 0 +2 6.21 4 0 +3 5.52 0 0 +4 0 0 0 +5 0 5.1 3.3 +6 6.21 4 3.3 +7 0 0 3.3 +8 5.52 0 3.3 +9 3.104999999991609 4.550000000001486 0 +10 3.104999999991609 4.550000000001486 3.3 +11 4.302270987143737 2.269570754717705 0 +12 4.657499999995805 4.275000000000743 1.650000000000001 +13 1.552499999995804 4.825000000000743 1.65 +14 4.302270987143737 2.269570754717706 3.3 +15 2.76 0 1.649999999999999 +16 0 2.55 1.65 +17 5.865 2 1.649999999999999 +$EndNodes +$Elements +44 +1 1 2 8 1 1 9 +2 1 2 8 1 9 2 +3 1 2 8 2 1 4 +4 1 2 8 3 1 5 +5 1 2 8 4 2 3 +6 1 2 8 5 2 6 +7 1 2 8 6 3 4 +8 1 2 8 7 3 8 +9 1 2 8 8 4 7 +10 1 2 8 9 5 10 +11 1 2 8 9 10 6 +12 1 2 8 10 5 7 +13 1 2 8 11 6 8 +14 1 2 8 12 7 8 +15 2 2 1 1 4 9 11 +16 2 2 1 1 3 4 11 +17 2 2 1 1 2 3 11 +18 2 2 1 1 9 2 11 +19 2 2 1 1 1 9 4 +20 2 2 2 2 5 13 1 +21 2 2 2 2 2 12 6 +22 2 2 2 2 9 12 2 +23 2 2 2 2 6 12 10 +24 2 2 2 2 9 13 12 +25 2 2 2 2 12 13 10 +26 2 2 2 2 1 13 9 +27 2 2 2 2 10 13 5 +28 2 2 3 3 7 14 10 +29 2 2 3 3 8 14 7 +30 2 2 3 3 6 14 8 +31 2 2 3 3 10 14 6 +32 2 2 3 3 10 5 7 +33 2 2 4 4 4 15 7 +34 2 2 4 4 8 15 3 +35 2 2 4 4 3 15 4 +36 2 2 4 4 7 15 8 +37 2 2 5 5 5 1 16 +38 2 2 5 5 4 7 16 +39 2 2 5 5 1 4 16 +40 2 2 5 5 7 5 16 +41 2 2 6 6 3 2 17 +42 2 2 6 6 6 8 17 +43 2 2 6 6 2 6 17 +44 2 2 6 6 8 3 17 +$EndElements From c58ca030b84330a471d3509b948ceba30a37d8b8 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 12 May 2026 13:46:51 +0200 Subject: [PATCH 09/40] Cleaned up --- .../modart_interface/modart_interface.py | 417 ++++++++---------- modart_method/tests/test_modart_cli.py | 5 - 2 files changed, 181 insertions(+), 241 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index f0bcfc7..920c85c 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -1,12 +1,11 @@ """Module implementing a CHORAS interface for MoDART. """ import json +import meshio import numpy as np from pathlib import Path -from pprint import pprint from raves import raves, run_MoDART -from raves.src.utils import visualize_mesh from numpy.random import default_rng from scipy.signal import butter, sosfilt @@ -15,6 +14,160 @@ from .definition import SimulationMethod +def convert_mesh(input_file_path: str | Path | None = None, + output_folder_path: str | Path | None = None): + if type(output_folder_path) == str: + output_folder_path = Path(output_folder_path) + + mesh = meshio.read(input_file_path) + + vertices = np.array(mesh.points).squeeze() + num_vertices = vertices.shape[0] + + # TODO: Take care: if the number of triangles is the same as the number of some other type of element, the material retrieval will fail. + + # TODO: Ensure that the mesh is triangulated. + + triangles = np.array([cell.data for cell in mesh.cells + if cell.type == "triangle"]).squeeze() + num_triangles = triangles.shape[0] + + # N.B.: the triangle normals are inverted w.r.t. what MoD-ART expects. Flip them. + triangles = triangles[:, ::-1] + + assert np.all(triangles < num_vertices), 'The triangle definitions include vertex indices out of range.' + + material_ids = np.array([l for l in mesh.cell_data['gmsh:physical'] + if len(l) == num_triangles]).squeeze() + + material_names = dict() + for k, [mat_idx, n_dims] in mesh.field_data.items(): + if n_dims == 2: + material_names[mat_idx] = k + + obj_output_lines = list() + mtl_output_lines = list() + + obj_output_lines.append('mtllib mesh.mtl\n') + + for v in vertices: + rounded_coords = [np.round(c, 3) for c in v] + line = 'v ' + ' '.join([str(c) for c in rounded_coords]) + '\n' + obj_output_lines.append(line) + for i in range(num_triangles): + patch_name = f'Patch_{i+1}_Mat_{material_names[material_ids[i]]}' + + obj_output_lines.append(f'usemtl {patch_name}\n') + obj_output_lines.append('f ' + ' '.join([str(v+1) for v in triangles[i]]) + '\n') + + mtl_output_lines.append(f'newmtl {patch_name}\n') + rand_color = np.round(np.random.uniform(size=3), 3) + mtl_output_lines.append(f'Kd {rand_color[0]} {rand_color[1]} {rand_color[2]}\n') + + with open(str(output_folder_path / 'mesh.obj'), mode='w') as file: + for line in obj_output_lines: + file.write(line) + with open(str(output_folder_path / 'mesh.mtl'), mode='w') as file: + for line in mtl_output_lines: + file.write(line) + + +def save_materials_file(json_file_path: str | Path): + # Load the input JSON file + with open(json_file_path, "r") as json_file: + result_container = json.load(json_file) + + # TODO: For now we assume that the length of each + # result_container['absorption_coefficients'].values() + # is the same as the length of each + # result_container['results'][res_idx]['frequencies'] + # Eventually this will change. + freq_bands = None + for res in result_container['results']: + freqs = np.array(res['frequencies'], dtype=float) + if freq_bands is None: + freq_bands = freqs + else: + assert len(freq_bands) == len(freqs) + + absorptions = dict() + for material, coeff_string in result_container['absorption_coefficients'].items(): + coeffs = np.array(coeff_string.replace(',', '').split(' '), dtype=float) + if freq_bands is None: + raise RuntimeError('The frequencies should be known before the coefficients are read.') + else: + assert len(freq_bands) == len(coeffs) + absorptions[material] = coeffs + + with open(str(Path(result_container['MoDART_data_subfolder']) / 'materials.csv'), mode='w') as file: + line = 'Frequencies, ' + ', '.join([str(f) for f in freq_bands]) + '\n' + file.write(line) + + for material, coeffs in absorptions.items(): + line = material + ', ' + ', '.join([str(c) for c in coeffs]) + '\n' + file.write(line) + + # TODO: For now, scattering coefficients are arbitrarily set to 0.3. + # Eventually they will be sent by the backend. + line = material + ', 0.3\n' + file.write(line) + + +def noise_shaping(fs: int | float, duration_in_samples: int, + frequencies: np.ndarray, envelopes: np.ndarray): + assert frequencies.ndim == 1 + assert envelopes.ndim == 4 + assert envelopes.shape[2] == frequencies.shape[0] + + # Random number generator for the stochastic signal to be modulated. + rng = default_rng() + + # White noise + # noise_signal = rng.normal(size=duration_in_samples) + # Poisson process + noise_signal = rng.poisson(lam=0.5, size=duration_in_samples).astype(float) + + # Ensure the noise signal has unit energy per second, matching the + # convention used to generate the echograms. + noise_signal *= np.sqrt(duration_in_samples * fs / np.sum(noise_signal**2)) + + # Factor for octave-band boundaries. + # TODO: These may become third-octave bands at some point. + band_bound = np.sqrt(2) + # Consider the frequency band centers provided alongside the input data. + band_centers = frequencies.copy() + num_bands = len(band_centers) + + # Ensure that all frequencies support band-pass filtering. + if np.any(band_centers * band_bound >= fs): + print('Warning: the audio sample rate is too low for some frequency bands.') + # Select only acceptable bands. + band_centers = band_centers[band_centers * band_bound < fs] + # Update the number of rendered bands. + num_bands = len(band_centers) + # Drop unused bands from the echogram, to preserve the right shape. + envelopes = envelopes[:, :, :num_bands] + + # Prepare an array for the band-pass filtered signals. + filtered_noise_signals = np.zeros((num_bands, duration_in_samples)) + + for b in range(num_bands): + # Prepare the suitable band-pass filter... + sos = butter(6, (band_centers[b] / band_bound, + band_centers[b] * band_bound), + btype='bandpass', output='sos', fs=fs) + # ...and apply it to the stochastic signal. + filtered_noise_signals[b] = sosfilt(sos, noise_signal) + + # The envelope array has shape (S, L, B, T), the noise signals have shape (B, T): + # we need to add two "leading" dimensions, which is done using [None, None]. + modulated_noise_signals = envelopes * filtered_noise_signals[None, None] + + # The dimension of index 2 holds the separate frequency bands. + # Sum the array along that dimension to obtain the complete room impulse responses. + return np.sum(modulated_noise_signals, axis=2) + + class MoDARTMethod(SimulationMethod): """Interface class to run the MoDART method. @@ -41,17 +194,17 @@ def run_simulation(self) -> None: with open(self.input_json_path, "r") as json_file: result_container = json.load(json_file) + # Create a folder for the "temporary" ART and MoD-ART data. temp_subfolder = Path(result_container['msh_path']).parent / 'MoDART_data' result_container['MoDART_data_subfolder'] = str(temp_subfolder) - - print('\tDEBUG MESSAGE: reading .msh file at path:') - print('\t', result_container['msh_path'], '\n') - - print('\tDEBUG MESSAGE: creating temp subfolder:') - print('\t', temp_subfolder, '\n') if not Path.is_dir(temp_subfolder): Path.mkdir(temp_subfolder) + # Save the updated JSON (with the added MoDART_data_subfolder field) + with open(self.input_json_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + # This was used to create a simplified "toy example" mesh for testing. """ import gmsh gmsh.initialize() @@ -67,140 +220,13 @@ def run_simulation(self) -> None: return """ - """ - import gmsh - gmsh.initialize() - try: - gmsh.open(result_container['msh_path']) - # Doing this does not include the materials. - gmsh.write(obj_path) - finally: - gmsh.finalize() - """ - - print('\tDEBUG MESSAGE: loading mesh using meshio') - import meshio - mesh = meshio.read(result_container['msh_path']) - - vertices = np.array(mesh.points).squeeze() - num_vertices = vertices.shape[0] - print('\tDEBUG MESSAGE: num vertices:', num_vertices) - print('\tDEBUG MESSAGE: first vertex:', vertices[0]) - - # TODO: Take care: if the number of triangles is the same as the number of some other type of element, the material retrieval will fail. - # TODO: Ensure that the mesh is triangulated. - - triangles = np.array([cell.data for cell in mesh.cells - if cell.type == "triangle"]).squeeze() - num_triangles = triangles.shape[0] - print('\tDEBUG MESSAGE: num triangles:', num_triangles) - print('\tDEBUG MESSAGE: first triangle:', triangles[0]) - - # N.B.: the triangle normals are inverted w.r.t. what MoD-ART expects. Flip them. - triangles = triangles[:, ::-1] - - assert np.all(triangles < num_vertices), 'The triangle definitions include vertex indices out of range.' - - material_ids = np.array([l for l in mesh.cell_data['gmsh:physical'] - if len(l) == num_triangles]).squeeze() - print('\tDEBUG MESSAGE: materials:', material_ids) - - material_names = dict() - for k, [mat_idx, n_dims] in mesh.field_data.items(): - if n_dims == 2: - material_names[mat_idx] = k - print('\tDEBUG MESSAGE: field_data:', mat_idx, k) - print() - - # Doing this does not include the materials. - # mesh.write(obj_path) - - print('\tDEBUG MESSAGE: beginning .obj/.mtl assembly') - obj_output_lines = list() - mtl_output_lines = list() - - obj_output_lines.append('mtllib mesh.mtl\n') - - for v in vertices: - rounded_coords = [np.round(c, 3) for c in v] - line = 'v ' + ' '.join([str(c) for c in rounded_coords]) + '\n' - obj_output_lines.append(line) - for i in range(num_triangles): - patch_name = f'Patch_{i+1}_Mat_{material_names[material_ids[i]]}' - - obj_output_lines.append(f'usemtl {patch_name}\n') - obj_output_lines.append('f ' + ' '.join([str(v+1) for v in triangles[i]]) + '\n') - - mtl_output_lines.append(f'newmtl {patch_name}\n') - rand_color = np.round(np.random.uniform(size=3), 3) - mtl_output_lines.append(f'Kd {rand_color[0]} {rand_color[1]} {rand_color[2]}\n') - - # print('\n') - # for line in obj_output_lines: - # print(line[:-1]) - # print('\n') - - # print('\n') - # for line in mtl_output_lines: - # print(line[:-1]) - # print('\n') - - obj_path = str(temp_subfolder / 'mesh.obj') - mtl_path = str(temp_subfolder / 'mesh.mtl') - print('\tDEBUG MESSAGE: writing temp files:') - print('\t\t', obj_path) - print('\t\t', mtl_path) - - with open(obj_path, mode='w') as file: - for line in obj_output_lines: - file.write(line) - with open(mtl_path, mode='w') as file: - for line in mtl_output_lines: - file.write(line) - - # TODO: For now we assume that the length of each - # result_container['absorption_coefficients'].values() - # is the same as the length of each - # result_container['results'][res_idx]['frequencies'] - # Eventually this will change. - freq_bands = None - for res in result_container['results']: - freqs = np.array(res['frequencies'], dtype=float) - if freq_bands is None: - freq_bands = freqs - else: - assert len(freq_bands) == len(freqs) - - absorptions = dict() - for material, coeff_string in result_container['absorption_coefficients'].items(): - coeffs = np.array(coeff_string.replace(',', '').split(' '), dtype=float) - if freq_bands is None: - raise RuntimeError('The frequencies should be known before the coefficients are read.') - else: - assert len(freq_bands) == len(coeffs) - absorptions[material] = coeffs - - csv_path = str(temp_subfolder / 'materials.csv') - print('\tDEBUG MESSAGE: writing temp file:') - print('\t\t', csv_path) - - with open(csv_path, mode='w') as file: - line = 'Frequencies, ' + ', '.join([str(f) for f in freq_bands]) + '\n' - file.write(line) + # Convert the .msh file into the format expected by MoD-ART. + convert_mesh(result_container['msh_path'], temp_subfolder) - for material, coeffs in absorptions.items(): - line = material + ', ' + ', '.join([str(c) for c in coeffs]) + '\n' - file.write(line) - - # TODO: For now, scattering coefficients are arbitrarily set to 0.3. - # Eventually they will be sent by the backend. - line = material + ', 0.3\n' - file.write(line) + # Save the material information into the .csv file expected by MoD-ART. + save_materials_file(self.input_json_path) - # Save the updated JSON (with the added MoDART_data_subfolder field) - with open(self.input_json_path, "w") as json_output: - json_output.write(json.dumps(result_container, indent=4)) - + # Run MoD-ART. self._modart_method(self.input_json_path) def _modart_method(self, json_file_path: str | Path) -> None: @@ -214,53 +240,32 @@ def _modart_method(self, json_file_path: str | Path) -> None: with open(json_file_path, "r") as json_file: result_container = json.load(json_file) - print('\tDEBUG MESSAGE: starting _modart_method\n') - - print('\tDEBUG MESSAGE: JSON contents:') - pprint(result_container) - print() - environment_folder = result_container['MoDART_data_subfolder'] + # TODO: this will eventually be named differently in the JSON. + response_duration = result_container['simulationSettings']['de_ir_length'] - # visualize_mesh(environment_folder) - - print('\tDEBUG MESSAGE: precomputation\n') + # TODO: echogram_sample_rate will eventually be a parameter set by the user. + echogram_sample_rate = int(1e3) + # TODO: audio_sample_rate will eventually be a parameter set by...? + audio_sample_rate = int(44.1e3) + # Run the pre-processing (shared by all sources, listeners). + # TODO: Update progress bar. raves(environment_folder) - print('\tDEBUG MESSAGE: runtime computation') - for sim_idx, sim_dict in enumerate(result_container['results']): - source_position = np.array([ - sim_dict['sourceX'], - sim_dict['sourceY'], - sim_dict['sourceZ'], - ]) + source_position = np.array([sim_dict['sourceX'], + sim_dict['sourceY'], + sim_dict['sourceZ']]) listener_positions = np.array([[pos['x'], pos['y'], pos['z']] for pos in sim_dict['responses']]) - # TODO: echogram_sample_rate will eventually be a parameter set by the user. - echogram_sample_rate = int(1e3) - # TODO: audio_sample_rate will eventually be a parameter set by...? - audio_sample_rate = int(44.1e3) - # TODO: this will eventually be named something else in the JSON. - response_duration = result_container['simulationSettings']['de_ir_length'] - - print('\t\tgenerating echograms') - # Generate the echograms with MoD-ART. MoDART_echograms, frequencies, _ = run_MoDART(environment_folder, source_position, listener_positions, echogram_duration=response_duration, echogram_sample_rate=echogram_sample_rate) - print('\t\techogram shape:', MoDART_echograms.shape) - - print('\t\tupsampling echograms') - - # Take note of the echogram energy, to compare it after upsampling. - old_energy = np.sum(MoDART_echograms, axis=-1) - # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) @@ -270,79 +275,19 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Normalize w.r.t. the new sample rate, to preserve the energy-per-second definition of echogram values. upsampled_echograms *= echogram_sample_rate / audio_sample_rate - - # Compare the new energy to the old one. - new_energy = np.sum(upsampled_echograms, axis=-1) - # The ratio (averaged over all frequency bands) should be close to 1 for all sources and listeners. - print('\t\techogram energy normalization:', old_energy / new_energy) - - print('\t\tamplitude modulation of noise signal') - - # Random number generator for the stochastic signal to be modulated. - rng = default_rng() - - # White noise - # noise_signal = rng.normal(size=len(audio_time_axis)) - # Poisson process - noise_signal = rng.poisson(lam=0.5, size=len(audio_time_axis)).astype(float) - - # Ensure the noise signal has unit energy per second, matching the - # convention used to generate the echograms. - noise_signal *= np.sqrt(response_duration / np.sum(noise_signal**2)) - - # Factor for octave-band boundaries. - # TODO: These may become third-octave bands at some point. - band_bound = np.sqrt(2) - # Consider the frequency band centers provided alongside the input data. - band_centers = frequencies - num_bands = len(frequencies) - - # Ensure that all frequencies support band-pass filtering. - if np.any(band_centers * band_bound >= audio_sample_rate): - print('Warning: the audio sample rate is too low for some frequency bands.') - # Select only acceptable bands. - band_centers = band_centers[band_centers * band_bound < audio_sample_rate] - # Update the number of rendered bands. - num_bands = len(band_centers) - # Drop unused bands from the echogram, to preserve the right shape. - upsampled_echograms = upsampled_echograms[:, :, :num_bands] - - # Prepare an array for the band-pass filtered signals. - filtered_noise_signals = np.zeros((num_bands, len(audio_time_axis))) - - print('\t\tfiltered_noise_signals shape:', filtered_noise_signals.shape) - - for b in range(num_bands): - # Prepare the suitable band-pass filter... - sos = butter(6, (band_centers[b] / band_bound, - band_centers[b] * band_bound), - btype='bandpass', output='sos', - fs=audio_sample_rate) - # ...and apply it to the stochastic signal. - filtered_noise_signals[b] = sosfilt(sos, noise_signal) - + # Translate the energy envelopes to amplitude envelopes. envelopes = np.sqrt(upsampled_echograms) - - # The envelope array has shape (S, L, B, T), the noise signals have shape (B, T): - # we need to add two "leading" dimensions, which is done using [None, None]. - modulated_noise_signals = envelopes * filtered_noise_signals[None, None] - - print('\t\tmodulated_noise_signals shape:', modulated_noise_signals.shape) - - # The dimension of index 2 holds the separate frequency bands. - # Sum the array along that dimension to obtain the complete room impulse responses. - responses = np.sum(modulated_noise_signals, axis=2) - print('\t\tresponses shape:', responses.shape) + # Amplitude-modulate band-passed stochastic signals to produce an impulse response. + responses = noise_shaping(audio_sample_rate, len(audio_time_axis), + frequencies, envelopes) # Write results back to JSON. for rec_idx in range(len(listener_positions)): # Note that the first index of "responses" is for the single source position. result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'] = responses[0, rec_idx].tolist() - print('\tDEBUG MESSAGE: ending _modart_method\n') - # Save the updated JSON with open(json_file_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index 4823aa6..8b118a1 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -5,8 +5,6 @@ from modart_interface import main -import matplotlib.pyplot as plt - def test_modart_method_cli(mock_requests_post, create_temporary_input_file): """Test the MoDART method CLI.""" @@ -27,9 +25,6 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): assert results is not None assert len(results) > 0 - # plt.plot(results) - # plt.show() - # Verify that requests.post was called (save_results was executed) mock_requests_post.assert_called_once() From b3063806cc21f1e6df2ddaaae3655c09f6104ead Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 12 May 2026 15:09:02 +0200 Subject: [PATCH 10/40] Docker setup --- example_settings/modart_setting.json | 100 ++++++++++++++++++ methods-config.json | 10 ++ .../modart_interface/modart_interface.py | 2 + 3 files changed, 112 insertions(+) create mode 100644 example_settings/modart_setting.json diff --git a/example_settings/modart_setting.json b/example_settings/modart_setting.json new file mode 100644 index 0000000..7b14e6a --- /dev/null +++ b/example_settings/modart_setting.json @@ -0,0 +1,100 @@ +{ + "type": "simulationSettings", + "options": [ + { + "name": "Echogram sample rate", + "id": "f_e", + "type": "float", + "display": "text", + "min": 5e1, + "max": 5e4, + "default": 5e3, + "step": 5e1, + "endAdornment": "Hz" + }, + { + "name": "T60 threshold", + "id": "T60", + "type": "float", + "display": "text", + "min": 1e-3, + "max": 1e1, + "default": 1e-1, + "step": 1e-2, + "endAdornment": "s" + }, + { + "name": "Max slopes", + "id": "slopes", + "type": "int", + "display": "text", + "min": 1, + "max": 100, + "default": 10, + "step": 1 + }, + { + "name": "Air humidity", + "id": "humi", + "type": "float", + "display": "text", + "min": 0.0, + "max": 100.0, + "default": 50.0, + "step": 5.0, + "endAdornment": "%" + }, + { + "name": "Air temperature", + "id": "temp", + "type": "float", + "display": "text", + "min": -100.0, + "max": 100.0, + "default": 20.0, + "step": 1.0, + "endAdornment": "°C" + }, + { + "name": "Atmospheric pressure", + "id": "temp", + "type": "float", + "display": "text", + "min": 50.0, + "max": 150.0, + "default": 100.0, + "step": 0.1, + "endAdornment": "kPa" + }, + { + "name": "Points per square meter", + "id": "ppsm", + "type": "float", + "display": "text", + "min": 0, + "max": 100.0, + "default": 30.0, + "step": 0.1 + }, + { + "name": "Rays per hemisphere", + "id": "rays", + "type": "int", + "display": "text", + "min": 10, + "max": 10000, + "default": 1000, + "step": 100 + }, + { + "name": "Max parallel processes", + "id": "pool", + "type": "int", + "display": "text", + "min": 1, + "max": 10000, + "default": 4, + "step": 1 + } + ] +} diff --git a/methods-config.json b/methods-config.json index 2683ca6..151abf7 100644 --- a/methods-config.json +++ b/methods-config.json @@ -19,6 +19,16 @@ "repositoryURL":"https://github.com/Building-acoustics-TU-Eindhoven/acousticDE/", "documentationURL":"https://building-acoustics-tu-eindhoven.github.io/acousticDE/index.html" }, + { + "simulationType": "MoDART", + "containerImage": "modart_image:latest", + "envVars": {}, + "label": "MoD-ART", + "entryFile":"modart_interface.py", + "settings":"modart_setting.json", + "repositoryURL":"https://github.com/IoSR-Surrey/MoD-ART/", + "documentationURL":"https://github.com/IoSR-Surrey/MoD-ART/blob/master/example%20usage/Tutorial.ipynb" + }, { "simulationType": "MyNewMethod", "containerImage": "mynewmethod_image:latest", diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 920c85c..b9d1512 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -243,6 +243,8 @@ def _modart_method(self, json_file_path: str | Path) -> None: environment_folder = result_container['MoDART_data_subfolder'] # TODO: this will eventually be named differently in the JSON. response_duration = result_container['simulationSettings']['de_ir_length'] + + # TODO: Load and use simulation settings. # TODO: echogram_sample_rate will eventually be a parameter set by the user. echogram_sample_rate = int(1e3) From ef87f46ceb8b6efd035b6902749a6c53e41bca1e Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 12 May 2026 21:25:23 +0200 Subject: [PATCH 11/40] Released raves on PyPI (Docker still doesn't work though) --- modart_method/Dockerfile | 1 + modart_method/pyproject.toml | 23 +++-------------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/modart_method/Dockerfile b/modart_method/Dockerfile index b766d96..bcb8060 100644 --- a/modart_method/Dockerfile +++ b/modart_method/Dockerfile @@ -9,6 +9,7 @@ RUN apt-get update && apt-get install -y \ build-essential \ gmsh \ && rm -rf /var/lib/apt/lists/* +# Note: I tried removing "gmsh \" from the command above, but the build took ages. Why? # Copy method package directory COPY modart_method /app/modart_method diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index 13a647f..05dc50b 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -7,13 +7,9 @@ authors = [ { name = "Matteo Scerbo", email = "matteo.scerbo@gmail.com" }, ] keywords = [ - "acoustic simulation", - "geometrical acoustics", - "acoustic radiance transfer", - ] classifiers = [ @@ -22,29 +18,16 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - ] -dependencies = [ - - "numpy>=2.2.5", - - "matplotlib>=3.8.4", - - "scipy>=1.14.1", - - "tqdm>=4.67.1", - - "raves @ git+https://github.com/IoSR-Surrey/MoD-ART.git@eb047d21ddf406859ea51b74e9d47e5dfe21365a", +dependencies = [ + "raves>=0.1.1", + "meshio>=5.3.5", "requests", - "gmsh", ] [project.optional-dependencies] From c25432fa7cd7eab97d73f860c4940a625498ac1a Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Wed, 13 May 2026 12:50:09 +0200 Subject: [PATCH 12/40] Adapted mesh conversion to the format currently used in CHORAS --- .../modart_interface/modart_interface.py | 115 +- modart_method/tests/conftest.py | 4 +- modart_method/tests/test_input_modart.json | 65 +- modart_method/tests/test_room_modart.geo | 66 +- modart_method/tests/test_room_modart.msh | 1266 ++++------------- .../tests/test_room_modart_simple.msh | 81 -- 6 files changed, 395 insertions(+), 1202 deletions(-) delete mode 100644 modart_method/tests/test_room_modart_simple.msh diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index b9d1512..5060cea 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -3,9 +3,11 @@ import json import meshio import numpy as np +from pprint import pprint from pathlib import Path from raves import raves, run_MoDART +from raves.src.utils import visualize_mesh from numpy.random import default_rng from scipy.signal import butter, sosfilt @@ -19,32 +21,61 @@ def convert_mesh(input_file_path: str | Path | None = None, if type(output_folder_path) == str: output_folder_path = Path(output_folder_path) + print('\n\nSTARTING MESH CONVERSION.\n') + + # TODO: Ensure that the mesh is triangulated. + mesh = meshio.read(input_file_path) vertices = np.array(mesh.points).squeeze() num_vertices = vertices.shape[0] - # TODO: Take care: if the number of triangles is the same as the number of some other type of element, the material retrieval will fail. - - # TODO: Ensure that the mesh is triangulated. - - triangles = np.array([cell.data for cell in mesh.cells - if cell.type == "triangle"]).squeeze() + triangles = list() + triangle_cell_ids = list() + triangle_group_ids = list() + num_groups = 0 + for cell_id, cell in enumerate(mesh.cells): + if cell.type == 'triangle': + group = cell.data + + if group.ndim == 1: + assert group.shape[0] == 3, 'Bad cell shape.' + triangles.append(group) + triangle_cell_ids.append(cell_id) + triangle_group_ids.append(num_groups) + + elif group.ndim == 2: + assert group.shape[1] == 3, 'Bad cell shape.' + for tri in group: + triangles.append(tri) + triangle_cell_ids.append(cell_id) + triangle_group_ids.append(num_groups) + else: + raise AssertionError('Bad cell shape.') + + num_groups += 1 + + triangles = np.array(triangles) num_triangles = triangles.shape[0] - + triangle_cell_ids = np.array(triangle_cell_ids) + triangle_group_ids = np.array(triangle_group_ids) + # N.B.: the triangle normals are inverted w.r.t. what MoD-ART expects. Flip them. triangles = triangles[:, ::-1] assert np.all(triangles < num_vertices), 'The triangle definitions include vertex indices out of range.' - material_ids = np.array([l for l in mesh.cell_data['gmsh:physical'] - if len(l) == num_triangles]).squeeze() - material_names = dict() for k, [mat_idx, n_dims] in mesh.field_data.items(): if n_dims == 2: material_names[mat_idx] = k + triangle_materials = list() + for tri_id in range(num_triangles): + # TODO: Consider the possibility of non-uniform materials within group; split group if that's the case. + mat_id = mesh.cell_data['gmsh:physical'][triangle_cell_ids[tri_id]][0] + triangle_materials.append(material_names[mat_id]) + obj_output_lines = list() mtl_output_lines = list() @@ -55,7 +86,7 @@ def convert_mesh(input_file_path: str | Path | None = None, line = 'v ' + ' '.join([str(c) for c in rounded_coords]) + '\n' obj_output_lines.append(line) for i in range(num_triangles): - patch_name = f'Patch_{i+1}_Mat_{material_names[material_ids[i]]}' + patch_name = f'Patch_{triangle_group_ids[i]+1}_Mat_{triangle_materials[i]}' obj_output_lines.append(f'usemtl {patch_name}\n') obj_output_lines.append('f ' + ' '.join([str(v+1) for v in triangles[i]]) + '\n') @@ -193,7 +224,7 @@ def run_simulation(self) -> None: # Load the input JSON file with open(self.input_json_path, "r") as json_file: result_container = json.load(json_file) - + # Create a folder for the "temporary" ART and MoD-ART data. temp_subfolder = Path(result_container['msh_path']).parent / 'MoDART_data' result_container['MoDART_data_subfolder'] = str(temp_subfolder) @@ -204,28 +235,18 @@ def run_simulation(self) -> None: with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) - # This was used to create a simplified "toy example" mesh for testing. - """ - import gmsh - gmsh.initialize() - try: - gmsh.open('C:/Users/matte/Desktop/CHORAS/simulation-backend/modart_method/tests/test_room_modart.geo') - gmsh.option.setNumber('Mesh.MeshSizeFactor', 6.0) - gmsh.option.setNumber('Mesh.SaveAll', 0) - gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) - gmsh.model.mesh.generate(2) - gmsh.write('C:/Users/matte/Desktop/CHORAS/simulation-backend/modart_method/tests/test_room_modart_simple.msh') - finally: - gmsh.finalize() - return - """ - # Convert the .msh file into the format expected by MoD-ART. - convert_mesh(result_container['msh_path'], temp_subfolder) + try: + convert_mesh(result_container['msh_path'], temp_subfolder) + except Exception as exc: + raise RuntimeError('Failed to reformat the input mesh as required.') from exc # Save the material information into the .csv file expected by MoD-ART. - save_materials_file(self.input_json_path) - + try: + save_materials_file(self.input_json_path) + except Exception as exc: + raise RuntimeError('Failed to reformat the material properties as required.') from exc + # Run MoD-ART. self._modart_method(self.input_json_path) @@ -241,6 +262,9 @@ def _modart_method(self, json_file_path: str | Path) -> None: result_container = json.load(json_file) environment_folder = result_container['MoDART_data_subfolder'] + + visualize_mesh(environment_folder) + # TODO: this will eventually be named differently in the JSON. response_duration = result_container['simulationSettings']['de_ir_length'] @@ -250,10 +274,15 @@ def _modart_method(self, json_file_path: str | Path) -> None: echogram_sample_rate = int(1e3) # TODO: audio_sample_rate will eventually be a parameter set by...? audio_sample_rate = int(44.1e3) + + raise NotImplementedError('Stopping here.') # Run the pre-processing (shared by all sources, listeners). # TODO: Update progress bar. - raves(environment_folder) + try: + raves(environment_folder) + except Exception as exc: + raise RuntimeError('Failed to run the pre-processing environment analysis.') from exc for sim_idx, sim_dict in enumerate(result_container['results']): source_position = np.array([sim_dict['sourceX'], @@ -263,11 +292,14 @@ def _modart_method(self, json_file_path: str | Path) -> None: for pos in sim_dict['responses']]) # Generate the echograms with MoD-ART. - MoDART_echograms, frequencies, _ = run_MoDART(environment_folder, - source_position, listener_positions, - echogram_duration=response_duration, - echogram_sample_rate=echogram_sample_rate) - + try: + MoDART_echograms, frequencies, _ = run_MoDART(environment_folder, + source_position, listener_positions, + echogram_duration=response_duration, + echogram_sample_rate=echogram_sample_rate) + except Exception as exc: + raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc + # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) @@ -282,9 +314,12 @@ def _modart_method(self, json_file_path: str | Path) -> None: envelopes = np.sqrt(upsampled_echograms) # Amplitude-modulate band-passed stochastic signals to produce an impulse response. - responses = noise_shaping(audio_sample_rate, len(audio_time_axis), - frequencies, envelopes) - + try: + responses = noise_shaping(audio_sample_rate, len(audio_time_axis), + frequencies, envelopes) + except Exception as exc: + raise RuntimeError(f'Failed to generate responses for simulation #{sim_idx+1}.') from exc + # Write results back to JSON. for rec_idx in range(len(listener_positions)): # Note that the first index of "responses" is for the single source position. diff --git a/modart_method/tests/conftest.py b/modart_method/tests/conftest.py index 84c2723..0164de3 100644 --- a/modart_method/tests/conftest.py +++ b/modart_method/tests/conftest.py @@ -37,7 +37,7 @@ def create_temporary_input_file(): geo_file = os.path.join( default_data_path(), "test_room_modart.geo") msh_file = os.path.join( - default_data_path(), "test_room_modart_simple.msh") + default_data_path(), "test_room_modart.msh") with tempfile.TemporaryDirectory() as tmpdirname: tmp_path = Path(tmpdirname) / "temp_input.json" @@ -46,7 +46,7 @@ def create_temporary_input_file(): input_tmp['geo_path'] = os.path.join( tmpdirname, "test_room_modart.geo") input_tmp['msh_path'] = os.path.join( - tmpdirname, "test_room_modart_simple.msh") + tmpdirname, "test_room_modart.msh") with open(tmp_path, 'w') as f: json.dump(input_tmp, f) diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json index cae2fd0..bb8d96b 100644 --- a/modart_method/tests/test_input_modart.json +++ b/modart_method/tests/test_input_modart.json @@ -1,43 +1,39 @@ { - "geo_path": "test_room_modart.geo", - "msh_path": "test_room_modart_simple.msh", "absorption_coefficients": { - "floor": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", - "wall1": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", - "ceiling": "0.4, 0.5, 0.6, 0.7, 0.8, 0.9", - "wall2": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", - "wall3": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15", - "wall4": "0.05, 0.05, 0.08, 0.1, 0.12, 0.15" - }, - "simulationSettings": { - "sim_len_type": "edt", - "edt": 30, - "de_ir_length": 0.5, - "de_lc": 0.5 + "1f791966-f811-4749-b3cf-6a2342a2f5fb": "0.02, 0.03, 0.04, 0.08, 0.15", + "300368c5-62b4-ec42-bea3-bce21338c848": "0.02, 0.03, 0.04, 0.08, 0.15", + "a89bb798-c0bc-0a40-b63f-4a07eb04fa22": "0.11, 0.22, 0.42, 0.57, 0.63", + "b51c2cdd-1035-224e-a373-8d4b9d277b38": "0.02, 0.03, 0.04, 0.08, 0.15", + "e9774010-4e39-2847-b243-a52df0a2a06c": "0.02, 0.03, 0.04, 0.08, 0.15", + "fc78b514-6596-764c-864a-6cfd3c416947": "0.01, 0.01, 0.01, 0.01, 0.01" }, + "msh_path": "/app/uploads/MeasurementRoom_tiny_7fe67a12829349da9195f35e4bb5aceb.msh", + "geo_path": "/app/uploads/MeasurementRoom_tiny_7fe67a12829349da9195f35e4bb5aceb.geo", "results": [ { - "simulationMethodId": 1, - "resultType": "IR", - "simulationId": 1, - "sourceX": 1.0, - "sourceY": 2.0, - "sourceZ": 1.5, + "label": "Source 1", + "orderNumber": 1, + "percentage": 0, + "sourcePointId": "fbc3e3f4-62ba-409b-87c4-8ad6da93713d", + "sourceX": 0.3, + "sourceY": 0.3, + "sourceZ": 0.3, + "resultType": "MoDART", "frequencies": [ 125, 250, 500, 1000, - 2000, - 4000 + 2000 ], - "percentage": 0, "responses": [ { - "responseId": 1, - "x": 5.0, - "y": 3.5, - "z": 1.5, + "label": "Receiver 1", + "orderNumber": 1, + "x": 0.6, + "y": 0.6, + "z": 0.3, + "pointId": "66821432-a860-4051-acf7-f3e719e479a3", "parameters": { "edt": [], "t20": [], @@ -51,5 +47,18 @@ } ] } - ] + ], + "task_id": "6511d180-1431-4d89-bcd5-a81dceab6f3a", + "fs_auralization": 44100, + "simulationSettings": { + "f_e": 5000, + "T60": 0.1, + "slopes": 10, + "humi": 50, + "temp": 100, + "ppsm": 30, + "rays": 1000, + "pool": 4 + }, + "settingsPreset": "Default" } diff --git a/modart_method/tests/test_room_modart.geo b/modart_method/tests/test_room_modart.geo index db900ed..72a8c08 100644 --- a/modart_method/tests/test_room_modart.geo +++ b/modart_method/tests/test_room_modart.geo @@ -1,31 +1,31 @@ -Point(1) = { 0.000000, 5.100000, 0.000000, 1.0 }; -Point(2) = { 6.210000, 4.000000, 0.000000, 1.0 }; -Point(3) = { 5.520000, 0.000000, 0.000000, 1.0 }; -Point(4) = { 0.000000, 0.000000, 0.000000, 1.0 }; -Point(5) = { 0.000000, 5.100000, 3.300000, 1.0 }; -Point(6) = { 6.210000, 4.000000, 3.300000, 1.0 }; -Point(7) = { 0.000000, 0.000000, 3.300000, 1.0 }; -Point(8) = { 5.520000, 0.000000, 3.300000, 1.0 }; +Point(1) = { 0.0, 0.0, 0.0, 1.0 }; +Point(2) = { 0.0, 0.0, 0.66, 1.0 }; +Point(3) = { 0.0, 1.02, 0.0, 1.0 }; +Point(4) = { 0.0, 1.02, 0.66, 1.0 }; +Point(5) = { 1.104, 0.0, 0.0, 1.0 }; +Point(6) = { 1.104, 0.0, 0.66, 1.0 }; +Point(7) = { 1.242, 0.8, 0.0, 1.0 }; +Point(8) = { 1.242, 0.8, 0.66, 1.0 }; -Line(1) = { 1, 2 }; -Line(2) = { 1, 4 }; +Line(1) = { 3, 7 }; +Line(2) = { 5, 7 }; Line(3) = { 1, 5 }; -Line(4) = { 2, 3 }; -Line(5) = { 2, 6 }; -Line(6) = { 3, 4 }; -Line(7) = { 3, 8 }; -Line(8) = { 4, 7 }; -Line(9) = { 5, 6 }; -Line(10) = { 5, 7 }; -Line(11) = { 6, 8 }; -Line(12) = { 7, 8 }; +Line(4) = { 1, 3 }; +Line(5) = { 4, 8 }; +Line(6) = { 7, 8 }; +Line(7) = { 3, 4 }; +Line(8) = { 2, 6 }; +Line(9) = { 6, 8 }; +Line(10) = { 2, 4 }; +Line(11) = { 1, 2 }; +Line(12) = { 5, 6 }; -Line Loop(1) = { 6, -2, 1, 4 }; -Line Loop(2) = { -1, 3, 9, -5 }; -Line Loop(3) = { -9, 10, 12, -11 }; -Line Loop(4) = { -6, 7, -12, -8 }; -Line Loop(5) = { 2, 8, -10, -3 }; -Line Loop(6) = { 11, -7, -4, 5 }; +Line Loop(1) = { 1, -2, -3, 4 }; +Line Loop(2) = { 5, -6, -1, 7 }; +Line Loop(3) = { 8, 9, -5, -10 }; +Line Loop(4) = { -8, -11, 3, 12 }; +Line Loop(5) = { 10, -7, -4, 11 }; +Line Loop(6) = { 2, 6, -9, -12 }; Plane Surface(1) = { 1 }; Plane Surface(2) = { 2 }; @@ -35,15 +35,15 @@ Plane Surface(5) = { 5 }; Plane Surface(6) = { 6 }; Surface Loop(1) = { 1, 2, 3, 4, 5, 6 }; -Physical Surface("floor") = { 1 }; -Physical Surface("wall1") = { 2 }; -Physical Surface("ceiling") = { 3 }; -Physical Surface("wall2") = { 4 }; -Physical Surface("wall3") = { 5 }; -Physical Surface("wall4") = { 6 }; -Volume( 1 ) = { 1 }; +Physical Surface("a89bb798-c0bc-0a40-b63f-4a07eb04fa22") = { 1 }; +Physical Surface("e9774010-4e39-2847-b243-a52df0a2a06c") = { 2 }; +Physical Surface("fc78b514-6596-764c-864a-6cfd3c416947") = { 3 }; +Physical Surface("300368c5-62b4-ec42-bea3-bce21338c848") = { 4 }; +Physical Surface("b51c2cdd-1035-224e-a373-8d4b9d277b38") = { 5 }; +Physical Surface("1f791966-f811-4749-b3cf-6a2342a2f5fb") = { 6 }; +Volume(1) = { 1 }; Physical Volume("RoomVolume") = { 1 }; -Physical Line ("default") = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; +Physical Line("default") = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; Mesh.Algorithm = 6; Mesh.Algorithm3D = 1; // Delaunay3D, works for boundary layer insertion. Mesh.Optimize = 1; // Gmsh smoother, works with boundary layers (netgen version does not). diff --git a/modart_method/tests/test_room_modart.msh b/modart_method/tests/test_room_modart.msh index e575648..20380ca 100644 --- a/modart_method/tests/test_room_modart.msh +++ b/modart_method/tests/test_room_modart.msh @@ -1,1029 +1,259 @@ $MeshFormat -2.2 0 8 +4.1 0 8 $EndMeshFormat $PhysicalNames 8 1 8 "default" -2 1 "floor" -2 2 "wall1" -2 3 "ceiling" -2 4 "wall2" -2 5 "wall3" -2 6 "wall4" +2 1 "a89bb798-c0bc-0a40-b63f-4a07eb04fa22" +2 2 "e9774010-4e39-2847-b243-a52df0a2a06c" +2 3 "fc78b514-6596-764c-864a-6cfd3c416947" +2 4 "300368c5-62b4-ec42-bea3-bce21338c848" +2 5 "b51c2cdd-1035-224e-a373-8d4b9d277b38" +2 6 "1f791966-f811-4749-b3cf-6a2342a2f5fb" 3 7 "RoomVolume" $EndPhysicalNames +$Entities +8 12 6 1 +1 0 0 0 0 +2 0 0 0.66 0 +3 0 1.02 0 0 +4 0 1.02 0.66 0 +5 1.104 0 0 0 +6 1.104 0 0.66 0 +7 1.242 0.8 0 0 +8 1.242 0.8 0.66 0 +1 0 0.8 0 1.242 1.02 0 1 8 2 3 -7 +2 1.104 0 0 1.242 0.8 0 1 8 2 5 -7 +3 0 0 0 1.104 0 0 1 8 2 1 -5 +4 0 0 0 0 1.02 0 1 8 2 1 -3 +5 0 0.8 0.66 1.242 1.02 0.66 1 8 2 4 -8 +6 1.242 0.8 0 1.242 0.8 0.66 1 8 2 7 -8 +7 0 1.02 0 0 1.02 0.66 1 8 2 3 -4 +8 0 0 0.66 1.104 0 0.66 1 8 2 2 -6 +9 1.104 0 0.66 1.242 0.8 0.66 1 8 2 6 -8 +10 0 0 0.66 0 1.02 0.66 1 8 2 2 -4 +11 0 0 0 0 0 0.66 1 8 2 1 -2 +12 1.104 0 0 1.104 0 0.66 1 8 2 5 -6 +1 0 0 0 1.242 1.02 0 1 1 4 1 -2 -3 4 +2 0 0.8 0 1.242 1.02 0.66 1 2 4 5 -6 -1 7 +3 0 0 0.66 1.242 1.02 0.66 1 3 4 8 9 -5 -10 +4 0 0 0 1.104 0 0.66 1 4 4 -8 -11 3 12 +5 0 0 0 0 1.02 0.66 1 5 4 10 -7 -4 11 +6 1.104 0 0 1.242 0.8 0.66 1 6 4 2 6 -9 -12 +1 0 0 0 1.242 1.02 0.66 1 7 6 1 2 3 4 5 6 +$EndEntities $Nodes -168 -1 0 5.1 0 -2 6.21 4 0 -3 5.52 0 0 -4 0 0 0 -5 0 5.1 3.3 -6 6.21 4 3.3 -7 0 0 3.3 -8 5.52 0 3.3 -9 1.034999999997969 4.916666666667026 0 -10 2.069999999994763 4.733333333334261 0 -11 3.10499999999161 4.550000000001486 0 -12 4.13999999999402 4.366666666667726 0 -13 5.174999999996958 4.183333333333872 0 -14 0 4.080000000002531 0 -15 0 3.060000000005065 0 -16 0 2.040000000005119 0 -17 0 1.020000000002431 0 -18 0 5.1 1.099999999997243 -19 0 5.1 2.199999999997244 -20 6.037500000000238 3.000000000001378 0 -21 5.865000000001396 2.000000000008094 0 -22 5.692500000000702 1.000000000004076 0 -23 6.21 4 1.099999999997243 -24 6.21 4 2.199999999997244 -25 4.41600000000253 0 0 -26 3.312000000005635 0 0 -27 2.208000000005758 0 0 -28 1.104000000002809 0 0 -29 5.52 0 1.099999999997243 -30 5.52 0 2.199999999997244 -31 0 0 1.099999999997243 -32 0 0 2.199999999997244 -33 1.034999999997969 4.916666666667026 3.3 -34 2.069999999994763 4.733333333334261 3.3 -35 3.10499999999161 4.550000000001486 3.3 -36 4.13999999999402 4.366666666667726 3.3 -37 5.174999999996958 4.183333333333872 3.3 -38 0 4.080000000002531 3.3 -39 0 3.060000000005065 3.3 -40 0 2.040000000005119 3.3 -41 0 1.020000000002431 3.3 -42 6.037500000000238 3.000000000001378 3.3 -43 5.865000000001396 2.000000000008094 3.3 -44 5.692500000000702 1.000000000004076 3.3 -45 1.103999999997441 0 3.3 -46 2.207999999994134 0 3.3 -47 3.311999999994089 0 3.3 -48 4.415999999997114 0 3.3 -49 2.386497302527005 3.730919978475254 0 -50 3.960392510002857 1.083598528600443 0 -51 4.432076326632258 3.3265620645578 0 -52 1.621307515214968 1.01661383527277 0 -53 0.8767230743720327 2.556581258312159 0 -54 1.167343462037464 3.719275428942924 0 -55 4.930220297964123 1.643458680208118 0 -56 2.771878283950587 0.9787751225948951 0 -57 3.251943127621877 1.976195550159624 0 -58 2.283266696299014 1.95502264736377 0 -59 3.404289098754766 3.591640835316118 0 -60 2.78600449478413 2.819098871455047 0 -61 1.826246986058131 2.800155175232311 0 -62 0.7848144778877137 1.563501256170337 0 -63 5.134455819504938 2.617910745983633 0 -64 4.212934756300453 2.240388354699605 0 -65 4.841732618044288 0.7712784380690728 0 -66 5.352673851426799 3.406841797813403 0 -67 0.7270945991708352 0.7238221488829299 0 -68 1.457646886329138 2.020032866844718 0 -69 3.581257103500146 2.793608248960197 0 -70 2.587499999993187 4.641666666667874 0.9102895601555816 -71 5.271974302055746 4.166155920730866 1.649999999997243 -72 1.560681022834067 4.823550865520535 2.469186580995072 -73 3.622499999992815 4.458333333334606 2.389710439841948 -74 3.622499999990019 4.458333333335101 0.9102895601555957 -75 1.560681022834242 4.823550865520504 0.8308134190039077 -76 2.587854931221515 4.641603796401986 2.407959536651757 -77 4.62736533843115 4.280337862757767 0.8493821351554303 -78 4.627365338433152 4.280337862757412 2.450617864843471 -79 0.8901710668505624 4.942320745002315 1.649999999997876 -80 3.104999999991593 4.550000000001489 1.649999999999781 -81 2.06999999999476 4.733333333334262 1.65 -82 4.146117496482412 4.365583052152874 1.649999999998912 -83 5.481978673881598 4.128957078700522 0.7414177774147986 -84 5.481978673882898 4.128957078700292 2.558582222584017 -85 0.6971704179365547 4.976507655437969 0.7161626837998054 -86 0.6971704179365198 4.976507655437976 2.583837316198038 -87 2.386403851037251 3.730380564715313 3.3 -88 3.965775674800577 1.083362816797341 3.3 -89 4.433132327960219 3.326072113895397 3.3 -90 1.627151092599739 1.01548622626109 3.3 -91 0.878936738206633 2.554689649487218 3.3 -92 1.142974245182026 3.742346051007342 3.3 -93 4.931150832522625 1.643333526254781 3.3 -94 2.777474348734183 0.9772965708701882 3.3 -95 3.256887919122461 1.973572264630932 3.3 -96 2.27362056338768 1.93287362800228 3.3 -97 3.404660201724602 3.590565843447886 3.3 -98 2.785534549692484 2.81477332476458 3.3 -99 1.816687742138859 2.844362833128078 3.3 -100 0.787390917524992 1.562956413140717 3.3 -101 5.135713562421214 2.617602212099839 3.3 -102 4.215502143431396 2.239392322736088 3.3 -103 4.841732618043357 0.7712784380679567 3.3 -104 5.352673851426799 3.406841797813403 3.3 -105 0.7178515418486776 0.7220002560822467 3.3 -106 1.465048282934295 2.0197830265027 3.3 -107 3.5830532113542 2.791490597479166 3.3 -108 4.48468980981603 0 1.649999999997916 -109 1.035310190183893 0 1.649999999999388 -110 2.759999999995056 0 2.37960991561047 -111 2.760000000004749 0 0.9203900843894577 -112 3.832171817007044 0 2.462506203432567 -113 1.687828182992578 0 0.8374937965665119 -114 1.687828182988288 0 2.462506203433918 -115 3.832171817011822 0 0.8374937965651579 -116 3.305991422412324 0 1.649999999996311 -117 2.210004029377576 0 1.650000000001565 -118 0.8056503301718059 0 2.495427304989771 -119 4.714349669827654 0 2.495427304990706 -120 4.714349669829664 0 0.8045726950072637 -121 0.805650330170864 0 0.8045726950063466 -122 0 2.550000000005092 2.416654088140993 -123 0 3.567406916949975 0.8676712655163928 -124 0 1.532593083057434 0.8676712655189218 -125 0 1.532593083057383 2.432328734481018 -126 0 3.567406916949724 2.432328734482401 -127 0 2.550000000004911 0.8672664086931592 -128 0 0.858221729039492 1.649999999997243 -129 0 4.241778270962218 1.649999999997244 -130 0 3.060000000004152 1.64999999999982 -131 0 2.040000000005835 1.650000000000142 -132 0 0.7315477981171267 2.544564769018438 -133 0 4.368452201887145 2.544564769021097 -134 0 0.7315477981183018 0.7554352309809245 -135 0 4.368452201886346 0.7554352309779164 -136 5.677733456182633 0.9143968474355568 1.649999999998703 -137 5.951250000000817 2.500000000004736 0.8788157870052324 -138 5.951250000000817 2.500000000004736 2.421184212994766 -139 5.778750000001049 1.500000000006085 2.454721090038176 -140 5.778750000001049 1.500000000006085 0.8452789099591339 -141 6.064307601881952 3.155406387721458 1.649999999997243 -142 5.863855733322866 1.99336656998763 1.649999999999426 -143 6.086483241547053 3.283960820562625 2.547756640661937 -144 5.643030432939424 0.7132199010981142 2.550395885654611 -145 5.643030432939587 0.7132199010990538 0.7496041143445117 -146 6.086483241546856 3.283960820561481 0.7522433593373904 -147 3.185152992809589 2.853842729420941 1.650105251411698 -148 1.848798741753221 1.785272088996948 1.649113242066087 -149 4.294795109466685 1.708491619923346 1.791678167333182 -150 1.753055973400559 3.274722774000739 1.857200121487178 -151 4.538875156952106 3.016527099416517 1.408338929901463 -152 3.094271366453881 1.597958473505834 1.25459524507098 -153 1.08724203698074 1.120888866969477 1.047102140042662 -154 1.082785338776251 1.11517678526899 2.255970139100719 -155 2.310694805362786 2.578141344200029 0.9937414242326503 -156 1.140445927663791 2.572711817267504 1.143520495002259 -157 3.943633811919474 3.384959792017217 2.288888650148183 -158 3.308816216352959 1.028420414186172 2.256405604516595 -159 2.381749043048582 2.488049082549255 2.320657494382369 -160 4.947756758381086 2.155663961686152 0.938396064409417 -161 4.88844102152302 2.469303483107215 2.264694198274357 -162 2.215565106161902 0.9688630562528956 0.9869268335752817 -163 1.032432474645413 3.893322723850346 1.225919309291406 -164 2.895167778353591 3.612966366114015 2.353749188670391 -165 2.942988247029935 3.635370012826987 0.9628371523745898 -166 4.690563675953788 1.123250848348735 0.9941834455913106 -167 2.211611067924777 0.9465777326923647 2.261932054260194 -168 1.074422649597229 2.334800898167823 2.227697036090162 +27 25 1 25 +0 1 0 1 +1 +0 0 0 +0 2 0 1 +2 +0 0 0.66 +0 3 0 1 +3 +0 1.02 0 +0 4 0 1 +4 +0 1.02 0.66 +0 5 0 1 +5 +1.104 0 0 +0 6 0 1 +6 +1.104 0 0.66 +0 7 0 1 +7 +1.242 0.8 0 +0 8 0 1 +8 +1.242 0.8 0.66 +1 1 0 1 +9 +0.6209999999983563 0.9100000000002912 0 +1 2 0 0 +1 3 0 1 +10 +0.5519999999985621 0 0 +1 4 0 1 +11 +0 0.5099999999986879 0 +1 5 0 1 +12 +0.6209999999983563 0.9100000000002912 0.66 +1 6 0 0 +1 7 0 0 +1 8 0 1 +13 +0.5519999999985621 0 0.66 +1 9 0 0 +1 10 0 1 +14 +0 0.5099999999986879 0.66 +1 11 0 0 +1 12 0 0 +2 1 0 2 +15 +16 +0.7852490381181173 0.4214107236109463 0 +0.4072451905936675 0.3970536180544399 0 +2 2 0 2 +17 +18 +0.9314999999991784 0.8550000000001458 0.33 +0.3104999999991782 0.9650000000001459 0.33 +2 3 0 2 +19 +20 +0.7852490381181172 0.4214107236109463 0.66 +0.4072451905936674 0.3970536180544399 0.66 +2 4 0 2 +21 +22 +0.7716734693870362 0 0.33 +0.3265789254470112 0 0.33 +2 5 0 2 +23 +24 +0 0.6921428571422011 0.33 +0 0.3424285714279154 0.3300000000000001 +2 6 0 1 +25 +1.173 0.3999999999999998 0.33 +3 1 0 0 $EndNodes $Elements -841 -1 1 2 8 1 1 9 -2 1 2 8 1 9 10 -3 1 2 8 1 10 11 -4 1 2 8 1 11 12 -5 1 2 8 1 12 13 -6 1 2 8 1 13 2 -7 1 2 8 2 1 14 -8 1 2 8 2 14 15 -9 1 2 8 2 15 16 -10 1 2 8 2 16 17 -11 1 2 8 2 17 4 -12 1 2 8 3 1 18 -13 1 2 8 3 18 19 -14 1 2 8 3 19 5 -15 1 2 8 4 2 20 -16 1 2 8 4 20 21 -17 1 2 8 4 21 22 -18 1 2 8 4 22 3 -19 1 2 8 5 2 23 -20 1 2 8 5 23 24 -21 1 2 8 5 24 6 -22 1 2 8 6 3 25 -23 1 2 8 6 25 26 -24 1 2 8 6 26 27 -25 1 2 8 6 27 28 -26 1 2 8 6 28 4 -27 1 2 8 7 3 29 -28 1 2 8 7 29 30 -29 1 2 8 7 30 8 -30 1 2 8 8 4 31 -31 1 2 8 8 31 32 -32 1 2 8 8 32 7 -33 1 2 8 9 5 33 -34 1 2 8 9 33 34 -35 1 2 8 9 34 35 -36 1 2 8 9 35 36 -37 1 2 8 9 36 37 -38 1 2 8 9 37 6 -39 1 2 8 10 5 38 -40 1 2 8 10 38 39 -41 1 2 8 10 39 40 -42 1 2 8 10 40 41 -43 1 2 8 10 41 7 -44 1 2 8 11 6 42 -45 1 2 8 11 42 43 -46 1 2 8 11 43 44 -47 1 2 8 11 44 8 -48 1 2 8 12 7 45 -49 1 2 8 12 45 46 -50 1 2 8 12 46 47 -51 1 2 8 12 47 48 -52 1 2 8 12 48 8 -53 2 2 1 1 1 9 14 -54 2 2 1 1 13 2 66 -55 2 2 1 1 2 20 66 -56 2 2 1 1 22 3 65 -57 2 2 1 1 3 25 65 -58 2 2 1 1 4 17 67 -59 2 2 1 1 28 4 67 -60 2 2 1 1 9 10 54 -61 2 2 1 1 14 9 54 -62 2 2 1 1 10 11 49 -63 2 2 1 1 10 49 54 -64 2 2 1 1 11 12 59 -65 2 2 1 1 49 11 59 -66 2 2 1 1 12 13 51 -67 2 2 1 1 12 51 59 -68 2 2 1 1 51 13 66 -69 2 2 1 1 15 14 54 -70 2 2 1 1 16 15 53 -71 2 2 1 1 53 15 54 -72 2 2 1 1 17 16 62 -73 2 2 1 1 16 53 62 -74 2 2 1 1 17 62 67 -75 2 2 1 1 20 21 63 -76 2 2 1 1 20 63 66 -77 2 2 1 1 21 22 55 -78 2 2 1 1 21 55 63 -79 2 2 1 1 55 22 65 -80 2 2 1 1 25 26 50 -81 2 2 1 1 25 50 65 -82 2 2 1 1 26 27 56 -83 2 2 1 1 50 26 56 -84 2 2 1 1 27 28 52 -85 2 2 1 1 27 52 56 -86 2 2 1 1 52 28 67 -87 2 2 1 1 54 49 61 -88 2 2 1 1 49 59 60 -89 2 2 1 1 49 60 61 -90 2 2 1 1 55 50 64 -91 2 2 1 1 50 55 65 -92 2 2 1 1 50 56 57 -93 2 2 1 1 50 57 64 -94 2 2 1 1 59 51 69 -95 2 2 1 1 51 63 64 -96 2 2 1 1 63 51 66 -97 2 2 1 1 51 64 69 -98 2 2 1 1 56 52 58 -99 2 2 1 1 58 52 68 -100 2 2 1 1 62 52 67 -101 2 2 1 1 52 62 68 -102 2 2 1 1 53 54 61 -103 2 2 1 1 53 61 68 -104 2 2 1 1 62 53 68 -105 2 2 1 1 63 55 64 -106 2 2 1 1 57 56 58 -107 2 2 1 1 57 58 60 -108 2 2 1 1 57 60 69 -109 2 2 1 1 64 57 69 -110 2 2 1 1 60 58 61 -111 2 2 1 1 61 58 68 -112 2 2 1 1 60 59 69 -113 2 2 2 2 1 85 9 -114 2 2 2 2 18 85 1 -115 2 2 2 2 13 83 2 -116 2 2 2 2 2 83 23 -117 2 2 2 2 5 86 19 -118 2 2 2 2 33 86 5 -119 2 2 2 2 24 84 6 -120 2 2 2 2 6 84 37 -121 2 2 2 2 9 75 10 -122 2 2 2 2 9 85 75 -123 2 2 2 2 10 70 11 -124 2 2 2 2 10 75 70 -125 2 2 2 2 11 74 12 -126 2 2 2 2 70 74 11 -127 2 2 2 2 12 77 13 -128 2 2 2 2 74 77 12 -129 2 2 2 2 77 83 13 -130 2 2 2 2 19 79 18 -131 2 2 2 2 79 85 18 -132 2 2 2 2 19 86 79 -133 2 2 2 2 23 71 24 -134 2 2 2 2 23 83 71 -135 2 2 2 2 71 84 24 -136 2 2 2 2 34 72 33 -137 2 2 2 2 72 86 33 -138 2 2 2 2 35 76 34 -139 2 2 2 2 34 76 72 -140 2 2 2 2 36 73 35 -141 2 2 2 2 73 76 35 -142 2 2 2 2 37 78 36 -143 2 2 2 2 36 78 73 -144 2 2 2 2 37 84 78 -145 2 2 2 2 70 80 74 -146 2 2 2 2 75 81 70 -147 2 2 2 2 70 81 80 -148 2 2 2 2 77 82 71 -149 2 2 2 2 71 83 77 -150 2 2 2 2 71 82 78 -151 2 2 2 2 78 84 71 -152 2 2 2 2 76 81 72 -153 2 2 2 2 72 81 79 -154 2 2 2 2 79 86 72 -155 2 2 2 2 73 80 76 -156 2 2 2 2 78 82 73 -157 2 2 2 2 73 82 80 -158 2 2 2 2 74 82 77 -159 2 2 2 2 80 82 74 -160 2 2 2 2 79 81 75 -161 2 2 2 2 75 85 79 -162 2 2 2 2 80 81 76 -163 2 2 3 3 33 5 38 -164 2 2 3 3 37 104 6 -165 2 2 3 3 6 104 42 -166 2 2 3 3 7 105 41 -167 2 2 3 3 45 105 7 -168 2 2 3 3 44 103 8 -169 2 2 3 3 8 103 48 -170 2 2 3 3 33 92 34 -171 2 2 3 3 38 92 33 -172 2 2 3 3 34 87 35 -173 2 2 3 3 34 92 87 -174 2 2 3 3 35 97 36 -175 2 2 3 3 87 97 35 -176 2 2 3 3 36 89 37 -177 2 2 3 3 36 97 89 -178 2 2 3 3 89 104 37 -179 2 2 3 3 39 92 38 -180 2 2 3 3 40 91 39 -181 2 2 3 3 91 92 39 -182 2 2 3 3 41 100 40 -183 2 2 3 3 40 100 91 -184 2 2 3 3 41 105 100 -185 2 2 3 3 42 101 43 -186 2 2 3 3 42 104 101 -187 2 2 3 3 43 93 44 -188 2 2 3 3 43 101 93 -189 2 2 3 3 93 103 44 -190 2 2 3 3 46 90 45 -191 2 2 3 3 90 105 45 -192 2 2 3 3 47 94 46 -193 2 2 3 3 46 94 90 -194 2 2 3 3 48 88 47 -195 2 2 3 3 88 94 47 -196 2 2 3 3 48 103 88 -197 2 2 3 3 92 99 87 -198 2 2 3 3 87 98 97 -199 2 2 3 3 87 99 98 -200 2 2 3 3 93 102 88 -201 2 2 3 3 88 103 93 -202 2 2 3 3 88 95 94 -203 2 2 3 3 88 102 95 -204 2 2 3 3 97 107 89 -205 2 2 3 3 89 102 101 -206 2 2 3 3 101 104 89 -207 2 2 3 3 89 107 102 -208 2 2 3 3 94 96 90 -209 2 2 3 3 96 106 90 -210 2 2 3 3 100 105 90 -211 2 2 3 3 90 106 100 -212 2 2 3 3 91 99 92 -213 2 2 3 3 91 106 99 -214 2 2 3 3 100 106 91 -215 2 2 3 3 101 102 93 -216 2 2 3 3 95 96 94 -217 2 2 3 3 95 98 96 -218 2 2 3 3 95 107 98 -219 2 2 3 3 102 107 95 -220 2 2 3 3 98 99 96 -221 2 2 3 3 99 106 96 -222 2 2 3 3 98 107 97 -223 2 2 4 4 3 120 25 -224 2 2 4 4 29 120 3 -225 2 2 4 4 28 121 4 -226 2 2 4 4 4 121 31 -227 2 2 4 4 32 118 7 -228 2 2 4 4 7 118 45 -229 2 2 4 4 8 119 30 -230 2 2 4 4 48 119 8 -231 2 2 4 4 25 115 26 -232 2 2 4 4 25 120 115 -233 2 2 4 4 26 111 27 -234 2 2 4 4 26 115 111 -235 2 2 4 4 27 113 28 -236 2 2 4 4 111 113 27 -237 2 2 4 4 113 121 28 -238 2 2 4 4 30 108 29 -239 2 2 4 4 108 120 29 -240 2 2 4 4 30 119 108 -241 2 2 4 4 31 109 32 -242 2 2 4 4 31 121 109 -243 2 2 4 4 109 118 32 -244 2 2 4 4 45 114 46 -245 2 2 4 4 45 118 114 -246 2 2 4 4 46 110 47 -247 2 2 4 4 46 114 110 -248 2 2 4 4 47 112 48 -249 2 2 4 4 110 112 47 -250 2 2 4 4 112 119 48 -251 2 2 4 4 112 116 108 -252 2 2 4 4 108 119 112 -253 2 2 4 4 108 116 115 -254 2 2 4 4 115 120 108 -255 2 2 4 4 113 117 109 -256 2 2 4 4 109 121 113 -257 2 2 4 4 109 117 114 -258 2 2 4 4 114 118 109 -259 2 2 4 4 110 116 112 -260 2 2 4 4 114 117 110 -261 2 2 4 4 110 117 116 -262 2 2 4 4 111 117 113 -263 2 2 4 4 115 116 111 -264 2 2 4 4 116 117 111 -265 2 2 5 5 1 14 135 -266 2 2 5 5 18 1 135 -267 2 2 5 5 17 4 134 -268 2 2 5 5 4 31 134 -269 2 2 5 5 5 19 133 -270 2 2 5 5 38 5 133 -271 2 2 5 5 32 7 132 -272 2 2 5 5 7 41 132 -273 2 2 5 5 14 15 123 -274 2 2 5 5 14 123 135 -275 2 2 5 5 15 16 127 -276 2 2 5 5 123 15 127 -277 2 2 5 5 16 17 124 -278 2 2 5 5 16 124 127 -279 2 2 5 5 124 17 134 -280 2 2 5 5 19 18 129 -281 2 2 5 5 129 18 135 -282 2 2 5 5 19 129 133 -283 2 2 5 5 31 32 128 -284 2 2 5 5 31 128 134 -285 2 2 5 5 128 32 132 -286 2 2 5 5 39 38 126 -287 2 2 5 5 126 38 133 -288 2 2 5 5 40 39 122 -289 2 2 5 5 122 39 126 -290 2 2 5 5 41 40 125 -291 2 2 5 5 40 122 125 -292 2 2 5 5 41 125 132 -293 2 2 5 5 125 122 131 -294 2 2 5 5 122 126 130 -295 2 2 5 5 122 130 131 -296 2 2 5 5 123 127 130 -297 2 2 5 5 129 123 130 -298 2 2 5 5 123 129 135 -299 2 2 5 5 127 124 131 -300 2 2 5 5 124 128 131 -301 2 2 5 5 128 124 134 -302 2 2 5 5 128 125 131 -303 2 2 5 5 125 128 132 -304 2 2 5 5 126 129 130 -305 2 2 5 5 129 126 133 -306 2 2 5 5 130 127 131 -307 2 2 6 6 20 2 146 -308 2 2 6 6 2 23 146 -309 2 2 6 6 3 22 145 -310 2 2 6 6 29 3 145 -311 2 2 6 6 24 6 143 -312 2 2 6 6 6 42 143 -313 2 2 6 6 8 30 144 -314 2 2 6 6 44 8 144 -315 2 2 6 6 21 20 137 -316 2 2 6 6 137 20 146 -317 2 2 6 6 22 21 140 -318 2 2 6 6 21 137 140 -319 2 2 6 6 22 140 145 -320 2 2 6 6 23 24 141 -321 2 2 6 6 23 141 146 -322 2 2 6 6 141 24 143 -323 2 2 6 6 30 29 136 -324 2 2 6 6 136 29 145 -325 2 2 6 6 30 136 144 -326 2 2 6 6 42 43 138 -327 2 2 6 6 42 138 143 -328 2 2 6 6 43 44 139 -329 2 2 6 6 138 43 139 -330 2 2 6 6 139 44 144 -331 2 2 6 6 139 136 142 -332 2 2 6 6 136 139 144 -333 2 2 6 6 136 140 142 -334 2 2 6 6 140 136 145 -335 2 2 6 6 140 137 142 -336 2 2 6 6 137 141 142 -337 2 2 6 6 141 137 146 -338 2 2 6 6 138 139 142 -339 2 2 6 6 141 138 142 -340 2 2 6 6 138 141 143 -341 4 2 7 1 107 149 147 157 -342 4 2 7 1 72 126 133 163 -343 4 2 7 1 147 151 149 152 -344 4 2 7 1 72 133 86 163 -345 4 2 7 1 107 95 147 149 -346 4 2 7 1 147 64 151 152 -347 4 2 7 1 69 147 64 151 -348 4 2 7 1 64 147 69 152 -349 4 2 7 1 149 151 64 152 -350 4 2 7 1 95 147 149 158 -351 4 2 7 1 64 151 149 160 -352 4 2 7 1 107 102 149 157 -353 4 2 7 1 147 158 95 159 -354 4 2 7 1 107 102 95 149 -355 4 2 7 1 126 72 92 163 -356 4 2 7 1 144 119 103 149 -357 4 2 7 1 133 126 72 92 -358 4 2 7 1 59 151 74 165 -359 4 2 7 1 147 151 69 165 -360 4 2 7 1 72 86 133 92 -361 4 2 7 1 59 69 151 165 -362 4 2 7 1 152 158 147 159 -363 4 2 7 1 147 152 149 158 -364 4 2 7 1 144 136 119 149 -365 4 2 7 1 59 51 74 151 -366 4 2 7 1 93 144 103 149 -367 4 2 7 1 126 92 150 163 -368 4 2 7 1 74 51 77 151 -369 4 2 7 1 150 155 49 165 -370 4 2 7 1 93 139 144 149 -371 4 2 7 1 49 163 150 165 -372 4 2 7 1 49 70 163 165 -373 4 2 7 1 49 155 150 163 -374 4 2 7 1 104 78 84 161 -375 4 2 7 1 149 158 152 166 -376 4 2 7 1 79 72 86 163 -377 4 2 7 1 72 150 92 163 -378 4 2 7 1 102 149 157 161 -379 4 2 7 1 78 71 84 161 -380 4 2 7 1 79 86 133 163 -381 4 2 7 1 61 155 49 163 -382 4 2 7 1 78 151 71 161 -383 4 2 7 1 59 69 51 151 -384 4 2 7 1 57 64 69 152 -385 4 2 7 1 78 157 151 161 -386 4 2 7 1 149 152 64 166 -387 4 2 7 1 95 107 147 159 -388 4 2 7 1 64 160 149 166 -389 4 2 7 1 75 49 70 163 -390 4 2 7 1 147 157 151 165 -391 4 2 7 1 61 156 155 163 -392 4 2 7 1 103 119 88 149 -393 4 2 7 1 89 78 104 161 -394 4 2 7 1 108 119 136 149 -395 4 2 7 1 143 84 141 161 -396 4 2 7 1 108 115 116 166 -397 4 2 7 1 63 137 66 151 -398 4 2 7 1 126 129 133 163 -399 4 2 7 1 108 116 158 166 -400 4 2 7 1 108 158 149 166 -401 4 2 7 1 89 157 78 161 -402 4 2 7 1 139 136 144 149 -403 4 2 7 1 136 108 149 166 -404 4 2 7 1 146 66 137 151 -405 4 2 7 1 137 151 63 160 -406 4 2 7 1 119 88 149 158 -407 4 2 7 1 74 151 82 165 -408 4 2 7 1 111 115 56 152 -409 4 2 7 1 151 157 82 165 -410 4 2 7 1 84 71 141 161 -411 4 2 7 1 155 156 150 163 -412 4 2 7 1 115 50 152 166 -413 4 2 7 1 69 60 57 155 -414 4 2 7 1 149 151 147 157 -415 4 2 7 1 98 147 107 159 -416 4 2 7 1 104 84 143 161 -417 4 2 7 1 119 112 88 158 -418 4 2 7 1 130 126 150 163 -419 4 2 7 1 130 150 126 168 -420 4 2 7 1 126 150 92 168 -421 4 2 7 1 98 107 147 164 -422 4 2 7 1 146 83 66 151 -423 4 2 7 1 50 64 152 166 -424 4 2 7 1 72 76 87 150 -425 4 2 7 1 79 133 129 163 -426 4 2 7 1 151 157 149 161 -427 4 2 7 1 141 71 151 161 -428 4 2 7 1 98 147 159 164 -429 4 2 7 1 106 154 148 167 -430 4 2 7 1 75 70 81 163 -431 4 2 7 1 74 77 82 151 -432 4 2 7 1 77 66 83 151 -433 4 2 7 1 148 153 68 162 -434 4 2 7 1 106 148 96 167 -435 4 2 7 1 111 116 115 152 -436 4 2 7 1 50 56 115 152 -437 4 2 7 1 96 159 158 167 -438 4 2 7 1 93 103 88 149 -439 4 2 7 1 96 148 106 159 -440 4 2 7 1 93 139 149 161 -441 4 2 7 1 54 61 49 163 -442 4 2 7 1 148 152 159 167 -443 4 2 7 1 58 148 68 162 -444 4 2 7 1 158 159 152 167 -445 4 2 7 1 107 157 147 164 -446 4 2 7 1 96 95 158 159 -447 4 2 7 1 83 146 141 151 -448 4 2 7 1 157 164 80 165 -449 4 2 7 1 76 87 150 164 -450 4 2 7 1 68 148 58 155 -451 4 2 7 1 124 62 127 156 -452 4 2 7 1 77 51 66 151 -453 4 2 7 1 147 164 157 165 -454 4 2 7 1 81 164 150 165 -455 4 2 7 1 80 82 157 165 -456 4 2 7 1 148 154 106 168 -457 4 2 7 1 68 153 148 156 -458 4 2 7 1 124 153 62 156 -459 4 2 7 1 95 149 88 158 -460 4 2 7 1 81 80 164 165 -461 4 2 7 1 148 106 159 168 -462 4 2 7 1 153 154 109 167 -463 4 2 7 1 126 91 122 168 -464 4 2 7 1 148 155 68 156 -465 4 2 7 1 95 98 107 159 -466 4 2 7 1 88 95 102 149 -467 4 2 7 1 57 50 64 152 -468 4 2 7 1 126 92 91 168 -469 4 2 7 1 83 141 71 151 -470 4 2 7 1 146 137 141 151 -471 4 2 7 1 96 148 159 167 -472 4 2 7 1 87 92 72 150 -473 4 2 7 1 151 160 142 161 -474 4 2 7 1 51 63 66 151 -475 4 2 7 1 72 81 76 150 -476 4 2 7 1 141 151 137 160 -477 4 2 7 1 131 154 153 168 -478 4 2 7 1 54 156 61 163 -479 4 2 7 1 56 111 152 162 -480 4 2 7 1 69 64 51 151 -481 4 2 7 1 142 136 139 149 -482 4 2 7 1 12 77 74 51 -483 4 2 7 1 153 156 131 168 -484 4 2 7 1 148 156 153 168 -485 4 2 7 1 150 156 130 163 -486 4 2 7 1 81 150 72 163 -487 4 2 7 1 51 74 12 59 -488 4 2 7 1 62 153 68 156 -489 4 2 7 1 49 75 54 163 -490 4 2 7 1 130 156 150 168 -491 4 2 7 1 142 149 139 161 -492 4 2 7 1 106 90 154 167 -493 4 2 7 1 148 58 155 162 -494 4 2 7 1 116 158 162 167 -495 4 2 7 1 141 151 142 161 -496 4 2 7 1 116 158 152 162 -497 4 2 7 1 153 154 148 168 -498 4 2 7 1 123 127 53 156 -499 4 2 7 1 53 127 62 156 -500 4 2 7 1 111 116 152 162 -501 4 2 7 1 62 16 124 127 -502 4 2 7 1 136 149 142 166 -503 4 2 7 1 141 142 151 160 -504 4 2 7 1 130 126 122 168 -505 4 2 7 1 54 123 156 163 -506 4 2 7 1 149 160 142 166 -507 4 2 7 1 141 137 142 160 -508 4 2 7 1 92 150 99 168 -509 4 2 7 1 150 164 147 165 -510 4 2 7 1 116 162 117 167 -511 4 2 7 1 94 96 158 167 -512 4 2 7 1 52 68 153 162 -513 4 2 7 1 147 155 152 159 -514 4 2 7 1 148 154 153 167 -515 4 2 7 1 124 131 153 156 -516 4 2 7 1 148 153 162 167 -517 4 2 7 1 109 113 153 162 -518 4 2 7 1 93 149 102 161 -519 4 2 7 1 81 76 150 164 -520 4 2 7 1 106 96 90 167 -521 4 2 7 1 88 102 93 149 -522 4 2 7 1 51 63 151 160 -523 4 2 7 1 82 151 78 157 -524 4 2 7 1 53 54 123 156 -525 4 2 7 1 94 96 95 158 -526 4 2 7 1 87 159 150 164 -527 4 2 7 1 147 155 150 165 -528 4 2 7 1 152 155 148 159 -529 4 2 7 1 152 155 58 162 -530 4 2 7 1 152 162 158 167 -531 4 2 7 1 100 125 40 168 -532 4 2 7 1 99 150 87 159 -533 4 2 7 1 78 82 71 151 -534 4 2 7 1 51 151 64 160 -535 4 2 7 1 73 80 157 164 -536 4 2 7 1 141 138 143 161 -537 4 2 7 1 40 125 122 168 -538 4 2 7 1 74 82 80 165 -539 4 2 7 1 26 56 111 115 -540 4 2 7 1 16 62 53 127 -541 4 2 7 1 109 117 113 162 -542 4 2 7 1 83 71 77 151 -543 4 2 7 1 140 142 160 166 -544 4 2 7 1 43 138 139 161 -545 4 2 7 1 91 92 99 168 -546 4 2 7 1 119 144 30 136 -547 4 2 7 1 128 153 131 154 -548 4 2 7 1 114 109 154 167 -549 4 2 7 1 140 136 142 166 -550 4 2 7 1 148 155 152 162 -551 4 2 7 1 101 89 104 161 -552 4 2 7 1 139 93 43 161 -553 4 2 7 1 73 80 82 157 -554 4 2 7 1 112 108 116 158 -555 4 2 7 1 43 101 138 161 -556 4 2 7 1 52 58 68 162 -557 4 2 7 1 70 80 81 165 -558 4 2 7 1 51 64 63 160 -559 4 2 7 1 93 101 43 161 -560 4 2 7 1 149 142 160 161 -561 4 2 7 1 81 80 76 164 -562 4 2 7 1 102 157 89 161 -563 4 2 7 1 111 117 116 162 -564 4 2 7 1 114 117 109 167 -565 4 2 7 1 99 92 87 150 -566 4 2 7 1 123 130 156 163 -567 4 2 7 1 108 119 30 136 -568 4 2 7 1 98 159 87 164 -569 4 2 7 1 124 127 131 156 -570 4 2 7 1 99 150 159 168 -571 4 2 7 1 130 129 126 163 -572 4 2 7 1 81 72 79 163 -573 4 2 7 1 53 62 68 156 -574 4 2 7 1 26 50 56 115 -575 4 2 7 1 100 154 125 168 -576 4 2 7 1 58 152 57 155 -577 4 2 7 1 85 135 54 163 -578 4 2 7 1 150 155 147 159 -579 4 2 7 1 150 159 147 164 -580 4 2 7 1 98 99 87 159 -581 4 2 7 1 125 154 131 168 -582 4 2 7 1 127 53 15 123 -583 4 2 7 1 126 122 91 39 -584 4 2 7 1 114 118 105 154 -585 4 2 7 1 99 159 106 168 -586 4 2 7 1 91 100 40 168 -587 4 2 7 1 113 67 121 153 -588 4 2 7 1 110 158 116 167 -589 4 2 7 1 55 160 64 166 -590 4 2 7 1 123 130 127 156 -591 4 2 7 1 114 105 90 154 -592 4 2 7 1 56 152 58 162 -593 4 2 7 1 106 154 100 168 -594 4 2 7 1 148 155 150 159 -595 4 2 7 1 128 124 131 153 -596 4 2 7 1 113 52 67 153 -597 4 2 7 1 128 131 125 154 -598 4 2 7 1 102 107 89 157 -599 4 2 7 1 57 56 50 152 -600 4 2 7 1 110 116 117 167 -601 4 2 7 1 122 91 40 168 -602 4 2 7 1 60 49 155 165 -603 4 2 7 1 152 148 162 167 -604 4 2 7 1 82 77 71 151 -605 4 2 7 1 75 49 10 70 -606 4 2 7 1 150 155 148 156 -607 4 2 7 1 136 29 108 166 -608 4 2 7 1 50 120 65 166 -609 4 2 7 1 141 142 138 161 -610 4 2 7 1 61 68 155 156 -611 4 2 7 1 34 72 76 87 -612 4 2 7 1 85 54 75 163 -613 4 2 7 1 135 123 54 163 -614 4 2 7 1 60 69 59 165 -615 4 2 7 1 97 157 107 164 -616 4 2 7 1 105 45 118 114 -617 4 2 7 1 115 120 50 166 -618 4 2 7 1 148 159 150 168 -619 4 2 7 1 56 57 58 152 -620 4 2 7 1 67 62 134 153 -621 4 2 7 1 134 62 124 153 -622 4 2 7 1 132 100 105 154 -623 4 2 7 1 105 45 114 90 -624 4 2 7 1 132 125 100 154 -625 4 2 7 1 28 67 121 113 -626 4 2 7 1 150 156 148 168 -627 4 2 7 1 50 55 64 166 -628 4 2 7 1 98 97 107 164 -629 4 2 7 1 28 67 113 52 -630 4 2 7 1 138 42 143 101 -631 4 2 7 1 109 153 128 154 -632 4 2 7 1 61 60 49 155 -633 4 2 7 1 53 54 15 123 -634 4 2 7 1 126 91 92 39 -635 4 2 7 1 36 97 73 157 -636 4 2 7 1 104 143 42 101 -637 4 2 7 1 149 160 151 161 -638 4 2 7 1 143 84 24 141 -639 4 2 7 1 83 23 141 146 -640 4 2 7 1 58 57 60 155 -641 4 2 7 1 128 121 134 153 -642 4 2 7 1 105 41 100 132 -643 4 2 7 1 54 53 61 156 -644 4 2 7 1 134 67 17 62 -645 4 2 7 1 132 118 128 154 -646 4 2 7 1 84 104 37 78 -647 4 2 7 1 83 77 13 66 -648 4 2 7 1 124 134 17 62 -649 4 2 7 1 132 41 100 125 -650 4 2 7 1 36 73 78 157 -651 4 2 7 1 120 25 50 65 -652 4 2 7 1 101 102 89 161 -653 4 2 7 1 106 90 100 154 -654 4 2 7 1 88 48 119 103 -655 4 2 7 1 52 68 62 153 -656 4 2 7 1 10 54 49 75 -657 4 2 7 1 87 34 72 92 -658 4 2 7 1 135 54 14 85 -659 4 2 7 1 121 128 109 153 -660 4 2 7 1 44 139 144 93 -661 4 2 7 1 104 89 37 78 -662 4 2 7 1 51 13 77 66 -663 4 2 7 1 36 78 89 157 -664 4 2 7 1 145 55 140 22 -665 4 2 7 1 29 120 108 166 -666 4 2 7 1 25 50 115 120 -667 4 2 7 1 109 128 118 154 -668 4 2 7 1 55 145 140 166 -669 4 2 7 1 144 103 44 93 -670 4 2 7 1 88 48 112 119 -671 4 2 7 1 84 24 141 71 -672 4 2 7 1 23 141 71 83 -673 4 2 7 1 22 145 55 65 -674 4 2 7 1 135 85 79 163 -675 4 2 7 1 131 156 130 168 -676 4 2 7 1 136 145 29 166 -677 4 2 7 1 115 108 120 166 -678 4 2 7 1 36 89 97 157 -679 4 2 7 1 90 114 154 167 -680 4 2 7 1 113 52 153 162 -681 4 2 7 1 145 55 65 166 -682 4 2 7 1 63 21 137 160 -683 4 2 7 1 38 133 86 92 -684 4 2 7 1 99 96 106 159 -685 4 2 7 1 35 73 97 164 -686 4 2 7 1 97 73 157 164 -687 4 2 7 1 85 54 9 75 -688 4 2 7 1 21 140 137 160 -689 4 2 7 1 14 54 135 123 -690 4 2 7 1 29 108 30 136 -691 4 2 7 1 98 95 96 159 -692 4 2 7 1 90 46 114 167 -693 4 2 7 1 74 11 59 165 -694 4 2 7 1 76 73 35 164 -695 4 2 7 1 88 112 47 158 -696 4 2 7 1 110 46 94 167 -697 4 2 7 1 138 142 139 161 -698 4 2 7 1 74 70 11 165 -699 4 2 7 1 47 112 110 158 -700 4 2 7 1 33 86 72 92 -701 4 2 7 1 61 68 58 155 -702 4 2 7 1 129 130 123 163 -703 4 2 7 1 94 90 96 167 -704 4 2 7 1 21 55 140 160 -705 4 2 7 1 113 27 52 162 -706 4 2 7 1 127 130 131 156 -707 4 2 7 1 131 130 122 168 -708 4 2 7 1 128 118 132 32 -709 4 2 7 1 31 121 134 128 -710 4 2 7 1 47 110 94 158 -711 4 2 7 1 27 111 56 162 -712 4 2 7 1 38 126 133 92 -713 4 2 7 1 79 129 135 163 -714 4 2 7 1 70 49 11 165 -715 4 2 7 1 122 125 131 168 -716 4 2 7 1 67 134 121 153 -717 4 2 7 1 94 158 110 167 -718 4 2 7 1 55 140 160 166 -719 4 2 7 1 90 94 46 167 -720 4 2 7 1 110 114 46 167 -721 4 2 7 1 105 118 132 154 -722 4 2 7 1 21 63 55 160 -723 4 2 7 1 75 81 79 163 -724 4 2 7 1 101 93 102 161 -725 4 2 7 1 27 113 111 162 -726 4 2 7 1 86 19 79 133 -727 4 2 7 1 85 18 135 79 -728 4 2 7 1 128 109 118 32 -729 4 2 7 1 128 31 121 109 -730 4 2 7 1 145 65 120 166 -731 4 2 7 1 76 35 87 164 -732 4 2 7 1 94 88 47 158 -733 4 2 7 1 120 29 145 166 -734 4 2 7 1 54 9 14 85 -735 4 2 7 1 52 56 58 162 -736 4 2 7 1 27 56 52 162 -737 4 2 7 1 61 53 68 156 -738 4 2 7 1 94 95 88 158 -739 4 2 7 1 21 20 63 137 -740 4 2 7 1 138 43 42 101 -741 4 2 7 1 91 106 100 168 -742 4 2 7 1 99 106 91 168 -743 4 2 7 1 49 59 11 165 -744 4 2 7 1 121 109 113 153 -745 4 2 7 1 74 11 12 59 -746 4 2 7 1 36 97 35 73 -747 4 2 7 1 128 134 124 153 -748 4 2 7 1 109 118 114 154 -749 4 2 7 1 33 38 86 92 -750 4 2 7 1 132 128 125 154 -751 4 2 7 1 107 97 89 157 -752 4 2 7 1 145 3 120 65 -753 4 2 7 1 8 144 119 103 -754 4 2 7 1 35 97 87 164 -755 4 2 7 1 89 36 37 78 -756 4 2 7 1 12 13 77 51 -757 4 2 7 1 100 90 105 154 -758 4 2 7 1 140 145 136 166 -759 4 2 7 1 52 62 67 153 -760 4 2 7 1 60 59 49 165 -761 4 2 7 1 19 129 79 133 -762 4 2 7 1 129 135 18 79 -763 4 2 7 1 40 100 41 125 -764 4 2 7 1 124 17 16 62 -765 4 2 7 1 93 44 139 43 -766 4 2 7 1 98 96 99 159 -767 4 2 7 1 55 21 140 22 -768 4 2 7 1 85 75 79 163 -769 4 2 7 1 46 114 45 90 -770 4 2 7 1 27 28 113 52 -771 4 2 7 1 80 73 76 164 -772 4 2 7 1 135 14 1 85 -773 4 2 7 1 133 86 5 38 -774 4 2 7 1 73 82 78 157 -775 4 2 7 1 129 123 135 163 -776 4 2 7 1 26 25 50 115 -777 4 2 7 1 88 48 47 112 -778 4 2 7 1 4 67 134 121 -779 4 2 7 1 118 7 105 132 -780 4 2 7 1 66 2 83 146 -781 4 2 7 1 6 143 104 84 -782 4 2 7 1 24 141 71 23 -783 4 2 7 1 50 65 55 166 -784 4 2 7 1 109 128 31 32 -785 4 2 7 1 60 61 58 155 -786 4 2 7 1 112 116 110 158 -787 4 2 7 1 98 87 97 164 -788 4 2 7 1 111 27 56 26 -789 4 2 7 1 74 80 70 165 -790 4 2 7 1 94 46 110 47 -791 4 2 7 1 54 14 15 123 -792 4 2 7 1 140 142 137 160 -793 4 2 7 1 92 38 126 39 -794 4 2 7 1 113 117 111 162 -795 4 2 7 1 63 64 55 160 -796 4 2 7 1 9 1 14 85 -797 4 2 7 1 38 86 5 33 -798 4 2 7 1 54 10 9 75 -799 4 2 7 1 114 110 117 167 -800 4 2 7 1 34 33 72 92 -801 4 2 7 1 11 10 49 70 -802 4 2 7 1 22 3 145 65 -803 4 2 7 1 103 8 144 44 -804 4 2 7 1 146 66 2 20 -805 4 2 7 1 143 42 6 104 -806 4 2 7 1 144 30 8 119 -807 4 2 7 1 120 29 3 145 -808 4 2 7 1 34 76 35 87 -809 4 2 7 1 122 91 39 40 -810 4 2 7 1 119 48 8 103 -811 4 2 7 1 3 25 120 65 -812 4 2 7 1 79 18 129 19 -813 4 2 7 1 53 15 16 127 -814 4 2 7 1 67 17 4 134 -815 4 2 7 1 2 23 83 146 -816 4 2 7 1 6 143 84 24 -817 4 2 7 1 67 4 28 121 -818 4 2 7 1 45 7 105 118 -819 4 2 7 1 7 41 105 132 -820 4 2 7 1 132 118 7 32 -821 4 2 7 1 121 134 4 31 -822 4 2 7 1 37 6 104 84 -823 4 2 7 1 13 2 83 66 -824 4 2 7 1 135 1 18 85 -825 4 2 7 1 5 19 86 133 -826 4 2 7 1 166 116 152 115 -827 4 2 7 1 166 152 116 158 -828 4 2 7 1 69 152 155 57 -829 4 2 7 1 155 152 69 147 -830 4 2 7 1 158 119 108 149 -831 4 2 7 1 158 108 119 112 -832 4 2 7 1 155 69 165 147 -833 4 2 7 1 155 165 69 60 -834 4 2 7 1 81 165 163 70 -835 4 2 7 1 163 165 81 150 -836 4 2 7 1 162 109 167 153 -837 4 2 7 1 162 167 109 117 -838 4 2 7 1 101 143 161 104 -839 4 2 7 1 101 161 143 138 -840 4 2 7 1 66 137 20 146 -841 4 2 7 1 20 137 66 63 +19 113 1 113 +1 1 1 2 +1 3 9 +2 9 7 +1 2 1 1 +3 5 7 +1 3 1 2 +4 1 10 +5 10 5 +1 4 1 2 +6 1 11 +7 11 3 +1 5 1 2 +8 4 12 +9 12 8 +1 6 1 1 +10 7 8 +1 7 1 1 +11 3 4 +1 8 1 2 +12 2 13 +13 13 6 +1 9 1 1 +14 6 8 +1 10 1 2 +15 2 14 +16 14 4 +1 11 1 1 +17 1 2 +1 12 1 1 +18 5 6 +2 1 2 9 +19 10 1 16 +20 1 11 16 +21 9 11 3 +22 7 5 15 +23 5 10 15 +24 9 7 15 +25 11 9 16 +26 9 15 16 +27 15 10 16 +2 2 2 8 +28 3 4 18 +29 9 3 18 +30 4 12 18 +31 8 7 17 +32 7 9 17 +33 12 8 17 +34 17 9 18 +35 12 17 18 +2 3 2 9 +36 2 13 20 +37 14 2 20 +38 14 12 4 +39 6 8 19 +40 13 6 19 +41 8 12 19 +42 12 14 20 +43 19 12 20 +44 13 19 20 +2 4 2 8 +45 1 22 2 +46 10 22 1 +47 2 22 13 +48 6 21 5 +49 5 21 10 +50 13 21 6 +51 21 22 10 +52 13 22 21 +2 5 2 8 +53 1 2 24 +54 11 1 24 +55 2 14 24 +56 4 3 23 +57 3 11 23 +58 14 4 23 +59 23 11 24 +60 14 23 24 +2 6 2 4 +61 6 5 25 +62 5 7 25 +63 8 6 25 +64 7 8 25 +3 1 4 49 +65 16 20 19 17 +66 20 22 16 21 +67 20 16 18 17 +68 17 19 16 15 +69 19 20 16 21 +70 23 24 16 20 +71 21 16 19 15 +72 18 23 16 20 +73 16 9 18 17 +74 18 12 20 17 +75 19 20 12 17 +76 16 9 17 15 +77 22 16 24 20 +78 10 16 22 21 +79 20 13 22 21 +80 25 21 19 15 +81 25 7 5 15 +82 6 25 19 8 +83 17 25 19 15 +84 13 20 19 21 +85 10 16 21 15 +86 2 1 24 22 +87 23 18 14 20 +88 23 16 11 18 +89 24 1 16 22 +90 22 24 2 20 +91 18 12 14 20 +92 9 11 16 18 +93 1 10 16 22 +94 13 22 2 20 +95 18 3 23 4 +96 11 24 16 23 +97 24 23 14 20 +98 23 11 3 18 +99 23 18 4 14 +100 12 4 18 14 +101 9 3 11 18 +102 21 25 5 15 +103 19 21 25 6 +104 21 25 6 5 +105 8 25 19 17 +106 25 17 7 15 +107 21 19 13 6 +108 5 10 21 15 +109 24 16 1 11 +110 24 14 2 20 +111 19 12 8 17 +112 7 17 9 15 +113 17 8 25 7 $EndElements diff --git a/modart_method/tests/test_room_modart_simple.msh b/modart_method/tests/test_room_modart_simple.msh deleted file mode 100644 index 49ac2b2..0000000 --- a/modart_method/tests/test_room_modart_simple.msh +++ /dev/null @@ -1,81 +0,0 @@ -$MeshFormat -2.2 0 8 -$EndMeshFormat -$PhysicalNames -8 -1 8 "default" -2 1 "floor" -2 2 "wall1" -2 3 "ceiling" -2 4 "wall2" -2 5 "wall3" -2 6 "wall4" -3 7 "RoomVolume" -$EndPhysicalNames -$Nodes -17 -1 0 5.1 0 -2 6.21 4 0 -3 5.52 0 0 -4 0 0 0 -5 0 5.1 3.3 -6 6.21 4 3.3 -7 0 0 3.3 -8 5.52 0 3.3 -9 3.104999999991609 4.550000000001486 0 -10 3.104999999991609 4.550000000001486 3.3 -11 4.302270987143737 2.269570754717705 0 -12 4.657499999995805 4.275000000000743 1.650000000000001 -13 1.552499999995804 4.825000000000743 1.65 -14 4.302270987143737 2.269570754717706 3.3 -15 2.76 0 1.649999999999999 -16 0 2.55 1.65 -17 5.865 2 1.649999999999999 -$EndNodes -$Elements -44 -1 1 2 8 1 1 9 -2 1 2 8 1 9 2 -3 1 2 8 2 1 4 -4 1 2 8 3 1 5 -5 1 2 8 4 2 3 -6 1 2 8 5 2 6 -7 1 2 8 6 3 4 -8 1 2 8 7 3 8 -9 1 2 8 8 4 7 -10 1 2 8 9 5 10 -11 1 2 8 9 10 6 -12 1 2 8 10 5 7 -13 1 2 8 11 6 8 -14 1 2 8 12 7 8 -15 2 2 1 1 4 9 11 -16 2 2 1 1 3 4 11 -17 2 2 1 1 2 3 11 -18 2 2 1 1 9 2 11 -19 2 2 1 1 1 9 4 -20 2 2 2 2 5 13 1 -21 2 2 2 2 2 12 6 -22 2 2 2 2 9 12 2 -23 2 2 2 2 6 12 10 -24 2 2 2 2 9 13 12 -25 2 2 2 2 12 13 10 -26 2 2 2 2 1 13 9 -27 2 2 2 2 10 13 5 -28 2 2 3 3 7 14 10 -29 2 2 3 3 8 14 7 -30 2 2 3 3 6 14 8 -31 2 2 3 3 10 14 6 -32 2 2 3 3 10 5 7 -33 2 2 4 4 4 15 7 -34 2 2 4 4 8 15 3 -35 2 2 4 4 3 15 4 -36 2 2 4 4 7 15 8 -37 2 2 5 5 5 1 16 -38 2 2 5 5 4 7 16 -39 2 2 5 5 1 4 16 -40 2 2 5 5 7 5 16 -41 2 2 6 6 3 2 17 -42 2 2 6 6 6 8 17 -43 2 2 6 6 2 6 17 -44 2 2 6 6 8 3 17 -$EndElements From 344216d085d808aa01c6875ab71499cf9aba55c3 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Wed, 13 May 2026 14:56:46 +0200 Subject: [PATCH 13/40] Tests pass with the new test inputs. --- example_settings/modart_setting.json | 13 +++- .../modart_interface/modart_interface.py | 70 ++++++++++++------- modart_method/tests/test_input_modart.json | 6 +- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/example_settings/modart_setting.json b/example_settings/modart_setting.json index 7b14e6a..0bbf8d3 100644 --- a/example_settings/modart_setting.json +++ b/example_settings/modart_setting.json @@ -1,6 +1,17 @@ { "type": "simulationSettings", "options": [ + { + "name": "Response duration", + "id": "durat", + "type": "float", + "display": "text", + "min": 1e-1, + "max": 1e1, + "default": 1e0, + "step": 1e-1, + "endAdornment": "s" + }, { "name": "Echogram sample rate", "id": "f_e", @@ -57,7 +68,7 @@ }, { "name": "Atmospheric pressure", - "id": "temp", + "id": "pres", "type": "float", "display": "text", "min": 50.0, diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 5060cea..50bd6ec 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -1,11 +1,14 @@ """Module implementing a CHORAS interface for MoDART. """ +import re import json import meshio import numpy as np from pprint import pprint from pathlib import Path +import matplotlib.pyplot as plt + from raves import raves, run_MoDART from raves.src.utils import visualize_mesh @@ -16,13 +19,17 @@ from .definition import SimulationMethod +# TODO: This function exists in "raves" but it not exposed. Perhaps it should be. +def sanitize_ascii(s: str) -> str: + return re.sub(r'[\W_]+', '_', s, flags=re.ASCII).strip('_') + + + def convert_mesh(input_file_path: str | Path | None = None, output_folder_path: str | Path | None = None): if type(output_folder_path) == str: output_folder_path = Path(output_folder_path) - print('\n\nSTARTING MESH CONVERSION.\n') - # TODO: Ensure that the mesh is triangulated. mesh = meshio.read(input_file_path) @@ -74,7 +81,7 @@ def convert_mesh(input_file_path: str | Path | None = None, for tri_id in range(num_triangles): # TODO: Consider the possibility of non-uniform materials within group; split group if that's the case. mat_id = mesh.cell_data['gmsh:physical'][triangle_cell_ids[tri_id]][0] - triangle_materials.append(material_names[mat_id]) + triangle_materials.append(sanitize_ascii(material_names[mat_id])) obj_output_lines = list() mtl_output_lines = list() @@ -82,9 +89,9 @@ def convert_mesh(input_file_path: str | Path | None = None, obj_output_lines.append('mtllib mesh.mtl\n') for v in vertices: - rounded_coords = [np.round(c, 3) for c in v] - line = 'v ' + ' '.join([str(c) for c in rounded_coords]) + '\n' + line = 'v ' + ' '.join([str(c) for c in v]) + '\n' obj_output_lines.append(line) + for i in range(num_triangles): patch_name = f'Patch_{triangle_group_ids[i]+1}_Mat_{triangle_materials[i]}' @@ -128,7 +135,7 @@ def save_materials_file(json_file_path: str | Path): raise RuntimeError('The frequencies should be known before the coefficients are read.') else: assert len(freq_bands) == len(coeffs) - absorptions[material] = coeffs + absorptions[sanitize_ascii(material)] = coeffs with open(str(Path(result_container['MoDART_data_subfolder']) / 'materials.csv'), mode='w') as file: line = 'Frequencies, ' + ', '.join([str(f) for f in freq_bands]) + '\n' @@ -263,24 +270,31 @@ def _modart_method(self, json_file_path: str | Path) -> None: environment_folder = result_container['MoDART_data_subfolder'] - visualize_mesh(environment_folder) - - # TODO: this will eventually be named differently in the JSON. - response_duration = result_container['simulationSettings']['de_ir_length'] - - # TODO: Load and use simulation settings. - - # TODO: echogram_sample_rate will eventually be a parameter set by the user. - echogram_sample_rate = int(1e3) - # TODO: audio_sample_rate will eventually be a parameter set by...? - audio_sample_rate = int(44.1e3) + # visualize_mesh(environment_folder) + + audio_sample_rate = result_container['fs_auralization'] + response_duration = result_container['simulationSettings']['durat'] + echogram_sample_rate = result_container['simulationSettings']['f_e'] + multiprocess_pool_size = result_container['simulationSettings']['pool'] + humidity = result_container['simulationSettings']['humi'] + temperature = result_container['simulationSettings']['temp'] + pressure = result_container['simulationSettings']['pres'] + points_per_square_meter = result_container['simulationSettings']['ppsm'] + rays_per_hemisphere = result_container['simulationSettings']['rays'] + T60_threshold = result_container['simulationSettings']['T60'] + max_slopes_per_band = result_container['simulationSettings']['slopes'] - raise NotImplementedError('Stopping here.') - # Run the pre-processing (shared by all sources, listeners). # TODO: Update progress bar. try: - raves(environment_folder) + raves(environment_folder, + echogram_sample_rate=echogram_sample_rate, + multiprocess_pool_size=multiprocess_pool_size, + humidity=humidity, temperature=temperature, pressure=pressure, + points_per_square_meter=points_per_square_meter, + rays_per_hemisphere=rays_per_hemisphere, + T60_threshold=T60_threshold, max_slopes_per_band=max_slopes_per_band, + skip_T60_plots=True) except Exception as exc: raise RuntimeError('Failed to run the pre-processing environment analysis.') from exc @@ -293,12 +307,18 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Generate the echograms with MoD-ART. try: - MoDART_echograms, frequencies, _ = run_MoDART(environment_folder, - source_position, listener_positions, - echogram_duration=response_duration, - echogram_sample_rate=echogram_sample_rate) + MoDART_tuple = run_MoDART(environment_folder, + source_position, listener_positions, + echogram_duration=response_duration, + echogram_sample_rate=echogram_sample_rate, + humidity=humidity, temperature=temperature, pressure=pressure, + num_rays=rays_per_hemisphere) + MoDART_echograms, frequencies, MoDART_data = MoDART_tuple except Exception as exc: raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc + + print(frequencies) + pprint(MoDART_data) # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) @@ -316,7 +336,7 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Amplitude-modulate band-passed stochastic signals to produce an impulse response. try: responses = noise_shaping(audio_sample_rate, len(audio_time_axis), - frequencies, envelopes) + frequencies, envelopes) except Exception as exc: raise RuntimeError(f'Failed to generate responses for simulation #{sim_idx+1}.') from exc diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json index bb8d96b..92a54de 100644 --- a/modart_method/tests/test_input_modart.json +++ b/modart_method/tests/test_input_modart.json @@ -51,11 +51,13 @@ "task_id": "6511d180-1431-4d89-bcd5-a81dceab6f3a", "fs_auralization": 44100, "simulationSettings": { + "durat": 1, "f_e": 5000, "T60": 0.1, - "slopes": 10, + "slopes": 2, "humi": 50, - "temp": 100, + "temp": 20, + "pres": 100, "ppsm": 30, "rays": 1000, "pool": 4 From a160c033d30e0a1c85eecc7b1991825145de7945 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Wed, 13 May 2026 15:09:17 +0200 Subject: [PATCH 14/40] Simulation ran in docker! --- modart_method/modart_interface/modart_interface.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 50bd6ec..52c24cf 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -317,9 +317,6 @@ def _modart_method(self, json_file_path: str | Path) -> None: except Exception as exc: raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc - print(frequencies) - pprint(MoDART_data) - # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) From c8f692262ef671c0a60795d741cca82308c5a0cc Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Wed, 13 May 2026 15:14:51 +0200 Subject: [PATCH 15/40] Request later version of raves --- modart_method/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index 05dc50b..fe27caf 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ - "raves>=0.1.1", + "raves>=0.1.2", "meshio>=5.3.5", "requests", ] From b610bb57d3ca486a397a3ed24f921b80cfab5ef4 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 18 May 2026 13:39:29 +0200 Subject: [PATCH 16/40] Reconfigured mesh conversion to use gmsh --- modart_method/Dockerfile | 1 - .../modart_interface/modart_interface.py | 106 +++++++++--------- modart_method/pyproject.toml | 2 +- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/modart_method/Dockerfile b/modart_method/Dockerfile index bcb8060..b766d96 100644 --- a/modart_method/Dockerfile +++ b/modart_method/Dockerfile @@ -9,7 +9,6 @@ RUN apt-get update && apt-get install -y \ build-essential \ gmsh \ && rm -rf /var/lib/apt/lists/* -# Note: I tried removing "gmsh \" from the command above, but the build took ages. Why? # Copy method package directory COPY modart_method /app/modart_method diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 52c24cf..be333ab 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -2,7 +2,7 @@ """ import re import json -import meshio +import gmsh import numpy as np from pprint import pprint from pathlib import Path @@ -25,64 +25,66 @@ def sanitize_ascii(s: str) -> str: -def convert_mesh(input_file_path: str | Path | None = None, +def convert_mesh(geo_file_path: str | Path | None = None, output_folder_path: str | Path | None = None): if type(output_folder_path) == str: output_folder_path = Path(output_folder_path) - # TODO: Ensure that the mesh is triangulated. - - mesh = meshio.read(input_file_path) - - vertices = np.array(mesh.points).squeeze() - num_vertices = vertices.shape[0] - - triangles = list() - triangle_cell_ids = list() - triangle_group_ids = list() - num_groups = 0 - for cell_id, cell in enumerate(mesh.cells): - if cell.type == 'triangle': - group = cell.data - - if group.ndim == 1: - assert group.shape[0] == 3, 'Bad cell shape.' - triangles.append(group) - triangle_cell_ids.append(cell_id) - triangle_group_ids.append(num_groups) - - elif group.ndim == 2: - assert group.shape[1] == 3, 'Bad cell shape.' - for tri in group: - triangles.append(tri) - triangle_cell_ids.append(cell_id) - triangle_group_ids.append(num_groups) - else: - raise AssertionError('Bad cell shape.') - - num_groups += 1 - + # TODO: Add gmsh to dependencies + + gmsh.initialize() + try: + # This factor scales the size of triangles generated by gmsh. + # The triangulation has no effect on the results, it only slows things down; + # make it as large as possible. + gmsh.option.setNumber('Mesh.MeshSizeFactor', 100) + + gmsh.open(geo_file_path) + + gmsh.model.mesh.generate(2) + + material_tags = gmsh.model.getPhysicalGroups(dim=2) + material_names = [gmsh.model.getPhysicalName(dim, tag) + for (dim, tag) in material_tags] + surface_entities = {name: [tag for dim, tag in gmsh.model.getEntitiesForPhysicalName(name)] + for name in material_names} + + node_tags_all, coords_all, _ = gmsh.model.mesh.getNodes() + vertices = coords_all.reshape((len(node_tags_all), 3)) + num_vertices = vertices.shape[0] + + gmsh_triangle_type = gmsh.model.mesh.getElementType('triangle', 1) + + triangles = list() + triangle_materials = list() + triangle_patch_ids = list() + num_patches = 0 + + for material_name, entity_tags_list in surface_entities.items(): + for tag in entity_tags_list: + face_nodes = gmsh.model.mesh.getElementFaceNodes(gmsh_triangle_type, 3, tag=tag) + num_triangles_in_element = len(face_nodes) // 3 + if num_triangles_in_element == 0: + continue + faces = np.reshape(face_nodes, (num_triangles_in_element, 3)) + + num_patches += 1 + for face in faces: + triangles.append(face) + triangle_materials.append(sanitize_ascii(material_name)) + triangle_patch_ids.append(num_patches) + finally: + gmsh.finalize() + triangles = np.array(triangles) num_triangles = triangles.shape[0] - triangle_cell_ids = np.array(triangle_cell_ids) - triangle_group_ids = np.array(triangle_group_ids) + triangle_patch_ids = np.array(triangle_patch_ids) # N.B.: the triangle normals are inverted w.r.t. what MoD-ART expects. Flip them. triangles = triangles[:, ::-1] - assert np.all(triangles < num_vertices), 'The triangle definitions include vertex indices out of range.' + assert np.all(triangles <= num_vertices), 'The triangle definitions include vertex indices out of range.' - material_names = dict() - for k, [mat_idx, n_dims] in mesh.field_data.items(): - if n_dims == 2: - material_names[mat_idx] = k - - triangle_materials = list() - for tri_id in range(num_triangles): - # TODO: Consider the possibility of non-uniform materials within group; split group if that's the case. - mat_id = mesh.cell_data['gmsh:physical'][triangle_cell_ids[tri_id]][0] - triangle_materials.append(sanitize_ascii(material_names[mat_id])) - obj_output_lines = list() mtl_output_lines = list() @@ -93,10 +95,10 @@ def convert_mesh(input_file_path: str | Path | None = None, obj_output_lines.append(line) for i in range(num_triangles): - patch_name = f'Patch_{triangle_group_ids[i]+1}_Mat_{triangle_materials[i]}' + patch_name = f'Patch_{triangle_patch_ids[i]}_Mat_{triangle_materials[i]}' obj_output_lines.append(f'usemtl {patch_name}\n') - obj_output_lines.append('f ' + ' '.join([str(v+1) for v in triangles[i]]) + '\n') + obj_output_lines.append('f ' + ' '.join([str(v) for v in triangles[i]]) + '\n') mtl_output_lines.append(f'newmtl {patch_name}\n') rand_color = np.round(np.random.uniform(size=3), 3) @@ -233,7 +235,7 @@ def run_simulation(self) -> None: result_container = json.load(json_file) # Create a folder for the "temporary" ART and MoD-ART data. - temp_subfolder = Path(result_container['msh_path']).parent / 'MoDART_data' + temp_subfolder = Path(result_container['geo_path']).parent / 'MoDART_data' result_container['MoDART_data_subfolder'] = str(temp_subfolder) if not Path.is_dir(temp_subfolder): Path.mkdir(temp_subfolder) @@ -244,7 +246,7 @@ def run_simulation(self) -> None: # Convert the .msh file into the format expected by MoD-ART. try: - convert_mesh(result_container['msh_path'], temp_subfolder) + convert_mesh(result_container['geo_path'], temp_subfolder) except Exception as exc: raise RuntimeError('Failed to reformat the input mesh as required.') from exc diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index fe27caf..0398fd4 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "raves>=0.1.2", - "meshio>=5.3.5", + "gmsh==4.13.1", "requests", ] From fcb7547c3fb3527297c61e0be7046e4d319f0026 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 18 May 2026 15:05:26 +0200 Subject: [PATCH 17/40] Revised comments --- .../modart_interface/modart_interface.py | 243 ++++++++++++------ 1 file changed, 171 insertions(+), 72 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index be333ab..b142bf3 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -1,16 +1,12 @@ """Module implementing a CHORAS interface for MoDART. """ -import re import json import gmsh import numpy as np -from pprint import pprint from pathlib import Path -import matplotlib.pyplot as plt - from raves import raves, run_MoDART -from raves.src.utils import visualize_mesh +from raves.src.utils import sanitize_ascii from numpy.random import default_rng from scipy.signal import butter, sosfilt @@ -19,19 +15,25 @@ from .definition import SimulationMethod -# TODO: This function exists in "raves" but it not exposed. Perhaps it should be. -def sanitize_ascii(s: str) -> str: - return re.sub(r'[\W_]+', '_', s, flags=re.ASCII).strip('_') - - - -def convert_mesh(geo_file_path: str | Path | None = None, - output_folder_path: str | Path | None = None): - if type(output_folder_path) == str: - output_folder_path = Path(output_folder_path) - - # TODO: Add gmsh to dependencies +def convert_mesh(geo_file_path: str | Path, + output_folder_path: str | Path) -> None: + """ + Read a .geo file (using gmsh), triangulate its surfaces, and save the result + in Wavefront format (.obj + .mtl) as expected by MoD-ART. + + Parameters + ---------- + geo_file_path : str or Path + Path to the .geo file to be read. + output_folder_path : str or Path + Path to the folder where converted mesh files will be saved. + """ + if not Path.is_file(geo_file_path): + raise FileNotFoundError('The specified .geo file could not be found.') + if not Path.is_dir(output_folder_path): + raise NotADirectoryError('The specified output folder could not be found.') + # Run the gmsh session in a "protected" scope to make sure it gets finalized no matter what. gmsh.initialize() try: # This factor scales the size of triangles generated by gmsh. @@ -39,70 +41,109 @@ def convert_mesh(geo_file_path: str | Path | None = None, # make it as large as possible. gmsh.option.setNumber('Mesh.MeshSizeFactor', 100) + # Read the input file... gmsh.open(geo_file_path) - + # ...and mesh its surfaces (2D elements). gmsh.model.mesh.generate(2) + # Retrieve the integer IDs of all surface materials (2D physical groups). material_tags = gmsh.model.getPhysicalGroups(dim=2) + # Retrieve the names of surface materials, as strings. material_names = [gmsh.model.getPhysicalName(dim, tag) for (dim, tag) in material_tags] + # Retrieve the lists of "entities" characterized by each of the retrieved surface materials. surface_entities = {name: [tag for dim, tag in gmsh.model.getEntitiesForPhysicalName(name)] for name in material_names} + # Retrieve the 3D coordinates of mesh vertices, and format them as an (N, 3) array. node_tags_all, coords_all, _ = gmsh.model.mesh.getNodes() vertices = coords_all.reshape((len(node_tags_all), 3)) num_vertices = vertices.shape[0] + # Reference object for gmsh elements of "triangle" type. gmsh_triangle_type = gmsh.model.mesh.getElementType('triangle', 1) + # Prepare the lists which will form the Wavefront-formatted faces. triangles = list() triangle_materials = list() triangle_patch_ids = list() num_patches = 0 + # Cycle over the detected surface materials. for material_name, entity_tags_list in surface_entities.items(): + # For each surface material, we found a list of "entities" which may be + # different polygons made of that material. + # TODO: Check if this understanding is correct. for tag in entity_tags_list: + # Find the triangles which form the surface entity. face_nodes = gmsh.model.mesh.getElementFaceNodes(gmsh_triangle_type, 3, tag=tag) num_triangles_in_element = len(face_nodes) // 3 + # Just in case a non-triangular surface element snuck in: ignore it. if num_triangles_in_element == 0: continue + # This forms an (M, 3) integer-valued array of vertex indices (1-indexed). faces = np.reshape(face_nodes, (num_triangles_in_element, 3)) + # For the purpose of MoD-ART, each "entity" is used as a surface patch. + # TODO: Allow automatic segmentation with maximum area threshold. num_patches += 1 for face in faces: triangles.append(face) + # Note: the material name is "sanitized" to only latin letters, digits, and underscores. triangle_materials.append(sanitize_ascii(material_name)) triangle_patch_ids.append(num_patches) finally: + # Close the gmsh session even in case of an exception. gmsh.finalize() + # Convert the lists to arrays, for easier handling. triangles = np.array(triangles) num_triangles = triangles.shape[0] triangle_patch_ids = np.array(triangle_patch_ids) - - # N.B.: the triangle normals are inverted w.r.t. what MoD-ART expects. Flip them. + + # The surface normals are inverted w.r.t. what MoD-ART expects. Flip them (by inverting the triangle winding). triangles = triangles[:, ::-1] + # Make sure all vertex indices are valid. assert np.all(triangles <= num_vertices), 'The triangle definitions include vertex indices out of range.' + # Prepare all of the lines to be written in each of the Wavefront files. obj_output_lines = list() mtl_output_lines = list() + # The .obj always starts by specifying the .mtl. obj_output_lines.append('mtllib mesh.mtl\n') + # List all of the vertex coordinates, in order, in the .obj. for v in vertices: line = 'v ' + ' '.join([str(c) for c in v]) + '\n' obj_output_lines.append(line) + # List all of the triangles in the .obj, and add their materials to the .mtl. + latest_patch_name = None for i in range(num_triangles): + # Consider the "dummy" material name assigned to this triangle. patch_name = f'Patch_{triangle_patch_ids[i]}_Mat_{triangle_materials[i]}' + # If it is different from the latest material specified in the .obj, it needs to be added. + if patch_name != latest_patch_name: + obj_output_lines.append(f'usemtl {patch_name}\n') + + # It also needs to be added to the .mtl, as it is a new material definition. + mtl_output_lines.append(f'newmtl {patch_name}\n') + # Assign the "dummy" material a random RGB color. + r, g, b = np.round(np.random.uniform(size=3), 3) + mtl_output_lines.append(f'Kd {r} {g} {b}\n') + + # All following triangles in the .obj implicitly use this material. + latest_patch_name = patch_name - obj_output_lines.append(f'usemtl {patch_name}\n') + # Add the vertex indices forming the triangle. + # Note: these are already 1-indexed, as they should be for the Wavefront format. obj_output_lines.append('f ' + ' '.join([str(v) for v in triangles[i]]) + '\n') - - mtl_output_lines.append(f'newmtl {patch_name}\n') - rand_color = np.round(np.random.uniform(size=3), 3) - mtl_output_lines.append(f'Kd {rand_color[0]} {rand_color[1]} {rand_color[2]}\n') + + # Appending filenames using " / " requires this to be a Path object, not a plain string. + if type(output_folder_path) == str: + output_folder_path = Path(output_folder_path) with open(str(output_folder_path / 'mesh.obj'), mode='w') as file: for line in obj_output_lines: @@ -112,11 +153,30 @@ def convert_mesh(geo_file_path: str | Path | None = None, file.write(line) -def save_materials_file(json_file_path: str | Path): - # Load the input JSON file +def save_materials_file(json_file_path: str | Path) -> None: + """ + Read the JSON settings file and save surface material data in the .csv format + expected by MoD-ART, in the folder spacified in the JSON. + + Parameters + ---------- + json_file_path : str or Path + Path to the JSON file to be read. + """ + if not Path.is_file(json_file_path): + raise FileNotFoundError('The specified JSON file could not be found.') + + # Load the input JSON file. with open(json_file_path, "r") as json_file: result_container = json.load(json_file) + # The formatted data needs to be saved in the specified folder. + if 'MoDART_data_subfolder' not in result_container: + raise ValueError('The MoD-ART data folder is not specified in the JSON.') + output_folder_path = Path(result_container['MoDART_data_subfolder']) + if not Path.is_dir(output_folder_path): + raise NotADirectoryError('The MoD-ART data folder could not be found.') + # TODO: For now we assume that the length of each # result_container['absorption_coefficients'].values() # is the same as the length of each @@ -130,6 +190,7 @@ def save_materials_file(json_file_path: str | Path): else: assert len(freq_bands) == len(freqs) + # Arrange the coefficients into a dict. absorptions = dict() for material, coeff_string in result_container['absorption_coefficients'].items(): coeffs = np.array(coeff_string.replace(',', '').split(' '), dtype=float) @@ -137,9 +198,12 @@ def save_materials_file(json_file_path: str | Path): raise RuntimeError('The frequencies should be known before the coefficients are read.') else: assert len(freq_bands) == len(coeffs) + # Note: the material name is "sanitized" to only latin letters, digits, and underscores, + # MATCHING THE NAME REPORTED IN THE CONVERTED MESH FILES. absorptions[sanitize_ascii(material)] = coeffs - with open(str(Path(result_container['MoDART_data_subfolder']) / 'materials.csv'), mode='w') as file: + # Save the data in .csv format. + with open(str(output_folder_path / 'materials.csv'), mode='w') as file: line = 'Frequencies, ' + ', '.join([str(f) for f in freq_bands]) + '\n' file.write(line) @@ -148,16 +212,53 @@ def save_materials_file(json_file_path: str | Path): file.write(line) # TODO: For now, scattering coefficients are arbitrarily set to 0.3. - # Eventually they will be sent by the backend. + # Eventually the true values will be specified in the JSON. line = material + ', 0.3\n' file.write(line) -def noise_shaping(fs: int | float, duration_in_samples: int, - frequencies: np.ndarray, envelopes: np.ndarray): - assert frequencies.ndim == 1 +def noise_shaping(fs: int | float, + band_centers: np.ndarray, + envelopes: np.ndarray) -> np.ndarray: + """ + Generate impulse responses based on band-wise signal envelopes, through the + amplitude modulation of stochastic signals. + + Parameters + ---------- + fs : int + Audio sample rate of the envelopes AND the final response. + band_centers : np.ndarray + One-dimensional array containing the center frequency of each band, in Hertz. + envelopes : np.ndarray + Four-dimensional array containing the modulation envelopes. + It has shape (S, R, B, T), where: + - S is the number of sound sources + - R is the number of receivers + - B is the number of frequency bands (must match length of "band_centers") + - T is the duration in samples. + + Returns + ------- + response : np.ndarray + Three-dimensional array containing the room impulse responses. + It has shape (S, R, T), matching the energy envelopes (all bands are combined). + + Notes + ----- + The provided envelopes are assumed to be AMPLITUDE, i.e., square-rooted energy. + The stochastic signal used for modulation is a Poisson process with lambda=0.5. + The signal is generated only once and used for all sources, receivers, and + frequency bands (band-passed appropriately for each). + The band-passing occurs BEFORE the amplitude modulation, so out-of-band artefacts + may be introduced if the envelopes are very jagged. + """ + assert band_centers.ndim == 1 assert envelopes.ndim == 4 - assert envelopes.shape[2] == frequencies.shape[0] + assert envelopes.shape[2] == band_centers.shape[0] + + num_bands = band_centers.shape[0] + duration_in_samples = envelopes.shape[3] # Random number generator for the stochastic signal to be modulated. rng = default_rng() @@ -174,14 +275,12 @@ def noise_shaping(fs: int | float, duration_in_samples: int, # Factor for octave-band boundaries. # TODO: These may become third-octave bands at some point. band_bound = np.sqrt(2) + # Consider the frequency band centers provided alongside the input data. - band_centers = frequencies.copy() - num_bands = len(band_centers) - - # Ensure that all frequencies support band-pass filtering. + # Ensure that all frequencies support filtering with the given sample rate. if np.any(band_centers * band_bound >= fs): print('Warning: the audio sample rate is too low for some frequency bands.') - # Select only acceptable bands. + # Select only the acceptable bands. band_centers = band_centers[band_centers * band_bound < fs] # Update the number of rendered bands. num_bands = len(band_centers) @@ -199,38 +298,38 @@ def noise_shaping(fs: int | float, duration_in_samples: int, # ...and apply it to the stochastic signal. filtered_noise_signals[b] = sosfilt(sos, noise_signal) - # The envelope array has shape (S, L, B, T), the noise signals have shape (B, T): + # The envelope array has shape (S, R, B, T), the noise signals have shape (B, T): # we need to add two "leading" dimensions, which is done using [None, None]. modulated_noise_signals = envelopes * filtered_noise_signals[None, None] # The dimension of index 2 holds the separate frequency bands. - # Sum the array along that dimension to obtain the complete room impulse responses. + # Sum the array along that dimension to obtain the complete impulse responses. return np.sum(modulated_noise_signals, axis=2) class MoDARTMethod(SimulationMethod): """Interface class to run the MoDART method. - The class implements method to run the calculations for the - MoDART simulation method. All required configuration parameters - are expected to be provided in the input JSON file passed during - initialization. - + The class implements a method to run the calculations for the + MoD-ART analysis and generate responses using its parameters. + All required configuration parameters are expected to be provided + in the input JSON file passed during initialization. """ - def __init__(self, input_json_path: str | Path | None = None): - """Initialize the MoDART method interface for the given JSON file.""" - super().__init__(input_json_path) - - def run_simulation(self) -> None: - """Run the simulation. + def __init__(self, input_json_path: str | Path): + """Initialize the MoD-ART interface for the given JSON file. Parameters ---------- - json_file_path : str | Path | None, optional - Path to the JSON file. If not provided, uses the path from initialization. + json_file_path : str | Path + Path to the JSON configuration file. + """ + super().__init__(input_json_path) + + def run_simulation(self) -> None: + """Prepare the input data and run the method proper. """ - # Load the input JSON file + # Load the JSON configuration file. with open(self.input_json_path, "r") as json_file: result_container = json.load(json_file) @@ -240,11 +339,11 @@ def run_simulation(self) -> None: if not Path.is_dir(temp_subfolder): Path.mkdir(temp_subfolder) - # Save the updated JSON (with the added MoDART_data_subfolder field) + # Save the updated JSON (with the added MoDART_data_subfolder field). with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) - # Convert the .msh file into the format expected by MoD-ART. + # Convert the .geo file into the format expected by MoD-ART. try: convert_mesh(result_container['geo_path'], temp_subfolder) except Exception as exc: @@ -256,24 +355,21 @@ def run_simulation(self) -> None: except Exception as exc: raise RuntimeError('Failed to reformat the material properties as required.') from exc - # Run MoD-ART. - self._modart_method(self.input_json_path) + # Run a one-shot MoD-ART simulation. + self._modart_method() - def _modart_method(self, json_file_path: str | Path) -> None: + def _modart_method(self) -> None: """ - Run MoDART simulation for acoustic wave propagation. - - Args: - json_file_path: Path to the JSON configuration file + Run a one-shot MoD-ART simulation, comprising: + - preparation of ART model (surface integrals); + - analysis of ART model (modal decomposition); + - generation of energy envelopes from MoD-ART parameters; + - generation of impulse responses from energy envelopes. """ - # Load the input JSON file - with open(json_file_path, "r") as json_file: + # Load the JSON file and extract its relevant contents. + with open(self.input_json_path, "r") as json_file: result_container = json.load(json_file) - environment_folder = result_container['MoDART_data_subfolder'] - - # visualize_mesh(environment_folder) - audio_sample_rate = result_container['fs_auralization'] response_duration = result_container['simulationSettings']['durat'] echogram_sample_rate = result_container['simulationSettings']['f_e'] @@ -300,6 +396,9 @@ def _modart_method(self, json_file_path: str | Path) -> None: except Exception as exc: raise RuntimeError('Failed to run the pre-processing environment analysis.') from exc + # Each element of "results" is assumed to be a simulation request for a different source + # in the same environment, analyzed once. + # TODO: Check that all relevant settings are identical across different "results" entries. for sim_idx, sim_dict in enumerate(result_container['results']): source_position = np.array([sim_dict['sourceX'], sim_dict['sourceY'], @@ -334,7 +433,7 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Amplitude-modulate band-passed stochastic signals to produce an impulse response. try: - responses = noise_shaping(audio_sample_rate, len(audio_time_axis), + responses = noise_shaping(audio_sample_rate, frequencies, envelopes) except Exception as exc: raise RuntimeError(f'Failed to generate responses for simulation #{sim_idx+1}.') from exc @@ -344,8 +443,8 @@ def _modart_method(self, json_file_path: str | Path) -> None: # Note that the first index of "responses" is for the single source position. result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'] = responses[0, rec_idx].tolist() - # Save the updated JSON - with open(json_file_path, "w") as json_output: + # Save the updated JSON. + with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) print("MoDART simulation completed successfully!") From f6a3752a969667a34a4a6374302bb9a0b96a8527 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 18 May 2026 15:13:08 +0200 Subject: [PATCH 18/40] Revised comments --- modart_method/modart_interface/modart_interface.py | 4 +++- modart_method/tests/test_modart_cli.py | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index b142bf3..4cbaa32 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -315,6 +315,9 @@ class MoDARTMethod(SimulationMethod): All required configuration parameters are expected to be provided in the input JSON file passed during initialization. """ + # TODO: Update progress bar. + # TODO: Add more tests? More example settings? + # TODO: Fill out metrics like T30? It will be done by the backend eventually. def __init__(self, input_json_path: str | Path): """Initialize the MoD-ART interface for the given JSON file. @@ -383,7 +386,6 @@ def _modart_method(self) -> None: max_slopes_per_band = result_container['simulationSettings']['slopes'] # Run the pre-processing (shared by all sources, listeners). - # TODO: Update progress bar. try: raves(environment_folder, echogram_sample_rate=echogram_sample_rate, diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index 8b118a1..27812a8 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -16,10 +16,6 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): with open(create_temporary_input_file, 'r') as f: data = json.load(f) - print('\n\tDEBUG MESSAGE: assessing modart_method results\n') - - # TODO: Add assertions specific to your simulation method - # For example, check that results were written to the JSON file assert "receiverResults" in data['results'][0]['responses'][0] results = data['results'][0]['responses'][0]['receiverResults'] assert results is not None From 2dc6872e67c6499d6dba29f5ed4f702387188cc8 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 19 May 2026 16:35:40 +0200 Subject: [PATCH 19/40] Updated progress bar. --- .../modart_interface/modart_interface.py | 89 +++++++++++++------ 1 file changed, 60 insertions(+), 29 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 4cbaa32..13815f8 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -5,7 +5,7 @@ import numpy as np from pathlib import Path -from raves import raves, run_MoDART +from raves import compute_ART, compute_MoDART, run_MoDART from raves.src.utils import sanitize_ascii from numpy.random import default_rng @@ -15,8 +15,8 @@ from .definition import SimulationMethod -def convert_mesh(geo_file_path: str | Path, - output_folder_path: str | Path) -> None: +def save_converted_mesh(geo_file_path: str | Path, + output_folder_path: str | Path) -> None: """ Read a .geo file (using gmsh), triangulate its surfaces, and save the result in Wavefront format (.obj + .mtl) as expected by MoD-ART. @@ -28,9 +28,9 @@ def convert_mesh(geo_file_path: str | Path, output_folder_path : str or Path Path to the folder where converted mesh files will be saved. """ - if not Path.is_file(geo_file_path): + if not Path(geo_file_path).is_file(): raise FileNotFoundError('The specified .geo file could not be found.') - if not Path.is_dir(output_folder_path): + if not Path(output_folder_path).is_dir(): raise NotADirectoryError('The specified output folder could not be found.') # Run the gmsh session in a "protected" scope to make sure it gets finalized no matter what. @@ -73,7 +73,7 @@ def convert_mesh(geo_file_path: str | Path, for material_name, entity_tags_list in surface_entities.items(): # For each surface material, we found a list of "entities" which may be # different polygons made of that material. - # TODO: Check if this understanding is correct. + # TODO: Confirm if it actually works like that. for tag in entity_tags_list: # Find the triangles which form the surface entity. face_nodes = gmsh.model.mesh.getElementFaceNodes(gmsh_triangle_type, 3, tag=tag) @@ -85,7 +85,7 @@ def convert_mesh(geo_file_path: str | Path, faces = np.reshape(face_nodes, (num_triangles_in_element, 3)) # For the purpose of MoD-ART, each "entity" is used as a surface patch. - # TODO: Allow automatic segmentation with maximum area threshold. + # TODO: Allow automatic segmentation with maximum area threshold (will use code from our upcoming paper). num_patches += 1 for face in faces: triangles.append(face) @@ -163,7 +163,7 @@ def save_materials_file(json_file_path: str | Path) -> None: json_file_path : str or Path Path to the JSON file to be read. """ - if not Path.is_file(json_file_path): + if not Path(json_file_path).is_file(): raise FileNotFoundError('The specified JSON file could not be found.') # Load the input JSON file. @@ -174,14 +174,14 @@ def save_materials_file(json_file_path: str | Path) -> None: if 'MoDART_data_subfolder' not in result_container: raise ValueError('The MoD-ART data folder is not specified in the JSON.') output_folder_path = Path(result_container['MoDART_data_subfolder']) - if not Path.is_dir(output_folder_path): + if not Path(output_folder_path).is_dir(): raise NotADirectoryError('The MoD-ART data folder could not be found.') - # TODO: For now we assume that the length of each + # TODO: For now, we assume that the length of each # result_container['absorption_coefficients'].values() # is the same as the length of each # result_container['results'][res_idx]['frequencies'] - # Eventually this will change. + # Eventually this will change. This portion of the code will need to be updated. freq_bands = None for res in result_container['results']: freqs = np.array(res['frequencies'], dtype=float) @@ -316,7 +316,7 @@ class MoDARTMethod(SimulationMethod): in the input JSON file passed during initialization. """ # TODO: Update progress bar. - # TODO: Add more tests? More example settings? + # TODO: Add more tests? More example settings and/or environments? # TODO: Fill out metrics like T30? It will be done by the backend eventually. def __init__(self, input_json_path: str | Path): @@ -339,7 +339,7 @@ def run_simulation(self) -> None: # Create a folder for the "temporary" ART and MoD-ART data. temp_subfolder = Path(result_container['geo_path']).parent / 'MoDART_data' result_container['MoDART_data_subfolder'] = str(temp_subfolder) - if not Path.is_dir(temp_subfolder): + if not temp_subfolder.is_dir(): Path.mkdir(temp_subfolder) # Save the updated JSON (with the added MoDART_data_subfolder field). @@ -348,7 +348,7 @@ def run_simulation(self) -> None: # Convert the .geo file into the format expected by MoD-ART. try: - convert_mesh(result_container['geo_path'], temp_subfolder) + save_converted_mesh(result_container['geo_path'], temp_subfolder) except Exception as exc: raise RuntimeError('Failed to reformat the input mesh as required.') from exc @@ -384,23 +384,47 @@ def _modart_method(self) -> None: rays_per_hemisphere = result_container['simulationSettings']['rays'] T60_threshold = result_container['simulationSettings']['T60'] max_slopes_per_band = result_container['simulationSettings']['slopes'] + + # Each element of "results" is assumed to be a simulation request for a different source + # in the same environment, analyzed once. + # TODO: Check that all relevant settings are identical across different "results" entries. + num_sims = len(result_container['results']) # Run the pre-processing (shared by all sources, listeners). + # Step 1: Prepare the ART model. try: - raves(environment_folder, - echogram_sample_rate=echogram_sample_rate, - multiprocess_pool_size=multiprocess_pool_size, - humidity=humidity, temperature=temperature, pressure=pressure, - points_per_square_meter=points_per_square_meter, - rays_per_hemisphere=rays_per_hemisphere, - T60_threshold=T60_threshold, max_slopes_per_band=max_slopes_per_band, - skip_T60_plots=True) + compute_ART(folder_path=environment_folder, + multiprocess_pool_size=multiprocess_pool_size, + humidity=humidity, temperature=temperature, pressure=pressure, + points_per_square_meter=points_per_square_meter, + rays_per_hemisphere=rays_per_hemisphere) except Exception as exc: - raise RuntimeError('Failed to run the pre-processing environment analysis.') from exc + raise RuntimeError('Failed to create the ART model (environment pre-processing).') from exc + + # Claim that the ART model constitutes 40% of the overall progress (very arbitrary). + for sim_idx in range(num_sims): + result_container['results'][sim_idx]['percentage'] = 40 + # Save the updated JSON. + with open(self.input_json_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + # Step 2: Analyze the ART model. + try: + compute_MoDART(folder_path=environment_folder, + echogram_sample_rate=echogram_sample_rate, + T60_threshold=T60_threshold, + max_slopes_per_band=max_slopes_per_band, + skip_T60_plots=True) + except Exception as exc: + raise RuntimeError('Failed to run the modal analysis (environment pre-processing).') from exc + + # Claim that the MoD-ART analysis constitutes another 40% of the overall progress (very arbitrary). + for sim_idx in range(num_sims): + result_container['results'][sim_idx]['percentage'] = 80 + # Save the updated JSON. + with open(self.input_json_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) - # Each element of "results" is assumed to be a simulation request for a different source - # in the same environment, analyzed once. - # TODO: Check that all relevant settings are identical across different "results" entries. for sim_idx, sim_dict in enumerate(result_container['results']): source_position = np.array([sim_dict['sourceX'], sim_dict['sourceY'], @@ -420,6 +444,12 @@ def _modart_method(self) -> None: except Exception as exc: raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc + # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). + result_container['results'][sim_idx]['percentage'] = 95 + # Save the updated JSON. + with open(self.input_json_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) @@ -445,8 +475,9 @@ def _modart_method(self) -> None: # Note that the first index of "responses" is for the single source position. result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'] = responses[0, rec_idx].tolist() - # Save the updated JSON. - with open(self.input_json_path, "w") as json_output: - json_output.write(json.dumps(result_container, indent=4)) + result_container['results'][sim_idx]['percentage'] = 100 + # Save the updated JSON. + with open(self.input_json_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) print("MoDART simulation completed successfully!") From 5731a662ff8a8c2e296cfcf7bfb1497b0492725e Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Wed, 20 May 2026 11:37:16 +0200 Subject: [PATCH 20/40] Removed completed TODO --- modart_method/modart_interface/modart_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 13815f8..b74d77a 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -315,7 +315,6 @@ class MoDARTMethod(SimulationMethod): All required configuration parameters are expected to be provided in the input JSON file passed during initialization. """ - # TODO: Update progress bar. # TODO: Add more tests? More example settings and/or environments? # TODO: Fill out metrics like T30? It will be done by the backend eventually. From 515dc859e239b0a992e1fc82b6bca79e373cdb6d Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Fri, 22 May 2026 15:19:28 +0200 Subject: [PATCH 21/40] Removed .msh from tests --- .../modart_interface/modart_interface.py | 70 ++++- modart_method/tests/conftest.py | 5 - modart_method/tests/test_fixtures.py | 1 - modart_method/tests/test_input_modart.json | 3 +- modart_method/tests/test_modart_cli.py | 13 +- modart_method/tests/test_room_modart.msh | 259 ------------------ 6 files changed, 80 insertions(+), 271 deletions(-) delete mode 100644 modart_method/tests/test_room_modart.msh diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index b74d77a..7568586 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -227,7 +227,7 @@ def noise_shaping(fs: int | float, Parameters ---------- fs : int - Audio sample rate of the envelopes AND the final response. + Audio sample rate of the envelopes AND the final response, in Hertz. band_centers : np.ndarray One-dimensional array containing the center frequency of each band, in Hertz. envelopes : np.ndarray @@ -307,6 +307,48 @@ def noise_shaping(fs: int | float, return np.sum(modulated_noise_signals, axis=2) +def schroeder_curves(fs: int | float, + band_centers: np.ndarray, + echograms: np.ndarray) -> np.ndarray: + """ + Convert a set of echograms into backward-integrated energy curves, with bandwitdh correction. + + Parameters + ---------- + fs : int + Audio sample rate used for the bandwidth correction, in Hertz. + band_centers : np.ndarray + One-dimensional array containing the center frequency of each band, in Hertz. + echograms : np.ndarray + Four-dimensional array containing the energy envelopes. + It has shape (S, R, B, T), where: + - S is the number of sound sources + - R is the number of receivers + - B is the number of frequency bands (must match length of "band_centers") + - T is the duration in samples. + + Returns + ------- + response : np.ndarray + Array of the same shape as "echograms", containing the energy decay curves. + """ + assert band_centers.ndim == 1 + assert echograms.ndim == 4 + assert echograms.shape[2] == band_centers.shape[0] + + # Factor for octave-band boundaries. + # TODO: These may become third-octave bands at some point. + band_bound = np.sqrt(2) + lower_thres = band_centers / band_bound + upper_thres = band_centers * band_bound + band_widths = upper_thres - lower_thres + + nyquist = fs / 2 + echograms *= band_widths[None, None, :, None] / nyquist + + return np.cumsum(echograms[:, :, :, ::-1], axis=-1)[:, :, :, ::-1] + + class MoDARTMethod(SimulationMethod): """Interface class to run the MoDART method. @@ -315,7 +357,9 @@ class MoDARTMethod(SimulationMethod): All required configuration parameters are expected to be provided in the input JSON file passed during initialization. """ - # TODO: Add more tests? More example settings and/or environments? + # TODO: Add more tests. + # Failure tests; make sure the correct exception is raised. + # More example settings and/or environments? # TODO: Fill out metrics like T30? It will be done by the backend eventually. def __init__(self, input_json_path: str | Path): @@ -443,12 +487,14 @@ def _modart_method(self) -> None: except Exception as exc: raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc + # Noise-shaping code, in case an impulse response was to be returned. + """ # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). result_container['results'][sim_idx]['percentage'] = 95 # Save the updated JSON. with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) - + # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) @@ -473,6 +519,24 @@ def _modart_method(self) -> None: for rec_idx in range(len(listener_positions)): # Note that the first index of "responses" is for the single source position. result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'] = responses[0, rec_idx].tolist() + """ + + EDCs = schroeder_curves(audio_sample_rate, frequencies, MoDART_echograms) + EDCs = np.clip(EDCs, 1e-20, None) + EDCs = 10 * np.log10(EDCs) + + time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) + + for rec_idx in range(len(listener_positions)): + for freq_idx, freq in enumerate(frequencies): + result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'].append( + { + "data": EDCs[0, rec_idx, freq_idx].tolist(), + "t": time_axis.tolist(), + "frequency": freq, + "type": "edc", + } + ) result_container['results'][sim_idx]['percentage'] = 100 # Save the updated JSON. diff --git a/modart_method/tests/conftest.py b/modart_method/tests/conftest.py index 0164de3..1139c91 100644 --- a/modart_method/tests/conftest.py +++ b/modart_method/tests/conftest.py @@ -36,17 +36,12 @@ def create_temporary_input_file(): input_tmp = load_default_input_data() geo_file = os.path.join( default_data_path(), "test_room_modart.geo") - msh_file = os.path.join( - default_data_path(), "test_room_modart.msh") with tempfile.TemporaryDirectory() as tmpdirname: tmp_path = Path(tmpdirname) / "temp_input.json" shutil.copy(geo_file, Path(tmpdirname)) - shutil.copy(msh_file, Path(tmpdirname)) input_tmp['geo_path'] = os.path.join( tmpdirname, "test_room_modart.geo") - input_tmp['msh_path'] = os.path.join( - tmpdirname, "test_room_modart.msh") with open(tmp_path, 'w') as f: json.dump(input_tmp, f) diff --git a/modart_method/tests/test_fixtures.py b/modart_method/tests/test_fixtures.py index fb4dc33..e9196be 100644 --- a/modart_method/tests/test_fixtures.py +++ b/modart_method/tests/test_fixtures.py @@ -12,7 +12,6 @@ def test_default_input_data_structure(default_input_data): assert "responses" in default_input_data["results"][0] assert len(default_input_data["results"][0]["responses"]) > 0 assert "geo_path" in default_input_data - assert "msh_path" in default_input_data assert "absorption_coefficients" in default_input_data diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json index 92a54de..bc72c7d 100644 --- a/modart_method/tests/test_input_modart.json +++ b/modart_method/tests/test_input_modart.json @@ -7,8 +7,7 @@ "e9774010-4e39-2847-b243-a52df0a2a06c": "0.02, 0.03, 0.04, 0.08, 0.15", "fc78b514-6596-764c-864a-6cfd3c416947": "0.01, 0.01, 0.01, 0.01, 0.01" }, - "msh_path": "/app/uploads/MeasurementRoom_tiny_7fe67a12829349da9195f35e4bb5aceb.msh", - "geo_path": "/app/uploads/MeasurementRoom_tiny_7fe67a12829349da9195f35e4bb5aceb.geo", + "geo_path": "tests/test_room_modart.geo", "results": [ { "label": "Source 1", diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index 27812a8..cd29d2a 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -2,6 +2,7 @@ import os import json import pytest +import numpy as np from modart_interface import main @@ -16,10 +17,20 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): with open(create_temporary_input_file, 'r') as f: data = json.load(f) - assert "receiverResults" in data['results'][0]['responses'][0] + assert 'receiverResults' in data['results'][0]['responses'][0] results = data['results'][0]['responses'][0]['receiverResults'] assert results is not None assert len(results) > 0 + for r in results: + assert len(r) == 4 + assert 'data' in r + assert 't' in r + assert 'frequency' in r + assert 'type' in r + assert r['type'] == 'edc' + assert len(r['data']) == len(r['t']) + + assert np.all(np.isfinite(r['data'])) # Verify that requests.post was called (save_results was executed) mock_requests_post.assert_called_once() diff --git a/modart_method/tests/test_room_modart.msh b/modart_method/tests/test_room_modart.msh deleted file mode 100644 index 20380ca..0000000 --- a/modart_method/tests/test_room_modart.msh +++ /dev/null @@ -1,259 +0,0 @@ -$MeshFormat -4.1 0 8 -$EndMeshFormat -$PhysicalNames -8 -1 8 "default" -2 1 "a89bb798-c0bc-0a40-b63f-4a07eb04fa22" -2 2 "e9774010-4e39-2847-b243-a52df0a2a06c" -2 3 "fc78b514-6596-764c-864a-6cfd3c416947" -2 4 "300368c5-62b4-ec42-bea3-bce21338c848" -2 5 "b51c2cdd-1035-224e-a373-8d4b9d277b38" -2 6 "1f791966-f811-4749-b3cf-6a2342a2f5fb" -3 7 "RoomVolume" -$EndPhysicalNames -$Entities -8 12 6 1 -1 0 0 0 0 -2 0 0 0.66 0 -3 0 1.02 0 0 -4 0 1.02 0.66 0 -5 1.104 0 0 0 -6 1.104 0 0.66 0 -7 1.242 0.8 0 0 -8 1.242 0.8 0.66 0 -1 0 0.8 0 1.242 1.02 0 1 8 2 3 -7 -2 1.104 0 0 1.242 0.8 0 1 8 2 5 -7 -3 0 0 0 1.104 0 0 1 8 2 1 -5 -4 0 0 0 0 1.02 0 1 8 2 1 -3 -5 0 0.8 0.66 1.242 1.02 0.66 1 8 2 4 -8 -6 1.242 0.8 0 1.242 0.8 0.66 1 8 2 7 -8 -7 0 1.02 0 0 1.02 0.66 1 8 2 3 -4 -8 0 0 0.66 1.104 0 0.66 1 8 2 2 -6 -9 1.104 0 0.66 1.242 0.8 0.66 1 8 2 6 -8 -10 0 0 0.66 0 1.02 0.66 1 8 2 2 -4 -11 0 0 0 0 0 0.66 1 8 2 1 -2 -12 1.104 0 0 1.104 0 0.66 1 8 2 5 -6 -1 0 0 0 1.242 1.02 0 1 1 4 1 -2 -3 4 -2 0 0.8 0 1.242 1.02 0.66 1 2 4 5 -6 -1 7 -3 0 0 0.66 1.242 1.02 0.66 1 3 4 8 9 -5 -10 -4 0 0 0 1.104 0 0.66 1 4 4 -8 -11 3 12 -5 0 0 0 0 1.02 0.66 1 5 4 10 -7 -4 11 -6 1.104 0 0 1.242 0.8 0.66 1 6 4 2 6 -9 -12 -1 0 0 0 1.242 1.02 0.66 1 7 6 1 2 3 4 5 6 -$EndEntities -$Nodes -27 25 1 25 -0 1 0 1 -1 -0 0 0 -0 2 0 1 -2 -0 0 0.66 -0 3 0 1 -3 -0 1.02 0 -0 4 0 1 -4 -0 1.02 0.66 -0 5 0 1 -5 -1.104 0 0 -0 6 0 1 -6 -1.104 0 0.66 -0 7 0 1 -7 -1.242 0.8 0 -0 8 0 1 -8 -1.242 0.8 0.66 -1 1 0 1 -9 -0.6209999999983563 0.9100000000002912 0 -1 2 0 0 -1 3 0 1 -10 -0.5519999999985621 0 0 -1 4 0 1 -11 -0 0.5099999999986879 0 -1 5 0 1 -12 -0.6209999999983563 0.9100000000002912 0.66 -1 6 0 0 -1 7 0 0 -1 8 0 1 -13 -0.5519999999985621 0 0.66 -1 9 0 0 -1 10 0 1 -14 -0 0.5099999999986879 0.66 -1 11 0 0 -1 12 0 0 -2 1 0 2 -15 -16 -0.7852490381181173 0.4214107236109463 0 -0.4072451905936675 0.3970536180544399 0 -2 2 0 2 -17 -18 -0.9314999999991784 0.8550000000001458 0.33 -0.3104999999991782 0.9650000000001459 0.33 -2 3 0 2 -19 -20 -0.7852490381181172 0.4214107236109463 0.66 -0.4072451905936674 0.3970536180544399 0.66 -2 4 0 2 -21 -22 -0.7716734693870362 0 0.33 -0.3265789254470112 0 0.33 -2 5 0 2 -23 -24 -0 0.6921428571422011 0.33 -0 0.3424285714279154 0.3300000000000001 -2 6 0 1 -25 -1.173 0.3999999999999998 0.33 -3 1 0 0 -$EndNodes -$Elements -19 113 1 113 -1 1 1 2 -1 3 9 -2 9 7 -1 2 1 1 -3 5 7 -1 3 1 2 -4 1 10 -5 10 5 -1 4 1 2 -6 1 11 -7 11 3 -1 5 1 2 -8 4 12 -9 12 8 -1 6 1 1 -10 7 8 -1 7 1 1 -11 3 4 -1 8 1 2 -12 2 13 -13 13 6 -1 9 1 1 -14 6 8 -1 10 1 2 -15 2 14 -16 14 4 -1 11 1 1 -17 1 2 -1 12 1 1 -18 5 6 -2 1 2 9 -19 10 1 16 -20 1 11 16 -21 9 11 3 -22 7 5 15 -23 5 10 15 -24 9 7 15 -25 11 9 16 -26 9 15 16 -27 15 10 16 -2 2 2 8 -28 3 4 18 -29 9 3 18 -30 4 12 18 -31 8 7 17 -32 7 9 17 -33 12 8 17 -34 17 9 18 -35 12 17 18 -2 3 2 9 -36 2 13 20 -37 14 2 20 -38 14 12 4 -39 6 8 19 -40 13 6 19 -41 8 12 19 -42 12 14 20 -43 19 12 20 -44 13 19 20 -2 4 2 8 -45 1 22 2 -46 10 22 1 -47 2 22 13 -48 6 21 5 -49 5 21 10 -50 13 21 6 -51 21 22 10 -52 13 22 21 -2 5 2 8 -53 1 2 24 -54 11 1 24 -55 2 14 24 -56 4 3 23 -57 3 11 23 -58 14 4 23 -59 23 11 24 -60 14 23 24 -2 6 2 4 -61 6 5 25 -62 5 7 25 -63 8 6 25 -64 7 8 25 -3 1 4 49 -65 16 20 19 17 -66 20 22 16 21 -67 20 16 18 17 -68 17 19 16 15 -69 19 20 16 21 -70 23 24 16 20 -71 21 16 19 15 -72 18 23 16 20 -73 16 9 18 17 -74 18 12 20 17 -75 19 20 12 17 -76 16 9 17 15 -77 22 16 24 20 -78 10 16 22 21 -79 20 13 22 21 -80 25 21 19 15 -81 25 7 5 15 -82 6 25 19 8 -83 17 25 19 15 -84 13 20 19 21 -85 10 16 21 15 -86 2 1 24 22 -87 23 18 14 20 -88 23 16 11 18 -89 24 1 16 22 -90 22 24 2 20 -91 18 12 14 20 -92 9 11 16 18 -93 1 10 16 22 -94 13 22 2 20 -95 18 3 23 4 -96 11 24 16 23 -97 24 23 14 20 -98 23 11 3 18 -99 23 18 4 14 -100 12 4 18 14 -101 9 3 11 18 -102 21 25 5 15 -103 19 21 25 6 -104 21 25 6 5 -105 8 25 19 17 -106 25 17 7 15 -107 21 19 13 6 -108 5 10 21 15 -109 24 16 1 11 -110 24 14 2 20 -111 19 12 8 17 -112 7 17 9 15 -113 17 8 25 7 -$EndElements From ad483c9e665792c1f5ea091348e0e1ac74d236df Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Fri, 22 May 2026 16:35:24 +0200 Subject: [PATCH 22/40] Updated test room to have multiple patches with the same material (to test gmsh logic) --- modart_method/modart_interface/modart_interface.py | 9 ++++++--- modart_method/tests/test_fixtures.py | 11 ++++++----- modart_method/tests/test_input_modart.json | 11 ++++------- modart_method/tests/test_room_modart.geo | 9 +++------ 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 7568586..0dae3dd 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -102,6 +102,7 @@ def save_converted_mesh(geo_file_path: str | Path, triangle_patch_ids = np.array(triangle_patch_ids) # The surface normals are inverted w.r.t. what MoD-ART expects. Flip them (by inverting the triangle winding). + # TODO: How can this be tested, to prevent breaking changes in the future? MoD-ART does not raise any errors if the normals are flipped. triangles = triangles[:, ::-1] # Make sure all vertex indices are valid. @@ -358,8 +359,9 @@ class MoDARTMethod(SimulationMethod): in the input JSON file passed during initialization. """ # TODO: Add more tests. - # Failure tests; make sure the correct exception is raised. # More example settings and/or environments? + # Failure tests; make sure the correct exception is raised + # (e.g., "with pytest.raises(FileNotFoundError, match='part of error message'):") # TODO: Fill out metrics like T30? It will be done by the backend eventually. def __init__(self, input_json_path: str | Path): @@ -380,7 +382,8 @@ def run_simulation(self) -> None: result_container = json.load(json_file) # Create a folder for the "temporary" ART and MoD-ART data. - temp_subfolder = Path(result_container['geo_path']).parent / 'MoDART_data' + geo_path = Path(result_container['geo_path']) + temp_subfolder = geo_path.parent / (str(geo_path.stem) + '_MoDART_data') result_container['MoDART_data_subfolder'] = str(temp_subfolder) if not temp_subfolder.is_dir(): Path.mkdir(temp_subfolder) @@ -486,7 +489,7 @@ def _modart_method(self) -> None: MoDART_echograms, frequencies, MoDART_data = MoDART_tuple except Exception as exc: raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc - + # Noise-shaping code, in case an impulse response was to be returned. """ # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). diff --git a/modart_method/tests/test_fixtures.py b/modart_method/tests/test_fixtures.py index e9196be..6db77d1 100644 --- a/modart_method/tests/test_fixtures.py +++ b/modart_method/tests/test_fixtures.py @@ -6,11 +6,12 @@ def test_default_input_data_structure(default_input_data): """Test that the default input data has the expected structure.""" assert "results" in default_input_data assert len(default_input_data["results"]) > 0 - assert "sourceX" in default_input_data["results"][0] - assert "sourceY" in default_input_data["results"][0] - assert "sourceZ" in default_input_data["results"][0] - assert "responses" in default_input_data["results"][0] - assert len(default_input_data["results"][0]["responses"]) > 0 + for i in range(len(default_input_data["results"])): + assert "sourceX" in default_input_data["results"][i] + assert "sourceY" in default_input_data["results"][i] + assert "sourceZ" in default_input_data["results"][i] + assert "responses" in default_input_data["results"][i] + assert len(default_input_data["results"][i]["responses"]) > 0 assert "geo_path" in default_input_data assert "absorption_coefficients" in default_input_data diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json index bc72c7d..639cbc9 100644 --- a/modart_method/tests/test_input_modart.json +++ b/modart_method/tests/test_input_modart.json @@ -1,11 +1,8 @@ { "absorption_coefficients": { - "1f791966-f811-4749-b3cf-6a2342a2f5fb": "0.02, 0.03, 0.04, 0.08, 0.15", - "300368c5-62b4-ec42-bea3-bce21338c848": "0.02, 0.03, 0.04, 0.08, 0.15", - "a89bb798-c0bc-0a40-b63f-4a07eb04fa22": "0.11, 0.22, 0.42, 0.57, 0.63", - "b51c2cdd-1035-224e-a373-8d4b9d277b38": "0.02, 0.03, 0.04, 0.08, 0.15", - "e9774010-4e39-2847-b243-a52df0a2a06c": "0.02, 0.03, 0.04, 0.08, 0.15", - "fc78b514-6596-764c-864a-6cfd3c416947": "0.01, 0.01, 0.01, 0.01, 0.01" + "walls": "0.02, 0.03, 0.04, 0.08, 0.15", + "floor": "0.11, 0.22, 0.42, 0.57, 0.63", + "ceiling": "0.01, 0.01, 0.01, 0.01, 0.01" }, "geo_path": "tests/test_room_modart.geo", "results": [ @@ -51,7 +48,7 @@ "fs_auralization": 44100, "simulationSettings": { "durat": 1, - "f_e": 5000, + "f_e": 10000, "T60": 0.1, "slopes": 2, "humi": 50, diff --git a/modart_method/tests/test_room_modart.geo b/modart_method/tests/test_room_modart.geo index 72a8c08..f2048e9 100644 --- a/modart_method/tests/test_room_modart.geo +++ b/modart_method/tests/test_room_modart.geo @@ -35,12 +35,9 @@ Plane Surface(5) = { 5 }; Plane Surface(6) = { 6 }; Surface Loop(1) = { 1, 2, 3, 4, 5, 6 }; -Physical Surface("a89bb798-c0bc-0a40-b63f-4a07eb04fa22") = { 1 }; -Physical Surface("e9774010-4e39-2847-b243-a52df0a2a06c") = { 2 }; -Physical Surface("fc78b514-6596-764c-864a-6cfd3c416947") = { 3 }; -Physical Surface("300368c5-62b4-ec42-bea3-bce21338c848") = { 4 }; -Physical Surface("b51c2cdd-1035-224e-a373-8d4b9d277b38") = { 5 }; -Physical Surface("1f791966-f811-4749-b3cf-6a2342a2f5fb") = { 6 }; +Physical Surface("floor") = { 1 }; +Physical Surface("ceiling") = { 3 }; +Physical Surface("walls") = { 2, 4, 5, 6 }; Volume(1) = { 1 }; Physical Volume("RoomVolume") = { 1 }; Physical Line("default") = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; From 6a6cdcf5f55eaa95b018fd4b9433019487411ed7 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Fri, 22 May 2026 16:35:40 +0200 Subject: [PATCH 23/40] Extended basic test --- .../modart_interface/modart_interface.py | 11 ++++ modart_method/tests/test_modart_cli.py | 58 +++++++++++++++---- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 0dae3dd..39f3dd6 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -471,6 +471,10 @@ def _modart_method(self) -> None: with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) + # Some MoD-ART parameters will be saved in the JSON. + # For now, this is just for debugging/testing. + result_container['MoDART_data'] = None + for sim_idx, sim_dict in enumerate(result_container['results']): source_position = np.array([sim_dict['sourceX'], sim_dict['sourceY'], @@ -490,6 +494,13 @@ def _modart_method(self) -> None: except Exception as exc: raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc + if result_container['MoDART_data'] is None: + result_container['MoDART_data'] = { + 'T60': MoDART_data['T60'].tolist(), + 'Band idx': MoDART_data['Band idx'].tolist(), + 'Eigenvector shape': MoDART_data['V_hat'].shape + } + # Noise-shaping code, in case an impulse response was to be returned. """ # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index cd29d2a..dcf9663 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -17,21 +17,55 @@ def test_modart_method_cli(mock_requests_post, create_temporary_input_file): with open(create_temporary_input_file, 'r') as f: data = json.load(f) - assert 'receiverResults' in data['results'][0]['responses'][0] - results = data['results'][0]['responses'][0]['receiverResults'] - assert results is not None + assert 'results' in data + results = data['results'] assert len(results) > 0 - for r in results: - assert len(r) == 4 - assert 'data' in r - assert 't' in r - assert 'frequency' in r - assert 'type' in r - assert r['type'] == 'edc' - assert len(r['data']) == len(r['t']) + for res in results: + assert 'responses' in res + responses = res['responses'] + assert len(responses) > 0 + for resp in responses: + assert 'receiverResults' in resp + rec_res = resp['receiverResults'] + assert rec_res is not None + assert len(rec_res) > 0 + for r in rec_res: + assert len(r) == 4 + assert 'data' in r + assert 't' in r + assert 'frequency' in r + assert 'type' in r + assert r['type'] == 'edc' + assert len(r['data']) == len(r['t']) - assert np.all(np.isfinite(r['data'])) + assert np.all(np.isfinite(r['data'])) + # The function returns a few MoD-ART parameters for debugging/testing. + assert 'MoDART_data' in data + MoDART_data = data['MoDART_data'] + assert len(MoDART_data) > 0 + + # N.B.: THE FOLLOWING TEST PARAMETERS ARE SPECIFIC TO THE EXAMPLE SETTINGS! + + # The test simulation asks for 2 slopes in 5 frequency bands, so there should be 10. + assert len(MoDART_data['T60']) == 10 + assert len(MoDART_data['Band idx']) == 10 + + # The band indices of the detected slopes should range from 0 to 4, + # and there should be exactly two of each. + unique, unique_counts = np.unique(MoDART_data['Band idx'], return_counts=True) + assert unique.tolist() == [0, 1, 2, 3, 4] + assert unique_counts.tolist() == [2, 2, 2, 2, 2] + + # Confirm that the T60 values are reasonably close to a reference run. + assert np.allclose(MoDART_data['T60'], + [0.56, 0.03, 0.32, 0.03, 0.19, 0.03, 0.13, 0.03, 0.1, 0.02], + rtol=0.05, atol=0.05) + + # The test room has 6 patches with full visibility, so there should be 6*(6-1)=30 paths. + # If the second dimension is != 30, that means something went wrong in the mesh decoding. + assert MoDART_data['Eigenvector shape'] == [10, 30] + # Verify that requests.post was called (save_results was executed) mock_requests_post.assert_called_once() From 746817ef57aacb283ad9cd127a633e27fcc74982 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 9 Jun 2026 10:34:17 +0200 Subject: [PATCH 24/40] Assume the list of receivers is the same for all sources. --- .../modart_interface/modart_interface.py | 111 ++++++++++-------- 1 file changed, 64 insertions(+), 47 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 39f3dd6..5c636c3 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -189,7 +189,7 @@ def save_materials_file(json_file_path: str | Path) -> None: if freq_bands is None: freq_bands = freqs else: - assert len(freq_bands) == len(freqs) + assert np.allclose(freq_bands, freqs) # Arrange the coefficients into a dict. absorptions = dict() @@ -431,12 +431,37 @@ def _modart_method(self) -> None: T60_threshold = result_container['simulationSettings']['T60'] max_slopes_per_band = result_container['simulationSettings']['slopes'] - # Each element of "results" is assumed to be a simulation request for a different source - # in the same environment, analyzed once. - # TODO: Check that all relevant settings are identical across different "results" entries. - num_sims = len(result_container['results']) + # TODO: Try to move all possible failure points (like accessing elements of the JSON) at the start. - # Run the pre-processing (shared by all sources, listeners). + # Each element of "results" is a simulation request for a different source in the same environment, + # which is analyzed only once. All share the same settings and the same list of receivers. + num_srcs = len(result_container['results']) + if num_srcs == 0: + raise ValueError('No source positions specified.') + + source_positions = np.zeros((num_srcs, 3)) + for src_idx in range(num_srcs): + src_dict = result_container['results'][src_idx] + + source_positions[src_idx] = np.array([src_dict['sourceX'], + src_dict['sourceY'], + src_dict['sourceZ']]) + + these_receiver_positions = np.array([[pos['x'], pos['y'], pos['z']] + for pos in src_dict['responses']]) + + if src_idx == 0: + receiver_positions = these_receiver_positions + elif these_receiver_positions.shape != receiver_positions.shape: + raise ValueError('The list of receiver positions is not the same for all sources.') + elif not np.allclose(receiver_positions, these_receiver_positions): + raise ValueError('The list of receiver positions is not the same for all sources.') + + num_rcvs = receiver_positions.shape[0] + if num_rcvs == 0: + raise ValueError('No receiver positions specified.') + + # Run the pre-processing (shared by all sources, receivers). # Step 1: Prepare the ART model. try: compute_ART(folder_path=environment_folder, @@ -448,8 +473,8 @@ def _modart_method(self) -> None: raise RuntimeError('Failed to create the ART model (environment pre-processing).') from exc # Claim that the ART model constitutes 40% of the overall progress (very arbitrary). - for sim_idx in range(num_sims): - result_container['results'][sim_idx]['percentage'] = 40 + for src_idx in range(num_srcs): + result_container['results'][src_idx]['percentage'] = 40 # Save the updated JSON. with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) @@ -465,46 +490,38 @@ def _modart_method(self) -> None: raise RuntimeError('Failed to run the modal analysis (environment pre-processing).') from exc # Claim that the MoD-ART analysis constitutes another 40% of the overall progress (very arbitrary). - for sim_idx in range(num_sims): - result_container['results'][sim_idx]['percentage'] = 80 + for src_idx in range(num_srcs): + result_container['results'][src_idx]['percentage'] = 80 # Save the updated JSON. with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) + # Generate the echograms with MoD-ART. + try: + MoDART_tuple = run_MoDART(environment_folder, + source_positions, receiver_positions, + echogram_duration=response_duration, + echogram_sample_rate=echogram_sample_rate, + humidity=humidity, temperature=temperature, pressure=pressure, + num_rays=rays_per_hemisphere) + MoDART_echograms, frequencies, MoDART_data = MoDART_tuple + except Exception as exc: + raise RuntimeError(f'Failed to generate echograms for simulation #{src_idx+1}.') from exc + # Some MoD-ART parameters will be saved in the JSON. # For now, this is just for debugging/testing. - result_container['MoDART_data'] = None - - for sim_idx, sim_dict in enumerate(result_container['results']): - source_position = np.array([sim_dict['sourceX'], - sim_dict['sourceY'], - sim_dict['sourceZ']]) - listener_positions = np.array([[pos['x'], pos['y'], pos['z']] - for pos in sim_dict['responses']]) - - # Generate the echograms with MoD-ART. - try: - MoDART_tuple = run_MoDART(environment_folder, - source_position, listener_positions, - echogram_duration=response_duration, - echogram_sample_rate=echogram_sample_rate, - humidity=humidity, temperature=temperature, pressure=pressure, - num_rays=rays_per_hemisphere) - MoDART_echograms, frequencies, MoDART_data = MoDART_tuple - except Exception as exc: - raise RuntimeError(f'Failed to generate echograms for simulation #{sim_idx+1}.') from exc - - if result_container['MoDART_data'] is None: - result_container['MoDART_data'] = { - 'T60': MoDART_data['T60'].tolist(), - 'Band idx': MoDART_data['Band idx'].tolist(), - 'Eigenvector shape': MoDART_data['V_hat'].shape - } - - # Noise-shaping code, in case an impulse response was to be returned. + if result_container['MoDART_data'] is None: + result_container['MoDART_data'] = { + 'T60': MoDART_data['T60'].tolist(), + 'Band idx': MoDART_data['Band idx'].tolist(), + 'Eigenvector shape': MoDART_data['V_hat'].shape + } + + for src_idx in range(num_srcs): + # Noise-shaping code (may need revising), in case an impulse response was to be returned. """ # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). - result_container['results'][sim_idx]['percentage'] = 95 + result_container['results'][src_idx]['percentage'] = 95 # Save the updated JSON. with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) @@ -527,12 +544,12 @@ def _modart_method(self) -> None: responses = noise_shaping(audio_sample_rate, frequencies, envelopes) except Exception as exc: - raise RuntimeError(f'Failed to generate responses for simulation #{sim_idx+1}.') from exc + raise RuntimeError(f'Failed to generate responses for simulation #{src_idx+1}.') from exc # Write results back to JSON. - for rec_idx in range(len(listener_positions)): + for rcv_idx in range(num_rcvs): # Note that the first index of "responses" is for the single source position. - result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'] = responses[0, rec_idx].tolist() + result_container['results'][src_idx]['responses'][rcv_idx]['receiverResults'] = responses[0, rcv_idx].tolist() """ EDCs = schroeder_curves(audio_sample_rate, frequencies, MoDART_echograms) @@ -541,18 +558,18 @@ def _modart_method(self) -> None: time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) - for rec_idx in range(len(listener_positions)): + for rcv_idx in range(num_rcvs): for freq_idx, freq in enumerate(frequencies): - result_container['results'][sim_idx]['responses'][rec_idx]['receiverResults'].append( + result_container['results'][src_idx]['responses'][rcv_idx]['receiverResults'].append( { - "data": EDCs[0, rec_idx, freq_idx].tolist(), + "data": EDCs[src_idx, rcv_idx, freq_idx].tolist(), "t": time_axis.tolist(), "frequency": freq, "type": "edc", } ) - result_container['results'][sim_idx]['percentage'] = 100 + result_container['results'][src_idx]['percentage'] = 100 # Save the updated JSON. with open(self.input_json_path, "w") as json_output: json_output.write(json.dumps(result_container, indent=4)) From a89a5b3b0ef94324531c273602e9cb6f7318296b Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 9 Jun 2026 13:40:16 +0200 Subject: [PATCH 25/40] Added more varied tests. --- .../modart_interface/modart_interface.py | 19 +- modart_method/tests/conftest.py | 120 +++++++++- modart_method/tests/test_modart_cli.py | 210 +++++++++++++----- 3 files changed, 277 insertions(+), 72 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 5c636c3..d4feee4 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -358,10 +358,8 @@ class MoDARTMethod(SimulationMethod): All required configuration parameters are expected to be provided in the input JSON file passed during initialization. """ - # TODO: Add more tests. - # More example settings and/or environments? - # Failure tests; make sure the correct exception is raised - # (e.g., "with pytest.raises(FileNotFoundError, match='part of error message'):") + # TODO: Add more "failure" tests: make sure the correct exception is raised, e.g., + # "with pytest.raises(FileNotFoundError, match='part of error message'):" # TODO: Fill out metrics like T30? It will be done by the backend eventually. def __init__(self, input_json_path: str | Path): @@ -431,8 +429,6 @@ def _modart_method(self) -> None: T60_threshold = result_container['simulationSettings']['T60'] max_slopes_per_band = result_container['simulationSettings']['slopes'] - # TODO: Try to move all possible failure points (like accessing elements of the JSON) at the start. - # Each element of "results" is a simulation request for a different source in the same environment, # which is analyzed only once. All share the same settings and the same list of receivers. num_srcs = len(result_container['results']) @@ -510,12 +506,11 @@ def _modart_method(self) -> None: # Some MoD-ART parameters will be saved in the JSON. # For now, this is just for debugging/testing. - if result_container['MoDART_data'] is None: - result_container['MoDART_data'] = { - 'T60': MoDART_data['T60'].tolist(), - 'Band idx': MoDART_data['Band idx'].tolist(), - 'Eigenvector shape': MoDART_data['V_hat'].shape - } + result_container['MoDART_data'] = { + 'T60': MoDART_data['T60'].tolist(), + 'Band idx': MoDART_data['Band idx'].tolist(), + 'Eigenvector shape': MoDART_data['V_hat'].shape + } for src_idx in range(num_srcs): # Noise-shaping code (may need revising), in case an impulse response was to be returned. diff --git a/modart_method/tests/conftest.py b/modart_method/tests/conftest.py index 1139c91..46e0f21 100644 --- a/modart_method/tests/conftest.py +++ b/modart_method/tests/conftest.py @@ -31,8 +31,11 @@ def default_input_data(): @pytest.fixture def create_temporary_input_file(): - """Fixture to create a temporary input JSON file which can be reused to - write results to.""" + """Fixture to create a temporary input JSON file for testing. + + Can be reused to write results to. + """ + input_tmp = load_default_input_data() geo_file = os.path.join( default_data_path(), "test_room_modart.geo") @@ -50,6 +53,119 @@ def create_temporary_input_file(): return str(tmp_path) +def create_modified_settings_input_file_multi_factory(**settings): + """Factory to create a modified input file with multiple settings changed. + + Parameters + ---------- + **settings : dict + Key-value pairs to update in simulationSettings. + + Returns + ------- + callable + A generator function that yields the path to the temporary file. + + Examples + -------- + >>> factory = create_modified_settings_input_file_multi_factory( + ... slopes=2, + ... pool=1 + ... ) + >>> gen = factory() + >>> json_path = next(gen) + """ + + def _create_modified_settings_input_file(): + input_tmp = load_default_input_data() + + if 'absorption_coefficients' in settings: + modified_coefficients = settings['absorption_coefficients'] + + # Ensure absorption_coefficients exists + if 'absorption_coefficients' not in input_tmp: + input_tmp['absorption_coefficients'] = {} + + # Update all provided coefficients + input_tmp['absorption_coefficients'].update(modified_coefficients) + + if 'simulationSettings' in settings: + modified_settings = settings['simulationSettings'] + + # Ensure simulationSettings exists + if 'simulationSettings' not in input_tmp: + input_tmp['simulationSettings'] = {} + + # Update all provided settings + input_tmp['simulationSettings'].update(modified_settings) + + geo_file = os.path.join( + default_data_path(), "test_room_modart.geo") + + with tempfile.TemporaryDirectory() as tmpdirname: + tmp_path = Path(tmpdirname) / "temp_input.json" + shutil.copy(geo_file, Path(tmpdirname)) + input_tmp['geo_path'] = os.path.join( + tmpdirname, "test_room_modart.geo") + with open(tmp_path, 'w') as f: + json.dump(input_tmp, f) + + yield str(tmp_path) + + return _create_modified_settings_input_file + + +@pytest.fixture +def json_file_factory(): + """Factory fixture to create JSON files with custom simulation settings. + + This fixture returns a callable that creates temporary JSON files with + modified simulation settings. Use this in your test files with + @pytest.mark.parametrize for flexible parametrization. + + Returns + ------- + callable + A function that takes **settings and returns a generator that yields + the JSON file path. + + Examples + -------- + In your test file: + + >>> @pytest.fixture + >>> def config_file(request, json_file_factory): + ... gen = json_file_factory(**request.param) + ... return next(gen) + >>> + >>> @pytest.mark.parametrize('config_file', [ + ... {'slopes': 2, 'pool': 1}, + ... {'slopes': 3, 'pool': 4}, + ... ], indirect=True) + >>> def test_something(config_file): + ... interface = MoDARTMethod(config_file) + ... # test logic... + """ + def _factory(**settings): + return create_modified_settings_input_file_multi_factory(**settings)() + + return _factory + + +@pytest.fixture +def create_modified_input_file(request, json_file_factory): + """Fixture that creates a JSON file based on test parameters.""" + gen = json_file_factory(**request.param) + json_path = next(gen) + # Make sure that the generator is properly closed after the test function + # finalizes to ensure cleanup of temporary files + try: + yield json_path + finally: + gen.close() + + + @pytest.fixture def mock_requests_post(): """Fixture to mock requests.post for CLI tests. diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index dcf9663..7c5cf30 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -7,67 +7,161 @@ from modart_interface import main -def test_modart_method_cli(mock_requests_post, create_temporary_input_file): +""" +DEFAULT VALUES: +{'absorption_coefficients': {'walls': '0.02, 0.03, 0.04, 0.08, 0.15', + 'floor': '0.11, 0.22, 0.42, 0.57, 0.63', + 'ceiling': '0.01, 0.01, 0.01, 0.01, 0.01'}, + 'simulationSettings': {'durat': 1, + 'f_e': 10000, + 'T60': 0.1, + 'slopes': 2, + 'humi': 50, + 'temp': 20, + 'pres': 100, + 'ppsm': 30, + 'rays': 1000, + 'pool': 4}}, +""" +@pytest.mark.parametrize('create_modified_input_file', [ + {'simulationSettings': {'pool': 1}}, + {'simulationSettings': {'pool': 2}}, + {'simulationSettings': {'slopes': 1}}, + {'simulationSettings': {'slopes': 3}}, + {'absorption_coefficients': {'walls': '0, 0, 0, 0, 0', + 'floor': '0, 0, 0, 0, 0', + 'ceiling': '0, 0, 0, 0, 0'}, + 'simulationSettings': {'slopes': 1, 'humi': 0, 'temp': 0}}, + {'absorption_coefficients': {'walls': '0.9999, 0.9999, 0.9999, 0.9999, 0.9999', + 'floor': '0.9999, 0.9999, 0.9999, 0.9999, 0.9999', + 'ceiling': '0.9999, 0.9999, 0.9999, 0.9999, 0.9999'}, + 'simulationSettings': {'slopes': 1}}, + {'absorption_coefficients': {'walls': '1, 1, 1, 1, 1', + 'floor': '1, 1, 1, 1, 1', + 'ceiling': '1, 1, 1, 1, 1'}, + 'simulationSettings': {'slopes': 1}}, +], indirect=True) +def test_modart_method_cli(mock_requests_post, create_modified_input_file): """Test the MoDART method CLI.""" # Set JSON_PATH environment variable and call main() directly - os.environ["JSON_PATH"] = create_temporary_input_file - - main() - - with open(create_temporary_input_file, 'r') as f: - data = json.load(f) - - assert 'results' in data - results = data['results'] - assert len(results) > 0 - for res in results: - assert 'responses' in res - responses = res['responses'] - assert len(responses) > 0 - for resp in responses: - assert 'receiverResults' in resp - rec_res = resp['receiverResults'] - assert rec_res is not None - assert len(rec_res) > 0 - for r in rec_res: - assert len(r) == 4 - assert 'data' in r - assert 't' in r - assert 'frequency' in r - assert 'type' in r - assert r['type'] == 'edc' - assert len(r['data']) == len(r['t']) - - assert np.all(np.isfinite(r['data'])) - - # The function returns a few MoD-ART parameters for debugging/testing. - assert 'MoDART_data' in data - MoDART_data = data['MoDART_data'] - assert len(MoDART_data) > 0 - - # N.B.: THE FOLLOWING TEST PARAMETERS ARE SPECIFIC TO THE EXAMPLE SETTINGS! - - # The test simulation asks for 2 slopes in 5 frequency bands, so there should be 10. - assert len(MoDART_data['T60']) == 10 - assert len(MoDART_data['Band idx']) == 10 - - # The band indices of the detected slopes should range from 0 to 4, - # and there should be exactly two of each. - unique, unique_counts = np.unique(MoDART_data['Band idx'], return_counts=True) - assert unique.tolist() == [0, 1, 2, 3, 4] - assert unique_counts.tolist() == [2, 2, 2, 2, 2] - - # Confirm that the T60 values are reasonably close to a reference run. - assert np.allclose(MoDART_data['T60'], - [0.56, 0.03, 0.32, 0.03, 0.19, 0.03, 0.13, 0.03, 0.1, 0.02], - rtol=0.05, atol=0.05) - - # The test room has 6 patches with full visibility, so there should be 6*(6-1)=30 paths. - # If the second dimension is != 30, that means something went wrong in the mesh decoding. - assert MoDART_data['Eigenvector shape'] == [10, 30] + os.environ["JSON_PATH"] = create_modified_input_file + + with open(create_modified_input_file, 'r') as f: + input_data = json.load(f) + + assert 'simulationSettings' in input_data + settings = input_data['simulationSettings'] + assert len(settings) > 0 + + assert 'absorption_coefficients' in input_data + coeffs = input_data['absorption_coefficients'] + assert len(coeffs) > 0 - # Verify that requests.post was called (save_results was executed) - mock_requests_post.assert_called_once() + if all(coeff == '1, 1, 1, 1, 1' + for coeff in coeffs.values()): + # When all material absorptions are exactly 1, reverberation time is 0. + # The state transition matrix becomes singular, and decomposition is impossible. + with pytest.raises(RuntimeError, match='Failed to run the modal analysis'): + main() + else: + # With other settings, the decomposition should not have any issues. + # N.B.: All remaining tests are in this scope, where "main()" is successful. + main() + + with open(create_modified_input_file, 'r') as f: + output_data = json.load(f) + + #### ASSERTIONS FOR ALL TEST CASES #### + + assert 'results' in output_data + results = output_data['results'] + assert len(results) > 0 + for res in results: + assert 'responses' in res + responses = res['responses'] + assert len(responses) > 0 + for resp in responses: + assert 'receiverResults' in resp + rec_res = resp['receiverResults'] + assert rec_res is not None + assert len(rec_res) > 0 + for r in rec_res: + assert len(r) == 4 + assert 'data' in r + assert 't' in r + assert 'frequency' in r + assert 'type' in r + assert r['type'] == 'edc' + assert len(r['data']) == len(r['t']) + + assert np.all(np.isfinite(r['data'])) + + # The function returns a few MoD-ART parameters for debugging/testing. + assert 'MoDART_data' in output_data + MoDART_data = output_data['MoDART_data'] + assert len(MoDART_data) > 0 + + # The simulation settings ask for a number of slopes in 5 frequency bands, + # so there should be this many in total. + expected_modes = settings['slopes'] * 5 + + assert len(MoDART_data['T60']) == expected_modes + assert len(MoDART_data['Band idx']) == expected_modes + + # The band indices of the detected slopes should range from 0 to 4, + # and there should be a set number of each. + unique, unique_counts = np.unique(MoDART_data['Band idx'], return_counts=True) + assert np.all(unique == np.arange(5)) + assert np.all(unique_counts == settings['slopes']) + + # The test room has 6 patches with full visibility, so there should be 6*(6-1)=30 paths. + # If the second dimension is != 30, that means something went wrong in the mesh decoding. + assert MoDART_data['Eigenvector shape'] == [expected_modes, 30] + + #### ASSERTIONS SPECIFIC TO EACH TEST CASE #### + + if all(coeff == '0, 0, 0, 0, 0' + for coeff in coeffs.values()): + if settings['slopes'] != 1: + raise NotImplementedError('I did not prepare that much reference data.') + + # When all material absorptions are 0, reverberation time is only governed by air absorption. + assert np.allclose(MoDART_data['T60'], + [187.97, 181.98, 174.76, 153.67, 103.97], + rtol=0.05, atol=0.05) + + elif all(coeff == '0.9999, 0.9999, 0.9999, 0.9999, 0.9999' + for coeff in coeffs.values()): + if settings['slopes'] != 1: + raise NotImplementedError('I did not prepare that much reference data.') + + # When all material absorptions are close to 1, reverberation time is miniscule. + assert np.allclose(MoDART_data['T60'], + 5e-3, rtol=1e-3, atol=1e-3) + + else: + # Confirm that the T60 values are reasonably close to a reference run. + if settings['slopes'] == 1: + assert np.allclose(MoDART_data['T60'], + [0.56, 0.32, 0.19, 0.13, 0.1], + rtol=0.05, atol=0.05) + elif settings['slopes'] == 2: + assert np.allclose(MoDART_data['T60'], + [0.56, 0.03, 0.32, 0.03, 0.19, 0.03, 0.13, 0.03, 0.1, 0.02], + rtol=0.05, atol=0.05) + elif settings['slopes'] == 3: + assert np.allclose(MoDART_data['T60'], + [0.56, 0.03, 0.03, + 0.32, 0.03, 0.03, + 0.19, 0.03, 0.03, + 0.13, 0.03, 0.03, + 0.1, 0.02, 0.02], + rtol=0.05, atol=0.05) + else: + raise NotImplementedError('I did not prepare that much reference data.') + + # Verify that requests.post was called (save_results was executed) + mock_requests_post.assert_called_once() def test_modart_method_cli_missing_json_path(mock_requests_post): From 68db22d9f8f964a73ea1946704841d7463725d09 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Wed, 26 Aug 2026 11:49:36 +0200 Subject: [PATCH 26/40] Reverted to RIR return; pytests pass. --- .../modart_interface/modart_interface.py | 35 +++++++++---------- modart_method/tests/test_modart_cli.py | 23 ++++++------ 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index d4feee4..b18c877 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -513,8 +513,7 @@ def _modart_method(self) -> None: } for src_idx in range(num_srcs): - # Noise-shaping code (may need revising), in case an impulse response was to be returned. - """ + # Noise-shaping code. TODO: may need revising. # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). result_container['results'][src_idx]['percentage'] = 95 # Save the updated JSON. @@ -524,6 +523,7 @@ def _modart_method(self) -> None: # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) + # We use a linear interpolation, because any other upsampling algorithm risks introducing negative values. linear_spline = make_interp_spline(echogram_time_axis, MoDART_echograms, k=1, axis=-1) upsampled_echograms = linear_spline(audio_time_axis) @@ -545,24 +545,23 @@ def _modart_method(self) -> None: for rcv_idx in range(num_rcvs): # Note that the first index of "responses" is for the single source position. result_container['results'][src_idx]['responses'][rcv_idx]['receiverResults'] = responses[0, rcv_idx].tolist() - """ - EDCs = schroeder_curves(audio_sample_rate, frequencies, MoDART_echograms) - EDCs = np.clip(EDCs, 1e-20, None) - EDCs = 10 * np.log10(EDCs) + # EDCs = schroeder_curves(audio_sample_rate, frequencies, MoDART_echograms) + # EDCs = np.clip(EDCs, 1e-20, None) + # EDCs = 10 * np.log10(EDCs) - time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) - - for rcv_idx in range(num_rcvs): - for freq_idx, freq in enumerate(frequencies): - result_container['results'][src_idx]['responses'][rcv_idx]['receiverResults'].append( - { - "data": EDCs[src_idx, rcv_idx, freq_idx].tolist(), - "t": time_axis.tolist(), - "frequency": freq, - "type": "edc", - } - ) + # time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) + + # for rcv_idx in range(num_rcvs): + # for freq_idx, freq in enumerate(frequencies): + # result_container['results'][src_idx]['responses'][rcv_idx]['receiverResults'].append( + # { + # "data": EDCs[src_idx, rcv_idx, freq_idx].tolist(), + # "t": time_axis.tolist(), + # "frequency": freq, + # "type": "edc", + # } + # ) result_container['results'][src_idx]['percentage'] = 100 # Save the updated JSON. diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index 7c5cf30..3e5e852 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -85,16 +85,19 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): rec_res = resp['receiverResults'] assert rec_res is not None assert len(rec_res) > 0 - for r in rec_res: - assert len(r) == 4 - assert 'data' in r - assert 't' in r - assert 'frequency' in r - assert 'type' in r - assert r['type'] == 'edc' - assert len(r['data']) == len(r['t']) - - assert np.all(np.isfinite(r['data'])) + + assert np.any(np.abs(rec_res) > 0) + + # for rir in rec_res: + # assert len(r) == 4 + # assert 'data' in r + # assert 't' in r + # assert 'frequency' in r + # assert 'type' in r + # assert r['type'] == 'edc' + # assert len(r['data']) == len(r['t']) + + # assert np.all(np.isfinite(r['data'])) # The function returns a few MoD-ART parameters for debugging/testing. assert 'MoDART_data' in output_data From 491eca525c10a682941caca49b0e9d0c913d8a8b Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Thu, 27 Aug 2026 11:20:45 +0200 Subject: [PATCH 27/40] pytests pass with new error handling. --- example_settings/modart_setting.json | 7 +++ .../modart_interface/modart_interface.py | 59 +++++++++++++------ modart_method/tests/test_modart_cli.py | 29 ++++++--- 3 files changed, 71 insertions(+), 24 deletions(-) diff --git a/example_settings/modart_setting.json b/example_settings/modart_setting.json index 0bbf8d3..e67023f 100644 --- a/example_settings/modart_setting.json +++ b/example_settings/modart_setting.json @@ -3,6 +3,7 @@ "options": [ { "name": "Response duration", + "description": "Duration of the returned impulse response, in seconds.", "id": "durat", "type": "float", "display": "text", @@ -14,6 +15,7 @@ }, { "name": "Echogram sample rate", + "description": "Sample rate used to compute the echogram (energy response envelope). COMPLETELY UNRELATED TO THE AURALIZATION SAMPLE RATE. Higher values will produce more accurate results, but the decomposition will take longer.", "id": "f_e", "type": "float", "display": "text", @@ -25,6 +27,7 @@ }, { "name": "T60 threshold", + "description": "The decomposition (in each band) will stop after locating all energy modes with longer reverberation times than this.", "id": "T60", "type": "float", "display": "text", @@ -36,6 +39,7 @@ }, { "name": "Max slopes", + "description": "The decomposition (in each band) will stop after locating this many energy modes, even if the T60 threshold has not been reached.", "id": "slopes", "type": "int", "display": "text", @@ -79,6 +83,7 @@ }, { "name": "Points per square meter", + "description": "Density of surface sample points used for the numerical area integration.", "id": "ppsm", "type": "float", "display": "text", @@ -89,6 +94,7 @@ }, { "name": "Rays per hemisphere", + "description": "Density of sample directions used for the numerical solid angle integration.", "id": "rays", "type": "int", "display": "text", @@ -99,6 +105,7 @@ }, { "name": "Max parallel processes", + "description": "Cap to the number of processes spawned for the numerical integration.", "id": "pool", "type": "int", "display": "text", diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index b18c877..755ff64 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -1,5 +1,6 @@ """Module implementing a CHORAS interface for MoDART. """ +import sys import json import gmsh import numpy as np @@ -168,7 +169,7 @@ def save_materials_file(json_file_path: str | Path) -> None: raise FileNotFoundError('The specified JSON file could not be found.') # Load the input JSON file. - with open(json_file_path, "r") as json_file: + with open(json_file_path, 'r') as json_file: result_container = json.load(json_file) # The formatted data needs to be saved in the specified folder. @@ -376,7 +377,7 @@ def run_simulation(self) -> None: """Prepare the input data and run the method proper. """ # Load the JSON configuration file. - with open(self.input_json_path, "r") as json_file: + with open(self.input_json_path, 'r') as json_file: result_container = json.load(json_file) # Create a folder for the "temporary" ART and MoD-ART data. @@ -387,23 +388,47 @@ def run_simulation(self) -> None: Path.mkdir(temp_subfolder) # Save the updated JSON (with the added MoDART_data_subfolder field). - with open(self.input_json_path, "w") as json_output: + with open(self.input_json_path, 'w') as json_output: json_output.write(json.dumps(result_container, indent=4)) # Convert the .geo file into the format expected by MoD-ART. try: save_converted_mesh(result_container['geo_path'], temp_subfolder) except Exception as exc: - raise RuntimeError('Failed to reformat the input mesh as required.') from exc + # raise RuntimeError('Failed to reformat the input mesh as required.') from exc + result_container['error'] = {'type': type(exc).__name__, + 'message': str(exc)} + + with open(self.input_json_path, 'w') as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + sys.exit(1) # Save the material information into the .csv file expected by MoD-ART. try: save_materials_file(self.input_json_path) except Exception as exc: - raise RuntimeError('Failed to reformat the material properties as required.') from exc + # raise RuntimeError('Failed to reformat the material properties as required.') from exc + result_container['error'] = {'type': type(exc).__name__, + 'message': str(exc)} + + with open(self.input_json_path, 'w') as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + sys.exit(1) # Run a one-shot MoD-ART simulation. - self._modart_method() + try: + self._modart_method() + except Exception as exc: + # raise RuntimeError('Failed to run the MoD-ART method.') from exc + result_container['error'] = {'type': type(exc).__name__, + 'message': str(exc)} + + with open(self.input_json_path, 'w') as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + sys.exit(1) def _modart_method(self) -> None: """ @@ -414,7 +439,7 @@ def _modart_method(self) -> None: - generation of impulse responses from energy envelopes. """ # Load the JSON file and extract its relevant contents. - with open(self.input_json_path, "r") as json_file: + with open(self.input_json_path, 'r') as json_file: result_container = json.load(json_file) environment_folder = result_container['MoDART_data_subfolder'] audio_sample_rate = result_container['fs_auralization'] @@ -472,7 +497,7 @@ def _modart_method(self) -> None: for src_idx in range(num_srcs): result_container['results'][src_idx]['percentage'] = 40 # Save the updated JSON. - with open(self.input_json_path, "w") as json_output: + with open(self.input_json_path, 'w') as json_output: json_output.write(json.dumps(result_container, indent=4)) # Step 2: Analyze the ART model. @@ -489,7 +514,7 @@ def _modart_method(self) -> None: for src_idx in range(num_srcs): result_container['results'][src_idx]['percentage'] = 80 # Save the updated JSON. - with open(self.input_json_path, "w") as json_output: + with open(self.input_json_path, 'w') as json_output: json_output.write(json.dumps(result_container, indent=4)) # Generate the echograms with MoD-ART. @@ -517,13 +542,13 @@ def _modart_method(self) -> None: # Claim that the response generation constitutes the last 5% of the overall progress (very arbitrary). result_container['results'][src_idx]['percentage'] = 95 # Save the updated JSON. - with open(self.input_json_path, "w") as json_output: + with open(self.input_json_path, 'w') as json_output: json_output.write(json.dumps(result_container, indent=4)) # Prepare the audio-rate time intervals at which we'll evaluate the upsampled echogram. echogram_time_axis = np.arange(0, response_duration, 1 / echogram_sample_rate) audio_time_axis = np.arange(0, response_duration, 1 / audio_sample_rate) - + # We use a linear interpolation, because any other upsampling algorithm risks introducing negative values. linear_spline = make_interp_spline(echogram_time_axis, MoDART_echograms, k=1, axis=-1) upsampled_echograms = linear_spline(audio_time_axis) @@ -556,16 +581,16 @@ def _modart_method(self) -> None: # for freq_idx, freq in enumerate(frequencies): # result_container['results'][src_idx]['responses'][rcv_idx]['receiverResults'].append( # { - # "data": EDCs[src_idx, rcv_idx, freq_idx].tolist(), - # "t": time_axis.tolist(), - # "frequency": freq, - # "type": "edc", + # 'data': EDCs[src_idx, rcv_idx, freq_idx].tolist(), + # 't': time_axis.tolist(), + # 'frequency': freq, + # 'type': 'edc', # } # ) result_container['results'][src_idx]['percentage'] = 100 # Save the updated JSON. - with open(self.input_json_path, "w") as json_output: + with open(self.input_json_path, 'w') as json_output: json_output.write(json.dumps(result_container, indent=4)) - print("MoDART simulation completed successfully!") + print('MoDART simulation completed successfully!') diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index 3e5e852..c453a6a 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -32,9 +32,9 @@ 'floor': '0, 0, 0, 0, 0', 'ceiling': '0, 0, 0, 0, 0'}, 'simulationSettings': {'slopes': 1, 'humi': 0, 'temp': 0}}, - {'absorption_coefficients': {'walls': '0.9999, 0.9999, 0.9999, 0.9999, 0.9999', - 'floor': '0.9999, 0.9999, 0.9999, 0.9999, 0.9999', - 'ceiling': '0.9999, 0.9999, 0.9999, 0.9999, 0.9999'}, + {'absorption_coefficients': {'walls': '0.999, 0.999, 0.999, 0.999, 0.999', + 'floor': '0.999, 0.999, 0.999, 0.999, 0.999', + 'ceiling': '0.999, 0.999, 0.999, 0.999, 0.999'}, 'simulationSettings': {'slopes': 1}}, {'absorption_coefficients': {'walls': '1, 1, 1, 1, 1', 'floor': '1, 1, 1, 1, 1', @@ -61,8 +61,23 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): for coeff in coeffs.values()): # When all material absorptions are exactly 1, reverberation time is 0. # The state transition matrix becomes singular, and decomposition is impossible. - with pytest.raises(RuntimeError, match='Failed to run the modal analysis'): + # with pytest.raises(RuntimeError, match='Failed to run the modal analysis'): + # main() + + # https://stackoverflow.com/a/73478360 + with pytest.raises(SystemExit) as excinfo: main() + assert excinfo.value.code == 1 + + with open(create_modified_input_file, 'r') as f: + output_data = json.load(f) + + assert 'error' in output_data + error = output_data['error'] + assert 'type' in error + assert 'message' in error + assert error['type'] == 'RuntimeError' + assert error['message'] == 'Failed to run the modal analysis (environment pre-processing).' else: # With other settings, the decomposition should not have any issues. # N.B.: All remaining tests are in this scope, where "main()" is successful. @@ -85,7 +100,7 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): rec_res = resp['receiverResults'] assert rec_res is not None assert len(rec_res) > 0 - + assert np.any(np.abs(rec_res) > 0) # for rir in rec_res: @@ -133,11 +148,11 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): [187.97, 181.98, 174.76, 153.67, 103.97], rtol=0.05, atol=0.05) - elif all(coeff == '0.9999, 0.9999, 0.9999, 0.9999, 0.9999' + elif all(coeff == '0.999, 0.999, 0.999, 0.999, 0.999' for coeff in coeffs.values()): if settings['slopes'] != 1: raise NotImplementedError('I did not prepare that much reference data.') - + # When all material absorptions are close to 1, reverberation time is miniscule. assert np.allclose(MoDART_data['T60'], 5e-3, rtol=1e-3, atol=1e-3) From fb14ea06c20534aedaa30a49dd2e74defe9bea00 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Thu, 27 Aug 2026 15:26:17 +0200 Subject: [PATCH 28/40] Successfully ran in docker. --- example_settings/modart_setting.json | 12 ++++++++++++ modart_method/modart_interface/modart_interface.py | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/example_settings/modart_setting.json b/example_settings/modart_setting.json index e67023f..9b98103 100644 --- a/example_settings/modart_setting.json +++ b/example_settings/modart_setting.json @@ -25,6 +25,18 @@ "step": 5e1, "endAdornment": "Hz" }, + { + "name": "Auralization sample rate", + "description": "Sample rate used to produce the auralization RIR.", + "id": "f_s", + "type": "float", + "display": "text", + "min": 5e1, + "max": 5e4, + "default": 44.1e3, + "step": 1e2, + "endAdornment": "Hz" + }, { "name": "T60 threshold", "description": "The decomposition (in each band) will stop after locating all energy modes with longer reverberation times than this.", diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 755ff64..a32891b 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -441,10 +441,11 @@ def _modart_method(self) -> None: # Load the JSON file and extract its relevant contents. with open(self.input_json_path, 'r') as json_file: result_container = json.load(json_file) + environment_folder = result_container['MoDART_data_subfolder'] - audio_sample_rate = result_container['fs_auralization'] response_duration = result_container['simulationSettings']['durat'] echogram_sample_rate = result_container['simulationSettings']['f_e'] + audio_sample_rate = result_container['simulationSettings']['f_s'] multiprocess_pool_size = result_container['simulationSettings']['pool'] humidity = result_container['simulationSettings']['humi'] temperature = result_container['simulationSettings']['temp'] From f88ad61b4ec026f24899a7f9851d39e10b430044 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Thu, 27 Aug 2026 15:34:33 +0200 Subject: [PATCH 29/40] Changed license to GPLv3 --- modart_method/LICENSE | 642 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 621 insertions(+), 21 deletions(-) diff --git a/modart_method/LICENSE b/modart_method/LICENSE index a8e20f6..ed990f8 100644 --- a/modart_method/LICENSE +++ b/modart_method/LICENSE @@ -1,21 +1,621 @@ -The MIT License (MIT) - -Copyright (c) 2025, Matteo Scerbo - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice (including the next -paragraph) shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS \ No newline at end of file From 944d46d4a522a333d5e877277ed7e46498a4737a Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Sun, 30 Aug 2026 16:21:13 +0200 Subject: [PATCH 30/40] Propagate error messages to the database --- modart_method/modart_interface/__cli__.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/modart_method/modart_interface/__cli__.py b/modart_method/modart_interface/__cli__.py index b0dcc6b..721876c 100644 --- a/modart_method/modart_interface/__cli__.py +++ b/modart_method/modart_interface/__cli__.py @@ -1,6 +1,8 @@ """CLI module for MoDART method.""" import os from .modart_interface import MoDARTMethod +import json +import sys def main() -> None: @@ -11,7 +13,20 @@ def main() -> None: print(f"Running MoDART method with JSON_PATH={json_file_path}") modart_method_object = MoDARTMethod(json_file_path) - modart_method_object.run_simulation() + try: + modart_method_object.run_simulation() + except Exception as e: + # Write error to result JSON so backend can read it + with open(json_file_path) as f: + data = json.load(f) + data['error'] = {'type': type(e).__name__, 'message': str(e)} + with open(json_file_path, 'w') as f: + json.dump(data, f, indent=4) + + # Ensure the container exits with exit code 1 to indicate failure + # The status code is used by the backend to determine if the simulation + # was successful or not. + sys.exit(1) # Save the results to a separate file modart_method_object.save_results() From f1391607db587246915236e38d88c4f3975522e7 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Sun, 30 Aug 2026 18:56:49 +0200 Subject: [PATCH 31/40] Update sampling rate setting --- example_settings/modart_setting.json | 2 +- modart_method/modart_interface/modart_interface.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example_settings/modart_setting.json b/example_settings/modart_setting.json index 9b98103..a4b95f3 100644 --- a/example_settings/modart_setting.json +++ b/example_settings/modart_setting.json @@ -28,7 +28,7 @@ { "name": "Auralization sample rate", "description": "Sample rate used to produce the auralization RIR.", - "id": "f_s", + "id": "sampling_rate", "type": "float", "display": "text", "min": 5e1, diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index a32891b..b24a948 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -445,7 +445,7 @@ def _modart_method(self) -> None: environment_folder = result_container['MoDART_data_subfolder'] response_duration = result_container['simulationSettings']['durat'] echogram_sample_rate = result_container['simulationSettings']['f_e'] - audio_sample_rate = result_container['simulationSettings']['f_s'] + audio_sample_rate = result_container['simulationSettings']['sampling_rate'] multiprocess_pool_size = result_container['simulationSettings']['pool'] humidity = result_container['simulationSettings']['humi'] temperature = result_container['simulationSettings']['temp'] From e4de74e788d5ac498b3d3002d0cef4c5db25cd78 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Sun, 30 Aug 2026 19:13:08 +0200 Subject: [PATCH 32/40] Add CI config for Mod-ART --- .github/workflows/mod-art.yml | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/mod-art.yml diff --git a/.github/workflows/mod-art.yml b/.github/workflows/mod-art.yml new file mode 100644 index 0000000..9acc1fa --- /dev/null +++ b/.github/workflows/mod-art.yml @@ -0,0 +1,53 @@ +name: Mod-ART Method CI + +on: + push: + branches: + - main + - develop + pull_request: + types: [opened, synchronize, reopened] + +jobs: + uv-test: + name: Pytest for the mod-art runner + 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: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libglu1-mesa libgl1 mesa-utils + + - name: Run test for modart method + run: | + cd modart_method + uv run --extra tests pytest tests/ + + build-docker-image: + name: Build Docker Image + runs-on: ubuntu-latest + continue-on-error: true + + steps: + - uses: actions/checkout@v6 + + - name: Build Docker Image + run: | + docker build -f modart_method/Dockerfile . --platform=linux/amd64 -t modart-method-test:latest From 81843542f7ff210e48abd78323a7176defa21cfa Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 31 Aug 2026 11:46:51 +0200 Subject: [PATCH 33/40] Improved noise-shaping flatness at frequency band crossings. --- .../modart_interface/modart_interface.py | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index a32891b..6bd3c3b 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -221,7 +221,8 @@ def save_materials_file(json_file_path: str | Path) -> None: def noise_shaping(fs: int | float, band_centers: np.ndarray, - envelopes: np.ndarray) -> np.ndarray: + envelopes: np.ndarray, + order: int = 5) -> np.ndarray: """ Generate impulse responses based on band-wise signal envelopes, through the amplitude modulation of stochastic signals. @@ -239,6 +240,9 @@ def noise_shaping(fs: int | float, - R is the number of receivers - B is the number of frequency bands (must match length of "band_centers") - T is the duration in samples. + order : int + Order passed to the "butter" bandpass filtering function. + Flat responses are achieved with odd numbers greater than three. Returns ------- @@ -268,7 +272,7 @@ def noise_shaping(fs: int | float, # White noise # noise_signal = rng.normal(size=duration_in_samples) # Poisson process - noise_signal = rng.poisson(lam=0.5, size=duration_in_samples).astype(float) + noise_signal = rng.poisson(lam=0.05, size=duration_in_samples).astype(float) # Ensure the noise signal has unit energy per second, matching the # convention used to generate the echograms. @@ -294,9 +298,9 @@ def noise_shaping(fs: int | float, for b in range(num_bands): # Prepare the suitable band-pass filter... - sos = butter(6, (band_centers[b] / band_bound, - band_centers[b] * band_bound), - btype='bandpass', output='sos', fs=fs) + sos = butter(order, (band_centers[b] / band_bound, + band_centers[b] * band_bound), + btype='bandpass', output='sos', fs=fs) # ...and apply it to the stochastic signal. filtered_noise_signals[b] = sosfilt(sos, noise_signal) @@ -595,3 +599,31 @@ def _modart_method(self) -> None: json_output.write(json.dumps(result_container, indent=4)) print('MoDART simulation completed successfully!') + + +def test_noise_shaping_flatness(): + from matplotlib import pyplot as plt + + for order in range(1, 12, 2): + resp = np.zeros(4097) + for i in range(100): + responses = noise_shaping(44100, + np.array([2.5e2, 5e2, 1e3, 2e3, 4e3, 8e3]), + np.ones((1, 1, 6, 88200)), + order) + response = responses.squeeze() + + transf = np.fft.rfft(response, n=2**13) + resp += np.abs(transf) + resp /= 100 + + plt.plot(resp, label=order) + + plt.xscale('log') + plt.yscale('log') + plt.legend() + plt.show() + + +# if __name__ == "__main__": +# test_noise_shaping_flatness() From 054fecb367eb2ab263d08d8917f278082e286833 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 31 Aug 2026 18:46:49 +0200 Subject: [PATCH 34/40] Add missing sampling rate --- modart_method/tests/test_input_modart.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modart_method/tests/test_input_modart.json b/modart_method/tests/test_input_modart.json index 639cbc9..3c78bcd 100644 --- a/modart_method/tests/test_input_modart.json +++ b/modart_method/tests/test_input_modart.json @@ -56,7 +56,8 @@ "pres": 100, "ppsm": 30, "rays": 1000, - "pool": 4 + "pool": 4, + "sampling_rate": 44100 }, "settingsPreset": "Default" } From 32e5bed06d503ab98c431a9995a48d51a32b5ed9 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 31 Aug 2026 18:54:40 +0200 Subject: [PATCH 35/40] Let the cli handle error message propagation --- .../modart_interface/modart_interface.py | 27 +++---------------- modart_method/tests/test_modart_cli.py | 2 +- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index cb4328a..6e33cd3 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -399,40 +399,19 @@ def run_simulation(self) -> None: try: save_converted_mesh(result_container['geo_path'], temp_subfolder) except Exception as exc: - # raise RuntimeError('Failed to reformat the input mesh as required.') from exc - result_container['error'] = {'type': type(exc).__name__, - 'message': str(exc)} - - with open(self.input_json_path, 'w') as json_output: - json_output.write(json.dumps(result_container, indent=4)) - - sys.exit(1) + raise RuntimeError('Failed to reformat the input mesh as required.') from exc # Save the material information into the .csv file expected by MoD-ART. try: save_materials_file(self.input_json_path) except Exception as exc: - # raise RuntimeError('Failed to reformat the material properties as required.') from exc - result_container['error'] = {'type': type(exc).__name__, - 'message': str(exc)} - - with open(self.input_json_path, 'w') as json_output: - json_output.write(json.dumps(result_container, indent=4)) - - sys.exit(1) + raise RuntimeError('Failed to reformat the material properties as required.') from exc # Run a one-shot MoD-ART simulation. try: self._modart_method() except Exception as exc: - # raise RuntimeError('Failed to run the MoD-ART method.') from exc - result_container['error'] = {'type': type(exc).__name__, - 'message': str(exc)} - - with open(self.input_json_path, 'w') as json_output: - json_output.write(json.dumps(result_container, indent=4)) - - sys.exit(1) + raise RuntimeError('Failed to run the MoD-ART method.') from exc def _modart_method(self) -> None: """ diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index c453a6a..f24d0f5 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -77,7 +77,7 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): assert 'type' in error assert 'message' in error assert error['type'] == 'RuntimeError' - assert error['message'] == 'Failed to run the modal analysis (environment pre-processing).' + assert 'Failed to run' in error['message'] else: # With other settings, the decomposition should not have any issues. # N.B.: All remaining tests are in this scope, where "main()" is successful. From 1196063f7fee6af3fe72c849dd2fdd3cecd2ec59 Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Mon, 31 Aug 2026 19:04:23 +0200 Subject: [PATCH 36/40] Contrain numpy to avoid deprecations --- modart_method/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index 0398fd4..9611cc1 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "raves>=0.1.2", "gmsh==4.13.1", "requests", + "numpy<2.5.0", ] [project.optional-dependencies] From 425f8bf3f68bfbe22bc397b50147f9be9cbb5187 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Tue, 1 Sep 2026 13:56:20 +0200 Subject: [PATCH 37/40] Bumped "raves" version, updated tests. --- modart_method/pyproject.toml | 2 +- modart_method/tests/test_modart_cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index 9611cc1..8966701 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ - "raves>=0.1.2", + "raves>=0.2.1", "gmsh==4.13.1", "requests", "numpy<2.5.0", diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index f24d0f5..c7bbfdb 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -145,7 +145,7 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): # When all material absorptions are 0, reverberation time is only governed by air absorption. assert np.allclose(MoDART_data['T60'], - [187.97, 181.98, 174.76, 153.67, 103.97], + [194.175, 184.681, 179.758, 169.130, 137.950], rtol=0.05, atol=0.05) elif all(coeff == '0.999, 0.999, 0.999, 0.999, 0.999' From ed3a303b5673e386bf5a2c4bee8efbd062694b3b Mon Sep 17 00:00:00 2001 From: Marco Berzborn Date: Tue, 1 Sep 2026 15:49:09 +0200 Subject: [PATCH 38/40] FIX: Update license identifier --- modart_method/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index 8966701..cf85c92 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -15,7 +15,7 @@ keywords = [ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", From 0e48ebace34aabe8672f9727c839d4379c02fc02 Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Thu, 3 Sep 2026 10:14:42 +0200 Subject: [PATCH 39/40] Caught a quiet failure. --- modart_method/modart_interface/modart_interface.py | 3 ++- modart_method/pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modart_method/modart_interface/modart_interface.py b/modart_method/modart_interface/modart_interface.py index 6e33cd3..d4be415 100644 --- a/modart_method/modart_interface/modart_interface.py +++ b/modart_method/modart_interface/modart_interface.py @@ -490,7 +490,8 @@ def _modart_method(self) -> None: echogram_sample_rate=echogram_sample_rate, T60_threshold=T60_threshold, max_slopes_per_band=max_slopes_per_band, - skip_T60_plots=True) + skip_T60_plots=True, + raise_on_failure=True) except Exception as exc: raise RuntimeError('Failed to run the modal analysis (environment pre-processing).') from exc diff --git a/modart_method/pyproject.toml b/modart_method/pyproject.toml index cf85c92..f538a88 100644 --- a/modart_method/pyproject.toml +++ b/modart_method/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ - "raves>=0.2.1", + "raves>=0.2.2", "gmsh==4.13.1", "requests", "numpy<2.5.0", From 87708f787e01044d8014aa920a8777f9fbce419d Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Fri, 4 Sep 2026 11:42:52 +0200 Subject: [PATCH 40/40] Removed brittle test. --- modart_method/tests/test_modart_cli.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/modart_method/tests/test_modart_cli.py b/modart_method/tests/test_modart_cli.py index c7bbfdb..871064b 100644 --- a/modart_method/tests/test_modart_cli.py +++ b/modart_method/tests/test_modart_cli.py @@ -32,10 +32,6 @@ 'floor': '0, 0, 0, 0, 0', 'ceiling': '0, 0, 0, 0, 0'}, 'simulationSettings': {'slopes': 1, 'humi': 0, 'temp': 0}}, - {'absorption_coefficients': {'walls': '0.999, 0.999, 0.999, 0.999, 0.999', - 'floor': '0.999, 0.999, 0.999, 0.999, 0.999', - 'ceiling': '0.999, 0.999, 0.999, 0.999, 0.999'}, - 'simulationSettings': {'slopes': 1}}, {'absorption_coefficients': {'walls': '1, 1, 1, 1, 1', 'floor': '1, 1, 1, 1, 1', 'ceiling': '1, 1, 1, 1, 1'}, @@ -147,15 +143,6 @@ def test_modart_method_cli(mock_requests_post, create_modified_input_file): assert np.allclose(MoDART_data['T60'], [194.175, 184.681, 179.758, 169.130, 137.950], rtol=0.05, atol=0.05) - - elif all(coeff == '0.999, 0.999, 0.999, 0.999, 0.999' - for coeff in coeffs.values()): - if settings['slopes'] != 1: - raise NotImplementedError('I did not prepare that much reference data.') - - # When all material absorptions are close to 1, reverberation time is miniscule. - assert np.allclose(MoDART_data['T60'], - 5e-3, rtol=1e-3, atol=1e-3) else: # Confirm that the T60 values are reasonably close to a reference run.