From 3278cd0e4d9fa4b25867a3d83c3788caa09641db Mon Sep 17 00:00:00 2001 From: Matteo-Scerbo Date: Mon, 11 May 2026 13:52:31 +0200 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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):