From 817e629de95aae18f2dcc8b8d063fc2f49ae300a Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 11 May 2026 16:21:19 +0200 Subject: [PATCH 01/33] first draft --- sparrowpy_method/Dockerfile | 22 + sparrowpy_method/LICENSE | 21 + sparrowpy_method/pyproject.toml | 75 + .../sparrowpy_interface/__cli__.py | 19 + .../sparrowpy_interface/__init__.py | 8 + .../sparrowpy_interface/__main__.py | 5 + .../sparrowpy_interface/definition.py | 92 + .../sparrowpy_interface.py | 268 +++ sparrowpy_method/tests/conftest.py | 68 + sparrowpy_method/tests/test_definition.py | 37 + sparrowpy_method/tests/test_fixtures.py | 23 + .../tests/test_input_sparrowpy.json | 55 + .../tests/test_room_sparrowpy.geo | 51 + .../tests/test_room_sparrowpy.msh | 2116 +++++++++++++++++ sparrowpy_method/tests/test_sparrowpy_cli.py | 43 + .../tests/test_sparrowpy_interface_class.py | 51 + 16 files changed, 2954 insertions(+) create mode 100644 sparrowpy_method/Dockerfile create mode 100644 sparrowpy_method/LICENSE create mode 100644 sparrowpy_method/pyproject.toml create mode 100644 sparrowpy_method/sparrowpy_interface/__cli__.py create mode 100644 sparrowpy_method/sparrowpy_interface/__init__.py create mode 100644 sparrowpy_method/sparrowpy_interface/__main__.py create mode 100644 sparrowpy_method/sparrowpy_interface/definition.py create mode 100644 sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py create mode 100644 sparrowpy_method/tests/conftest.py create mode 100644 sparrowpy_method/tests/test_definition.py create mode 100644 sparrowpy_method/tests/test_fixtures.py create mode 100644 sparrowpy_method/tests/test_input_sparrowpy.json create mode 100644 sparrowpy_method/tests/test_room_sparrowpy.geo create mode 100644 sparrowpy_method/tests/test_room_sparrowpy.msh create mode 100644 sparrowpy_method/tests/test_sparrowpy_cli.py create mode 100644 sparrowpy_method/tests/test_sparrowpy_interface_class.py diff --git a/sparrowpy_method/Dockerfile b/sparrowpy_method/Dockerfile new file mode 100644 index 0000000..5660806 --- /dev/null +++ b/sparrowpy_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 sparrowpy_method /app/sparrowpy_method + +# Install the method package +RUN pip install --no-cache-dir /app/sparrowpy_method + +WORKDIR /app/sparrowpy_method + +# Default command to run the containerized sparrowpy method +CMD ["python", "-m", "sparrowpy_interface"] diff --git a/sparrowpy_method/LICENSE b/sparrowpy_method/LICENSE new file mode 100644 index 0000000..3e7d213 --- /dev/null +++ b/sparrowpy_method/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026, The sparrowpy developers + +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/sparrowpy_method/pyproject.toml b/sparrowpy_method/pyproject.toml new file mode 100644 index 0000000..a2cfe54 --- /dev/null +++ b/sparrowpy_method/pyproject.toml @@ -0,0 +1,75 @@ +[project] +name = "sparrowpy_interface" +version = "0.1.0" +description = "Sound Propagation with Acoustic Radiosity for Realistic Outdoor Worlds" +requires-python = ">=3.11,<3.15" +authors = [ + { name = "Anne Heimes", email = "ahe@akustik.rwth-aachen.de" }, +] +keywords = [ + "acoustic simulation", + "geometrical acoustics", + "acoustic radiance transfer", + "brdf", + "scattering", +] + +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>=1.23.0", + "sparrowpy>=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 = ["sparrowpy_interface[deploy,tests,docs]"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["sparrowpy_interface"] + +[project.scripts] +sparrowpy_interface = "sparrowpy_interface:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/sparrowpy_method/sparrowpy_interface/__cli__.py b/sparrowpy_method/sparrowpy_interface/__cli__.py new file mode 100644 index 0000000..29a0063 --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/__cli__.py @@ -0,0 +1,19 @@ +"""CLI module for sparrowpy method.""" +import os +from .sparrowpy_interface import sparrowpyMethod + + +def main() -> None: + """Run the sparrowpy 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 sparrowpy method with JSON_PATH={json_file_path}") + sparrowpy_method_object = sparrowpyMethod(json_file_path) + sparrowpy_method_object.run_simulation() + + # Save the results to a separate file + sparrowpy_method_object.save_results() + + print("sparrowpy container finished.") diff --git a/sparrowpy_method/sparrowpy_interface/__init__.py b/sparrowpy_method/sparrowpy_interface/__init__.py new file mode 100644 index 0000000..f6f01d9 --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/__init__.py @@ -0,0 +1,8 @@ +"""sparrowpyMethod package.""" +from .__main__ import main +from .sparrowpy_interface import sparrowpyMethod + +__all__ = [ + "main", + "sparrowpyMethod" +] diff --git a/sparrowpy_method/sparrowpy_interface/__main__.py b/sparrowpy_method/sparrowpy_interface/__main__.py new file mode 100644 index 0000000..c6c687c --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/__main__.py @@ -0,0 +1,5 @@ +"""Main module for sparrowpy method.""" +from .__cli__ import main + +if __name__ == "__main__": + main() diff --git a/sparrowpy_method/sparrowpy_interface/definition.py b/sparrowpy_method/sparrowpy_interface/definition.py new file mode 100644 index 0000000..ebd5739 --- /dev/null +++ b/sparrowpy_method/sparrowpy_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 execution 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/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py new file mode 100644 index 0000000..10720a8 --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -0,0 +1,268 @@ +"""Module implementing a CHORAS interface for sparrowpy. +""" +import json +from pathlib import Path + +from .definition import SimulationMethod +import sparrowpy +import gmsh +import pyfar as pf +import numpy as np +import trimesh + + + + +class sparrowpyMethod(SimulationMethod): + """Interface class to run the sparrowpy method. + + The class implements method to run the calculations for the + sparrowpy 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 sparrowpy 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._sparrowpy_method(self.input_json_path) + + def _sparrowpy_method(self, json_file_path: str | Path) -> None: + """ + Run sparrowpy 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) + + # extract simulation settings + + frequencies = result_container['results'][0]['frequencies'] + n_bands = len(frequencies) + simulation_settings = result_container["simulationSettings"] + etc_time_resolution_s = simulation_settings['etc_time_resolution_s'] + speed_of_sound = simulation_settings['speed_of_sound'] + etc_duration_s = simulation_settings['etc_duration_s'] + max_reflection_order = simulation_settings['max_reflection_order'] + + # Read source and receiver positions + source_coords = pf.Coordinates( + result_container["results"][0]["sourceX"], + result_container["results"][0]["sourceY"], + result_container["results"][0]["sourceZ"], + ) + receiver_coords = pf.Coordinates( + result_container["results"][0]["responses"][0]["x"], + result_container["results"][0]["responses"][0]["y"], + result_container["results"][0]["responses"][0]["z"], + ) + + # read walls and triangular patches + ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering, + ) = _import_room_geometry(json_file_path) + + radiosity = sparrowpy.DirectionalRadiosityFast( + walls_points, + walls_normal, + walls_up_vector, + patches_points, + n_patches, + patch_to_wall_ids, + ) + + # apply materials + incoming = pf.Coordinates(0, 0, 1, weights=1) + outgoing = pf.Coordinates(0, 0, 1, weights=1) + for ii, jj in enumerate(material_to_walls): + brdf = sparrowpy.brdf.create_from_scattering( + incoming, outgoing, + pf.FrequencyData(scattering[ii], frequencies), + pf.FrequencyData(alphas[ii], frequencies), + ) + radiosity.set_wall_brdf(jj, brdf, incoming, outgoing) + + # run simulation + radiosity.bake_geometry() + + radiosity.init_source_energy(source_coords) + + radiosity.calculate_energy_exchange( + speed_of_sound=speed_of_sound, + etc_time_resolution=etc_time_resolution_s, + etc_duration=etc_duration_s, + max_reflection_order=max_reflection_order) + + + etc_radiosity = radiosity.collect_energy_receiver_mono( + receivers=receiver_coords) + + # Write results back to JSON + for i_frequency in range(n_bands): + result_container["results"][0]["responses"][0]["receiverResults"].append( + { + "data": etc_radiosity.time[i_frequency].tolist(), + "t": etc_radiosity.times, + "frequency": frequencies[i_frequency], + "type": "edc", + } + ) + result_container["results"][0]["percentage"] = 100 + + # Save the updated JSON + with open(json_file_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + + print("sparrowpy simulation completed successfully!") + + +def _import_room_geometry(json_file_path): + """Import room geometry and absorption coefficients. + + The geometry is read from a .geo file specified in the JSON input file. + The absorption coefficients are directly read from the JSON file. + + Parameters + ---------- + json_file_path : str + Path to the JSON file containing room geometry and absorption + coefficients. + + + Raises + ------ + ValueError + If absorption coefficients for any surface are not found in the + input JSON file. + """ + + with open(json_file_path, 'r') as f: + import json + input_data = json.load(f) + + frequencies = input_data['results'][0]['frequencies'] + n_bands = len(frequencies) + + # initialize gmsh and load the geometry file + gmsh.initialize() + geometry_file = input_data['geo_path'] + gmsh.open(geometry_file) + + # generate 2d surface mesh + dim = 2 # 2D surfaces + gmsh.model.mesh.generate(dim) + + # get all named surfaces in the geometry + surface_group_tags = gmsh.model.getPhysicalGroups(dim=dim) + surface_group_names = [ + gmsh.model.getPhysicalName(dim, tag) + for (dim, tag) in surface_group_tags + ] + + # get all nodes of the surface mesh + node_tags_all, coords_all, _ = gmsh.model.mesh.getNodes() + coords = coords_all.reshape((len(node_tags_all), 3)) + + # get the material names from absorption coefficient input + absorption_names = list(input_data['absorption_coefficients'].keys()) + + # check if absorption coefficient data are available for all surfaces + for material_name in surface_group_names: + if material_name not in absorption_names: + raise ValueError( + "Absorption coefficients for surface " + f"'{material_name}' not found in input JSON file.") + + # create materials + alphas = [] + scatterings = [] + material_to_walls = [] + for material_name in absorption_names: + alphas.append(np.array(input_data['absorption_coefficients'][material_name])) + scatterings.append(np.ones_like(frequencies)) + + # materials + indies_material = [] + for s_name in surface_group_names: + if material_name == s_name: + indies_material.append(s_name) + material_to_walls.append(indies_material) + + + # get the element type for surface mesh + element_type = gmsh.model.mesh.getElementType("Triangle", 1, True) + + + alphas = [] + walls_points = [] + walls_normal = [] + walls_up_vector = [] + patches_points = [] + n_patches = 0 + patch_to_wall_ids = [] + material_to_walls = [] + for i, surface_name in enumerate(surface_group_names): + dim_tags = gmsh.model.getEntitiesForPhysicalName(surface_name) + dim, tag = dim_tags[0] + + face_nodes = gmsh.model.mesh.getElementFaceNodes( + element_type, 3, tag=tag, ) + faces = np.reshape(face_nodes, (len(face_nodes) // 3, 3)) + + # extract wall information + mesh = trimesh.Trimesh(coords, faces-1) + wall_points = np.unique(mesh.bounding_box.vertices, axis=0, ) + wall_idx = [] + for p in wall_points: + wall_idx.append(np.argmin(np.sum(np.abs((mesh.vertices-p)), axis=1))) + wall_points = np.unique(mesh.vertices[wall_idx], axis=0, ) + wall_normal = np.median(mesh.face_normals, axis=0) + if np.abs(wall_normal[2]) > 1e-2: + wall_up_vector = [1, 0, 0] + else: + wall_up_vector = [0, 0, 1] + + walls_points.append(wall_points) + walls_normal.append(wall_normal) + walls_up_vector.append(wall_up_vector) + + # write patches + n_patches_wall = faces.shape[0] + for jj in range(n_patches_wall): + patch_to_wall_ids.append(i) + n_patches += n_patches_wall + patches_points.append(coords[faces-1, :]) + + alpha = np.array(input_data['absorption_coefficients'][surface_name].split(', '), dtype=float) + alphas.append(alpha) + + # finalizing gmsh + gmsh.finalize() + + # save wall information + walls_points = np.array(walls_points) + walls_normal = np.array(walls_normal) + walls_up_vector = np.array(walls_up_vector) + patches_points = np.concatenate(patches_points) + + scattering = np.zeros_like(alphas) + + return ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering) diff --git a/sparrowpy_method/tests/conftest.py b/sparrowpy_method/tests/conftest.py new file mode 100644 index 0000000..1b85a68 --- /dev/null +++ b/sparrowpy_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_sparrowpy.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_sparrowpy.geo") + msh_file = os.path.join( + default_data_path(), "test_room_sparrowpy.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_sparrowpy.geo") + input_tmp['msh_path'] = os.path.join( + tmpdirname, "test_room_sparrowpy.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("sparrowpy_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/sparrowpy_method/tests/test_definition.py b/sparrowpy_method/tests/test_definition.py new file mode 100644 index 0000000..59f388a --- /dev/null +++ b/sparrowpy_method/tests/test_definition.py @@ -0,0 +1,37 @@ +"""Test the SimulationMethod base class for sparrowpy method.""" +import pytest +from unittest.mock import patch +from pathlib import Path + +from sparrowpy_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/sparrowpy_method/tests/test_fixtures.py b/sparrowpy_method/tests/test_fixtures.py new file mode 100644 index 0000000..0eb2fde --- /dev/null +++ b/sparrowpy_method/tests/test_fixtures.py @@ -0,0 +1,23 @@ +"""Test fixtures for sparrowpy 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/sparrowpy_method/tests/test_input_sparrowpy.json b/sparrowpy_method/tests/test_input_sparrowpy.json new file mode 100644 index 0000000..27b04a1 --- /dev/null +++ b/sparrowpy_method/tests/test_input_sparrowpy.json @@ -0,0 +1,55 @@ +{ + "geo_path": "test_room_sparrowpy.geo", + "msh_path": "test_room_sparrowpy.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": { + "speed_of_sound": 343.2, + "etc_time_resolution_s": 0.05, + "etc_duration_s": 0.5, + "max_reflection_order": 2 + }, + "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/sparrowpy_method/tests/test_room_sparrowpy.geo b/sparrowpy_method/tests/test_room_sparrowpy.geo new file mode 100644 index 0000000..db900ed --- /dev/null +++ b/sparrowpy_method/tests/test_room_sparrowpy.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/sparrowpy_method/tests/test_room_sparrowpy.msh b/sparrowpy_method/tests/test_room_sparrowpy.msh new file mode 100644 index 0000000..bbcdc1f --- /dev/null +++ b/sparrowpy_method/tests/test_room_sparrowpy.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 diff --git a/sparrowpy_method/tests/test_sparrowpy_cli.py b/sparrowpy_method/tests/test_sparrowpy_cli.py new file mode 100644 index 0000000..1273447 --- /dev/null +++ b/sparrowpy_method/tests/test_sparrowpy_cli.py @@ -0,0 +1,43 @@ +"""Test the sparrowpy method CLI.""" +import os +import json +import pytest + +from sparrowpy_interface import main, sparrowpyMethod + + +def test_sparrowpy_method_cli(mock_requests_post, create_temporary_input_file): + """Test the sparrowpy 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) + + + sparrowpy_method_object = sparrowpyMethod(create_temporary_input_file) + sparrowpy_method_object.run_simulation() + + # Save the results to a separate file + sparrowpy_method_object.save_results() + + # 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_sparrowpy_method_cli_missing_json_path(mock_requests_post): + """Test the sparrowpy 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/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py new file mode 100644 index 0000000..aaf7cc5 --- /dev/null +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -0,0 +1,51 @@ + +from sparrowpy_interface import sparrowpyMethod +import json +import sparrowpy +import numpy.testing as npt +import numpy as np + +from sparrowpy_interface.sparrowpy_interface import _import_room_geometry + + +def test_simple_method(create_temporary_input_file): + + sparrowpy_method_object = sparrowpyMethod(create_temporary_input_file) + sparrowpy_method_object.run_simulation() + + 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 len(results) > 0 + + +def test_import_room_geometry(create_temporary_input_file): + ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering, + ) = _import_room_geometry( + create_temporary_input_file) + + # create radiosity object + radiosity = sparrowpy.DirectionalRadiosityFast( + walls_points, + walls_normal, + walls_up_vector, + patches_points, + n_patches, + patch_to_wall_ids, + ) + + # check geometry stuff + radiosity.check() + + # check material stuff + npt.assert_equal(material_to_walls, np.arange(6)) + npt.assert_equal(np.array(alphas).shape, (6, 6)) + npt.assert_equal(np.array(scattering).shape, (6, 6)) + npt.assert_array_less(np.array(alphas), 1) + npt.assert_array_less(np.array(scattering), 1) From 00694c51cabed78b77b15cfc0a3a5430f6162660 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 11 May 2026 16:53:20 +0200 Subject: [PATCH 02/33] allow multiple receivers --- sparrowpy_method/pyproject.toml | 2 + .../sparrowpy_interface.py | 48 ++++++++++++------- .../tests/test_sparrowpy_interface_class.py | 2 +- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/sparrowpy_method/pyproject.toml b/sparrowpy_method/pyproject.toml index a2cfe54..9b6e9d2 100644 --- a/sparrowpy_method/pyproject.toml +++ b/sparrowpy_method/pyproject.toml @@ -30,6 +30,8 @@ dependencies = [ "sparrowpy>=1", "requests", "gmsh", + "trimesh", + "pyfar", ] [project.optional-dependencies] diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 10720a8..3f2051a 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -64,11 +64,15 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: result_container["results"][0]["sourceY"], result_container["results"][0]["sourceZ"], ) - receiver_coords = pf.Coordinates( - result_container["results"][0]["responses"][0]["x"], - result_container["results"][0]["responses"][0]["y"], - result_container["results"][0]["responses"][0]["z"], - ) + n_receivers = len(result_container["results"][0]["responses"]) + receiver_coords = pf.Coordinates(np.zeros((n_receivers)), 0, 0) + cart = receiver_coords.cartesian + for i_rec in range(n_receivers): + rec = result_container["results"][0]["responses"][i_rec] + cart[i_rec, 0] = rec["x"] + cart[i_rec, 1] = rec["y"] + cart[i_rec, 2] = rec["z"] + receiver_coords.cartesian = cart # read walls and triangular patches ( @@ -113,15 +117,16 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: receivers=receiver_coords) # Write results back to JSON - for i_frequency in range(n_bands): - result_container["results"][0]["responses"][0]["receiverResults"].append( - { - "data": etc_radiosity.time[i_frequency].tolist(), - "t": etc_radiosity.times, - "frequency": frequencies[i_frequency], - "type": "edc", - } - ) + for i_rec in range(n_receivers): + for i_frequency in range(n_bands): + result_container["results"][0]["responses"][i_rec]["receiverResults"].append( + { + "data": 10*np.log10(etc_radiosity.time[i_rec, i_frequency]).tolist(), + "t": etc_radiosity.times.tolist(), + "frequency": frequencies[i_frequency], + "type": "edc", + } + ) result_container["results"][0]["percentage"] = 100 # Save the updated JSON @@ -163,6 +168,16 @@ def _import_room_geometry(json_file_path): geometry_file = input_data['geo_path'] gmsh.open(geometry_file) + # If an lc is given in the geo file, we want to compensate for this + lc_value = 1 # set to 1 by default + # for line in geo_content: + # if "lc =" in line: + # lc_value = float(line.split('=')[1].strip().strip(';')) + # print("Extracted value:", lc_value) + # break + + gmsh.option.setNumber('Mesh.MeshSizeFactor', 5/lc_value) + # generate 2d surface mesh dim = 2 # 2D surfaces gmsh.model.mesh.generate(dim) @@ -198,9 +213,9 @@ def _import_room_geometry(json_file_path): # materials indies_material = [] - for s_name in surface_group_names: + for ii, s_name in enumerate(surface_group_names): if material_name == s_name: - indies_material.append(s_name) + indies_material.append(ii) material_to_walls.append(indies_material) @@ -215,7 +230,6 @@ def _import_room_geometry(json_file_path): patches_points = [] n_patches = 0 patch_to_wall_ids = [] - material_to_walls = [] for i, surface_name in enumerate(surface_group_names): dim_tags = gmsh.model.getEntitiesForPhysicalName(surface_name) dim, tag = dim_tags[0] diff --git a/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py index aaf7cc5..048a3d5 100644 --- a/sparrowpy_method/tests/test_sparrowpy_interface_class.py +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -44,7 +44,7 @@ def test_import_room_geometry(create_temporary_input_file): radiosity.check() # check material stuff - npt.assert_equal(material_to_walls, np.arange(6)) + npt.assert_equal(np.squeeze(material_to_walls), np.arange(6)) npt.assert_equal(np.array(alphas).shape, (6, 6)) npt.assert_equal(np.array(scattering).shape, (6, 6)) npt.assert_array_less(np.array(alphas), 1) From 7b55d7fb9496edd8b18935fe809057969732e5f6 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 11 May 2026 16:58:02 +0200 Subject: [PATCH 03/33] add patch length to settings --- .../sparrowpy_interface/sparrowpy_interface.py | 8 +++++--- sparrowpy_method/tests/test_input_sparrowpy.json | 3 ++- sparrowpy_method/tests/test_sparrowpy_cli.py | 7 ------- sparrowpy_method/tests/test_sparrowpy_interface_class.py | 2 +- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 3f2051a..71ea800 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -57,6 +57,8 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: speed_of_sound = simulation_settings['speed_of_sound'] etc_duration_s = simulation_settings['etc_duration_s'] max_reflection_order = simulation_settings['max_reflection_order'] + patch_length = simulation_settings['patch_length'] + # Read source and receiver positions source_coords = pf.Coordinates( @@ -79,7 +81,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: walls_points, walls_normal, walls_up_vector, patches_points, n_patches, patch_to_wall_ids, material_to_walls, alphas, scattering, - ) = _import_room_geometry(json_file_path) + ) = _import_room_geometry(json_file_path, patch_length) radiosity = sparrowpy.DirectionalRadiosityFast( walls_points, @@ -136,7 +138,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: print("sparrowpy simulation completed successfully!") -def _import_room_geometry(json_file_path): +def _import_room_geometry(json_file_path, patch_length): """Import room geometry and absorption coefficients. The geometry is read from a .geo file specified in the JSON input file. @@ -176,7 +178,7 @@ def _import_room_geometry(json_file_path): # print("Extracted value:", lc_value) # break - gmsh.option.setNumber('Mesh.MeshSizeFactor', 5/lc_value) + gmsh.option.setNumber('Mesh.MeshSizeFactor', patch_length/lc_value) # generate 2d surface mesh dim = 2 # 2D surfaces diff --git a/sparrowpy_method/tests/test_input_sparrowpy.json b/sparrowpy_method/tests/test_input_sparrowpy.json index 27b04a1..b0c42cc 100644 --- a/sparrowpy_method/tests/test_input_sparrowpy.json +++ b/sparrowpy_method/tests/test_input_sparrowpy.json @@ -13,7 +13,8 @@ "speed_of_sound": 343.2, "etc_time_resolution_s": 0.05, "etc_duration_s": 0.5, - "max_reflection_order": 2 + "max_reflection_order": 2, + "patch_length": 5 }, "results": [ { diff --git a/sparrowpy_method/tests/test_sparrowpy_cli.py b/sparrowpy_method/tests/test_sparrowpy_cli.py index 1273447..db45dac 100644 --- a/sparrowpy_method/tests/test_sparrowpy_cli.py +++ b/sparrowpy_method/tests/test_sparrowpy_cli.py @@ -15,13 +15,6 @@ def test_sparrowpy_method_cli(mock_requests_post, create_temporary_input_file): with open(create_temporary_input_file, 'r') as f: data = json.load(f) - - sparrowpy_method_object = sparrowpyMethod(create_temporary_input_file) - sparrowpy_method_object.run_simulation() - - # Save the results to a separate file - sparrowpy_method_object.save_results() - # check that results were written to the JSON file assert "receiverResults" in data['results'][0]['responses'][0] results = data['results'][0]['responses'][0]['receiverResults'] diff --git a/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py index 048a3d5..ef2f8db 100644 --- a/sparrowpy_method/tests/test_sparrowpy_interface_class.py +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -28,7 +28,7 @@ def test_import_room_geometry(create_temporary_input_file): patches_points, n_patches, patch_to_wall_ids, material_to_walls, alphas, scattering, ) = _import_room_geometry( - create_temporary_input_file) + create_temporary_input_file, patch_length=5) # create radiosity object radiosity = sparrowpy.DirectionalRadiosityFast( From ef49870557cc4514385e4445245dee4f40577e48 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 11 May 2026 17:49:56 +0200 Subject: [PATCH 04/33] give plausible inputs activate direct sound --- .../sparrowpy_interface/sparrowpy_interface.py | 6 +----- sparrowpy_method/tests/test_input_sparrowpy.json | 8 ++++---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 71ea800..8671bcb 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -11,8 +11,6 @@ import trimesh - - class sparrowpyMethod(SimulationMethod): """Interface class to run the sparrowpy method. @@ -59,7 +57,6 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: max_reflection_order = simulation_settings['max_reflection_order'] patch_length = simulation_settings['patch_length'] - # Read source and receiver positions source_coords = pf.Coordinates( result_container["results"][0]["sourceX"], @@ -113,10 +110,9 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: etc_time_resolution=etc_time_resolution_s, etc_duration=etc_duration_s, max_reflection_order=max_reflection_order) - etc_radiosity = radiosity.collect_energy_receiver_mono( - receivers=receiver_coords) + receivers=receiver_coords, direct_sound=True) # Write results back to JSON for i_rec in range(n_receivers): diff --git a/sparrowpy_method/tests/test_input_sparrowpy.json b/sparrowpy_method/tests/test_input_sparrowpy.json index b0c42cc..1dc435a 100644 --- a/sparrowpy_method/tests/test_input_sparrowpy.json +++ b/sparrowpy_method/tests/test_input_sparrowpy.json @@ -11,10 +11,10 @@ }, "simulationSettings": { "speed_of_sound": 343.2, - "etc_time_resolution_s": 0.05, - "etc_duration_s": 0.5, - "max_reflection_order": 2, - "patch_length": 5 + "etc_time_resolution_s": 0.001, + "etc_duration_s": 0.1, + "max_reflection_order": 20, + "patch_length": 3 }, "results": [ { From f97fbd543791e0c676addb8e735901a1ff35750d Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 11 May 2026 17:50:00 +0200 Subject: [PATCH 05/33] add notes --- sparrowpy_method/todos.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 sparrowpy_method/todos.md diff --git a/sparrowpy_method/todos.md b/sparrowpy_method/todos.md new file mode 100644 index 0000000..ce63bfd --- /dev/null +++ b/sparrowpy_method/todos.md @@ -0,0 +1,9 @@ +# todos + +- [ ] add progress bar + + +## open + +- convex surfaces +- \ No newline at end of file From 033723567fa89ec313714c68de0cf250b19915be Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 10:01:51 +0200 Subject: [PATCH 06/33] fix normal calculation --- .../sparrowpy_interface.py | 30 ++++++++++++++----- .../tests/test_sparrowpy_interface_class.py | 14 +++++++++ sparrowpy_method/todos.md | 2 ++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 8671bcb..07ae266 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -119,7 +119,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: for i_frequency in range(n_bands): result_container["results"][0]["responses"][i_rec]["receiverResults"].append( { - "data": 10*np.log10(etc_radiosity.time[i_rec, i_frequency]).tolist(), + "data": 10*np.log10(etc_radiosity.time[i_rec, i_frequency]/1e-12).tolist(), "t": etc_radiosity.times.tolist(), "frequency": frequencies[i_frequency], "type": "edc", @@ -166,13 +166,17 @@ def _import_room_geometry(json_file_path, patch_length): geometry_file = input_data['geo_path'] gmsh.open(geometry_file) + # Read the content of the Geo file + with open(geometry_file, 'r') as file: + geo_content = file.readlines() + # If an lc is given in the geo file, we want to compensate for this lc_value = 1 # set to 1 by default - # for line in geo_content: - # if "lc =" in line: - # lc_value = float(line.split('=')[1].strip().strip(';')) - # print("Extracted value:", lc_value) - # break + for line in geo_content: + if "lc =" in line: + lc_value = float(line.split('=')[1].strip().strip(';')) + print("Extracted value:", lc_value) + break gmsh.option.setNumber('Mesh.MeshSizeFactor', patch_length/lc_value) @@ -219,7 +223,8 @@ def _import_room_geometry(json_file_path, patch_length): # get the element type for surface mesh element_type = gmsh.model.mesh.getElementType("Triangle", 1, True) - + + room_center = np.mean(coords, axis=0) alphas = [] walls_points = [] @@ -243,7 +248,18 @@ def _import_room_geometry(json_file_path, patch_length): for p in wall_points: wall_idx.append(np.argmin(np.sum(np.abs((mesh.vertices-p)), axis=1))) wall_points = np.unique(mesh.vertices[wall_idx], axis=0, ) + + # flip normals to the center wall_normal = np.median(mesh.face_normals, axis=0) + normal_dimension_mask = np.abs(wall_normal)>1e-3 + surface_center = np.mean(wall_points, axis=0) + pointing_inwards = np.sign((room_center-surface_center)[normal_dimension_mask]) == np.sign(wall_normal[normal_dimension_mask]) + if not np.all(pointing_inwards): + wall_normal *= -1 + elif not np.any(pointing_inwards): + raise ValueError('Flipping normals inwards did not work.') + + # calculate wall up vector if np.abs(wall_normal[2]) > 1e-2: wall_up_vector = [1, 0, 0] else: diff --git a/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py index ef2f8db..434a66f 100644 --- a/sparrowpy_method/tests/test_sparrowpy_interface_class.py +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -49,3 +49,17 @@ def test_import_room_geometry(create_temporary_input_file): npt.assert_equal(np.array(scattering).shape, (6, 6)) npt.assert_array_less(np.array(alphas), 1) npt.assert_array_less(np.array(scattering), 1) + + +def test_import_room_geometry_normals(create_temporary_input_file): + ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering, + ) = _import_room_geometry( + create_temporary_input_file, patch_length=5) + + npt.assert_almost_equal(walls_normal[0], [0, 0, 1]) # floor + npt.assert_almost_equal(walls_normal[2], [0, 0, -1]) # ceiling + npt.assert_almost_equal(walls_normal[3], [0, 1, 0]) # wall2 + npt.assert_almost_equal(walls_normal[4], [1, 0, 0]) # wall3 diff --git a/sparrowpy_method/todos.md b/sparrowpy_method/todos.md index ce63bfd..e47d480 100644 --- a/sparrowpy_method/todos.md +++ b/sparrowpy_method/todos.md @@ -1,6 +1,8 @@ # todos - [ ] add progress bar +- [x] make normals point inside + ## open From 57b4c5fd06e558da11c5bd001f721a40e484822c Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 10:13:04 +0200 Subject: [PATCH 07/33] add config fowllowing guidelines --- example_settings/sparrowpy_setting.json | 60 +++++++++++++++++++++++++ methods-config.json | 10 +++++ sparrowpy_method/todos.md | 1 - 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 example_settings/sparrowpy_setting.json diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json new file mode 100644 index 0000000..655eabc --- /dev/null +++ b/example_settings/sparrowpy_setting.json @@ -0,0 +1,60 @@ +{ + "type": "simulationSettings", + "options": [ + { + "name": "Speed of sound", + "id": "speed_of_sound", + "type": "float", + "display": "text", + "min": 100, + "max": 500, + "default": 343, + "step": 1, + "endAdornment": "m/s" + }, + { + "name": "ETC time resolution", + "id": "etc_time_resolution_s", + "type": "float", + "display": "text", + "min": 0.001, + "max": 0.5, + "step": 0.001, + "default": 1, + "endAdornment": "s" + }, + { + "name": "ETC duration", + "id": "etc_duration_s", + "type": "float", + "display": "text", + "min": 0.1, + "max": 4, + "step": 0.1, + "default": 1, + "endAdornment": "s" + }, + { + "name": "ETC time resolution", + "id": "max_reflection_order", + "type": "int", + "display": "text", + "min": 1, + "max": 100, + "step": 1, + "default": 30, + "endAdornment": "s" + }, + { + "name": "Patch size", + "id": "patch_length", + "type": "float", + "display": "text", + "min": 0.01, + "max": 10, + "step": 0.01, + "default": 3, + "endAdornment": "m" + } + ] +} \ No newline at end of file diff --git a/methods-config.json b/methods-config.json index 2683ca6..d82c366 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": "sparrowpy", + "containerImage": "sparrowpy_image:latest", + "envVars": {}, + "label": "Acoustic Radiance Transfer assuming diffuse reflections", + "settings":"sparrowpy_setting.json", + "entryFile":"sparrowpy_interface.py", + "repositoryURL":"https://github.com/sparrow-acoustics/sparrowpy", + "documentationURL":"https://sparrowpy.readthedocs.io/en/stable" + }, { "simulationType": "MyNewMethod", "containerImage": "mynewmethod_image:latest", diff --git a/sparrowpy_method/todos.md b/sparrowpy_method/todos.md index e47d480..eaa1f81 100644 --- a/sparrowpy_method/todos.md +++ b/sparrowpy_method/todos.md @@ -4,7 +4,6 @@ - [x] make normals point inside - ## open - convex surfaces From a6f7753dd00d4279f5b88c3e636705081f7d68c5 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 10:39:56 +0200 Subject: [PATCH 08/33] fix settings --- example_settings/sparrowpy_setting.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json index 655eabc..c139b2e 100644 --- a/example_settings/sparrowpy_setting.json +++ b/example_settings/sparrowpy_setting.json @@ -20,7 +20,7 @@ "min": 0.001, "max": 0.5, "step": 0.001, - "default": 1, + "default": 0.01, "endAdornment": "s" }, { @@ -35,7 +35,7 @@ "endAdornment": "s" }, { - "name": "ETC time resolution", + "name": "Maximum reflection order", "id": "max_reflection_order", "type": "int", "display": "text", @@ -43,7 +43,7 @@ "max": 100, "step": 1, "default": 30, - "endAdornment": "s" + "endAdornment": "" }, { "name": "Patch size", From f413ff892eae9b8d64833272a5260430667cf613 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 10:41:21 +0200 Subject: [PATCH 09/33] fix repo url in docs --- docs/source/includes/contributing/setup_dev.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/includes/contributing/setup_dev.rst b/docs/source/includes/contributing/setup_dev.rst index e3ab26e..fa6d8d4 100644 --- a/docs/source/includes/contributing/setup_dev.rst +++ b/docs/source/includes/contributing/setup_dev.rst @@ -5,7 +5,7 @@ Ready to contribute? Here's how to set up your development environment for CHORA We always recommend creating a `fork `_ of the repository you would like to contribute to. This allows you to freely develop and test your changes without affecting the main repository until you're ready to submit a pull request. -1. Fork the repository you want to contribute to (e.g., `choras/simulation-backend `_). Please make sure that you enable giving maintainers access to your fork, so we can help you if you run into issues. +1. Fork the repository you want to contribute to (e.g., `choras-org/simulation-backend `_). Please make sure that you enable giving maintainers access to your fork, so we can help you if you run into issues. 2. Clone your forked main repository to your local machine: .. code-block:: bash @@ -39,7 +39,7 @@ This allows you to freely develop and test your changes without affecting the ma .. code-block:: bash - git remote add upstream https://github.com/choras/simulation-backend + git remote add upstream https://github.com/choras-org/simulation-backend 6. Finally, create a new branch for your changes: From 36658e8ded9598e5347e46484e91ec1d5d37b567 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 10:42:47 +0200 Subject: [PATCH 10/33] Revert "fix repo url in docs" This reverts commit f413ff892eae9b8d64833272a5260430667cf613. --- docs/source/includes/contributing/setup_dev.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/includes/contributing/setup_dev.rst b/docs/source/includes/contributing/setup_dev.rst index fa6d8d4..e3ab26e 100644 --- a/docs/source/includes/contributing/setup_dev.rst +++ b/docs/source/includes/contributing/setup_dev.rst @@ -5,7 +5,7 @@ Ready to contribute? Here's how to set up your development environment for CHORA We always recommend creating a `fork `_ of the repository you would like to contribute to. This allows you to freely develop and test your changes without affecting the main repository until you're ready to submit a pull request. -1. Fork the repository you want to contribute to (e.g., `choras-org/simulation-backend `_). Please make sure that you enable giving maintainers access to your fork, so we can help you if you run into issues. +1. Fork the repository you want to contribute to (e.g., `choras/simulation-backend `_). Please make sure that you enable giving maintainers access to your fork, so we can help you if you run into issues. 2. Clone your forked main repository to your local machine: .. code-block:: bash @@ -39,7 +39,7 @@ This allows you to freely develop and test your changes without affecting the ma .. code-block:: bash - git remote add upstream https://github.com/choras-org/simulation-backend + git remote add upstream https://github.com/choras/simulation-backend 6. Finally, create a new branch for your changes: From 62acee1af9b4bc0678c689c362ec88fb269cd410 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 10:57:46 +0200 Subject: [PATCH 11/33] add progressbar in infos --- .../sparrowpy_interface.py | 28 +++++++++++++++---- sparrowpy_method/todos.md | 4 +-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 07ae266..e2f46a1 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -42,12 +42,12 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: Args: json_file_path: Path to the JSON configuration file """ + print('extract simulation settings...') # Load the input JSON file with open(json_file_path, "r") as json_file: result_container = json.load(json_file) # extract simulation settings - frequencies = result_container['results'][0]['frequencies'] n_bands = len(frequencies) simulation_settings = result_container["simulationSettings"] @@ -73,6 +73,8 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: cart[i_rec, 2] = rec["z"] receiver_coords.cartesian = cart + print('extract geometry...') + set_progress_and_save(5, result_container, json_file_path) # read walls and triangular patches ( walls_points, walls_normal, walls_up_vector, @@ -89,6 +91,8 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: patch_to_wall_ids, ) + print('set materials...') + set_progress_and_save(10, result_container, json_file_path) # apply materials incoming = pf.Coordinates(0, 0, 1, weights=1) outgoing = pf.Coordinates(0, 0, 1, weights=1) @@ -101,19 +105,29 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: radiosity.set_wall_brdf(jj, brdf, incoming, outgoing) # run simulation + print('bake geometry...') + set_progress_and_save(15, result_container, json_file_path) radiosity.bake_geometry() + print('initialize source...') + set_progress_and_save(40, result_container, json_file_path) radiosity.init_source_energy(source_coords) + print('compute energy exchange...') + set_progress_and_save(65, result_container, json_file_path) radiosity.calculate_energy_exchange( speed_of_sound=speed_of_sound, etc_time_resolution=etc_time_resolution_s, etc_duration=etc_duration_s, max_reflection_order=max_reflection_order) + print('collect energy at receiver...') + set_progress_and_save(90, result_container, json_file_path) etc_radiosity = radiosity.collect_energy_receiver_mono( receivers=receiver_coords, direct_sound=True) + print('writing results...') + set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON for i_rec in range(n_receivers): for i_frequency in range(n_bands): @@ -125,14 +139,18 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: "type": "edc", } ) - result_container["results"][0]["percentage"] = 100 - + # Save the updated JSON - with open(json_file_path, "w") as json_output: - json_output.write(json.dumps(result_container, indent=4)) + set_progress_and_save(100, result_container, json_file_path) print("sparrowpy simulation completed successfully!") +def set_progress_and_save(percentage, result_container, json_file_path): + result_container["results"][0]["percentage"] = percentage + # Save the updated JSON + with open(json_file_path, "w") as json_output: + json_output.write(json.dumps(result_container, indent=4)) + def _import_room_geometry(json_file_path, patch_length): """Import room geometry and absorption coefficients. diff --git a/sparrowpy_method/todos.md b/sparrowpy_method/todos.md index eaa1f81..dfc53c8 100644 --- a/sparrowpy_method/todos.md +++ b/sparrowpy_method/todos.md @@ -1,10 +1,10 @@ # todos -- [ ] add progress bar +- [x] add progress bar - [x] make normals point inside ## open -- convex surfaces +- convex surfaces? - \ No newline at end of file From 944103522f16f19b70a2c73e1f53878af885f422 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 11:13:27 +0200 Subject: [PATCH 12/33] remove -inf from results --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index e2f46a1..a1e7078 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -129,11 +129,13 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: print('writing results...') set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON + result_db = 10*np.log10(etc_radiosity.time/1e-12) + result_db[result_db==-np.inf] = 10*np.log10(np.finfo(float).eps) for i_rec in range(n_receivers): for i_frequency in range(n_bands): result_container["results"][0]["responses"][i_rec]["receiverResults"].append( { - "data": 10*np.log10(etc_radiosity.time[i_rec, i_frequency]/1e-12).tolist(), + "data": result_db[i_rec, i_frequency].tolist(), "t": etc_radiosity.times.tolist(), "frequency": frequencies[i_frequency], "type": "edc", From c31cc9d7f2681f86517753276acbf88258691914 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 11:17:47 +0200 Subject: [PATCH 13/33] fix no inf --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 2 +- sparrowpy_method/tests/test_sparrowpy_interface_class.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index a1e7078..0eec079 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -130,7 +130,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON result_db = 10*np.log10(etc_radiosity.time/1e-12) - result_db[result_db==-np.inf] = 10*np.log10(np.finfo(float).eps) + result_db[result_db<-500] = -500 for i_rec in range(n_receivers): for i_frequency in range(n_bands): result_container["results"][0]["responses"][i_rec]["receiverResults"].append( diff --git a/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py index 434a66f..27ac800 100644 --- a/sparrowpy_method/tests/test_sparrowpy_interface_class.py +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -21,6 +21,11 @@ def test_simple_method(create_temporary_input_file): assert results is not None assert len(results) > 0 + # test no inf + for i in range(6): + assert np.min(results[i]['data']) > -np.inf + + def test_import_room_geometry(create_temporary_input_file): ( From 805bc1922f591bed3958d49f5984232b516e7e2b Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 12:05:16 +0200 Subject: [PATCH 14/33] set more useful db lim --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 0eec079..cbdf78b 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -129,8 +129,10 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: print('writing results...') set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON + dynamic_range_db = 100 result_db = 10*np.log10(etc_radiosity.time/1e-12) - result_db[result_db<-500] = -500 + limit = np.max(result_db) - dynamic_range_db + result_db[result_db Date: Tue, 12 May 2026 12:24:31 +0200 Subject: [PATCH 15/33] fix lim for etc resoltuion --- example_settings/sparrowpy_setting.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json index c139b2e..463c095 100644 --- a/example_settings/sparrowpy_setting.json +++ b/example_settings/sparrowpy_setting.json @@ -17,10 +17,10 @@ "id": "etc_time_resolution_s", "type": "float", "display": "text", - "min": 0.001, - "max": 0.5, + "min": 0.0001, + "max": 0.1, "step": 0.001, - "default": 0.01, + "default": 0.001, "endAdornment": "s" }, { From a591ed76dd7ca497b8d9ee160d9b3705440144ba Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 12:30:37 +0200 Subject: [PATCH 16/33] add parameters calciualtoon --- sparrowpy_method/pyproject.toml | 1 + .../sparrowpy_interface.py | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sparrowpy_method/pyproject.toml b/sparrowpy_method/pyproject.toml index 9b6e9d2..d228921 100644 --- a/sparrowpy_method/pyproject.toml +++ b/sparrowpy_method/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "gmsh", "trimesh", "pyfar", + "pyrato", ] [project.optional-dependencies] diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index cbdf78b..1dc2568 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -9,6 +9,7 @@ import pyfar as pf import numpy as np import trimesh +import pyrato class sparrowpyMethod(SimulationMethod): @@ -143,7 +144,25 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: "type": "edc", } ) - + edc = pyrato.edc.schroeder_integration(etc_radiosity[i_rec, :], is_energy=True) + t20 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T20') + result_container["results"][0]["responses"][i_rec]["parameters"]['t20'] = t20.tolist() + + t30 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T30')[0] + result_container["results"][0]["responses"][i_rec]["parameters"]['t30'] = t30.tolist() + + c80 = pyrato.parameters.clarity(edc, 80)[0] + result_container["results"][0]["responses"][i_rec]["parameters"]['c80'] = c80.tolist() + + d50 = pyrato.parameters.definition(edc, 50)[0] + result_container["results"][0]["responses"][i_rec]["parameters"]['d50'] = d50.tolist() + + ts = np.ones_like(d50) + result_container["results"][0]["responses"][i_rec]["parameters"]['ts'] = ts.tolist() + + spl = 10*np.log10(edc.time[i_rec, ..., 0]/1e-12) + result_container["results"][0]["responses"][i_rec]["parameters"]['spl_t0_freq'] = spl.tolist() + # Save the updated JSON set_progress_and_save(100, result_container, json_file_path) From bc8a0c91807201afaf4a10e5ac8395983b7c9a60 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 12:44:27 +0200 Subject: [PATCH 17/33] add center time --- .../sparrowpy_interface.py | 89 +++++++++++++++++-- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 1dc2568..898a541 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -148,19 +148,19 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: t20 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T20') result_container["results"][0]["responses"][i_rec]["parameters"]['t20'] = t20.tolist() - t30 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T30')[0] + t30 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T30') result_container["results"][0]["responses"][i_rec]["parameters"]['t30'] = t30.tolist() - c80 = pyrato.parameters.clarity(edc, 80)[0] + c80 = pyrato.parameters.clarity(edc, 80) result_container["results"][0]["responses"][i_rec]["parameters"]['c80'] = c80.tolist() - d50 = pyrato.parameters.definition(edc, 50)[0] + d50 = pyrato.parameters.definition(edc, 50) result_container["results"][0]["responses"][i_rec]["parameters"]['d50'] = d50.tolist() - ts = np.ones_like(d50) + ts = center_time(edc) # TODO replace by pyrato 1.1.0 version result_container["results"][0]["responses"][i_rec]["parameters"]['ts'] = ts.tolist() - spl = 10*np.log10(edc.time[i_rec, ..., 0]/1e-12) + spl = 10*np.log10(edc.time[..., 0]/1e-12) result_container["results"][0]["responses"][i_rec]["parameters"]['spl_t0_freq'] = spl.tolist() # Save the updated JSON @@ -335,3 +335,82 @@ def _import_room_geometry(json_file_path, patch_length): walls_points, walls_normal, walls_up_vector, patches_points, n_patches, patch_to_wall_ids, material_to_walls, alphas, scattering) + + +# copy pasted from pyrato +def center_time(energy_decay_curve): + r""" + Calculate the room-acoustic center time (:math:`T_s`). + + The center time :math:`T_s` is the time of the centroid of the squared + impulse response. It quantifies the balance between early and late + sound energy [#isoTs]_. + + The parameter is defined as + + .. math:: + + T_s = + \frac{ + \displaystyle \int_{0}^{\infty} t \cdot p^2(t)\,\mathrm{d}t + }{ + \displaystyle \int_{0}^{\infty} p^2(t)\,\mathrm{d}t + } + + where :math:`p(t)` is the room impulse response sound pressure. + + Using the energy decay curve :math:`e(t)`, the parameter can be + computed efficiently via the EDC identity as + + .. math:: + + T_s = + \frac{ + \displaystyle \int_{0}^{\infty} e(t)\,\mathrm{d}t + }{ + e(0) + }. + + Parameters + ---------- + energy_decay_curve : pyfar.TimeData + Energy decay curve of the room impulse response. The EDC must + start at time zero and must have equal time spacing. + + Returns + ------- + center_time : numpy.ndarray + Center time (:math:`T_s`) in seconds, + shaped according to the channel shape of the input EDC. + + References + ---------- + .. [#isoTs] ISO 3382, Acoustics — Measurement of the reverberation + time of rooms with reference to other acoustical parameters. + """ + + if not isinstance(energy_decay_curve, pf.TimeData): + raise TypeError( + "energy_decay_curve must be a pyfar.TimeData or derived object.") + + if not np.isclose(energy_decay_curve.times[0], 0.0): + raise ValueError("energy_decay_curve must start at time zero.") + + if np.any(energy_decay_curve.time[..., 0] == 0): + raise ValueError( + "Initial energy of energy_decay_curve must not be zero.") + + dt = np.diff(energy_decay_curve.times) + if not np.allclose(dt, dt[0]): + raise ValueError( + "energy_decay_curve must have equal time spacing.") + + sampling_interval = dt[0] + initial_energy = energy_decay_curve.time[..., 0] + center_time = ( + np.nansum(energy_decay_curve.time, axis=-1) + * sampling_interval + / initial_energy + ) + + return center_time \ No newline at end of file From ee631d8f86ae16d2bbb1ddcd53c65dce1ae83fda Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 12:48:33 +0200 Subject: [PATCH 18/33] asd --- .../sparrowpy_interface/sparrowpy_interface.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 898a541..91b6b84 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -135,16 +135,19 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: limit = np.max(result_db) - dynamic_range_db result_db[result_db Date: Tue, 12 May 2026 12:55:04 +0200 Subject: [PATCH 19/33] fix --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 91b6b84..fb7d1dd 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -127,7 +127,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: etc_radiosity = radiosity.collect_energy_receiver_mono( receivers=receiver_coords, direct_sound=True) - print('writing results...') + print('calculating room parameters and writing results...') set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON dynamic_range_db = 100 @@ -136,7 +136,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: result_db[result_db Date: Tue, 12 May 2026 13:43:33 +0200 Subject: [PATCH 20/33] fix scattering --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index fb7d1dd..d2a8575 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -332,12 +332,10 @@ def _import_room_geometry(json_file_path, patch_length): walls_up_vector = np.array(walls_up_vector) patches_points = np.concatenate(patches_points) - scattering = np.zeros_like(alphas) - return ( walls_points, walls_normal, walls_up_vector, patches_points, n_patches, patch_to_wall_ids, - material_to_walls, alphas, scattering) + material_to_walls, alphas, scatterings) # copy pasted from pyrato From 6d3081e4a4dca9fda8fb9eb9627209622490a9e7 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 13:55:16 +0200 Subject: [PATCH 21/33] fix test --- sparrowpy_method/tests/test_sparrowpy_interface_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py index 27ac800..6d2100b 100644 --- a/sparrowpy_method/tests/test_sparrowpy_interface_class.py +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -53,7 +53,7 @@ def test_import_room_geometry(create_temporary_input_file): npt.assert_equal(np.array(alphas).shape, (6, 6)) npt.assert_equal(np.array(scattering).shape, (6, 6)) npt.assert_array_less(np.array(alphas), 1) - npt.assert_array_less(np.array(scattering), 1) + npt.assert_allclose(np.array(scattering), 1) def test_import_room_geometry_normals(create_temporary_input_file): From bd954787eaa927468251eb670ecb905876dc55b4 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 14:11:07 +0200 Subject: [PATCH 22/33] fix parameters --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index d2a8575..c56339c 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -160,10 +160,10 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: d50 = pyrato.parameters.definition(edc, 50) result_container["results"][0]["responses"][i_rec]["parameters"]['d50'] = d50.tolist() - ts = center_time(edc) # TODO replace by pyrato 1.1.0 version + ts = center_time(edc)*1000 # in ms TODO replace by pyrato 1.1.0 version result_container["results"][0]["responses"][i_rec]["parameters"]['ts'] = ts.tolist() - spl = 10*np.log10(edc.time[..., 0]/1e-12) + spl = 10*np.log10(edc.time[..., 0]/1e-12/(4*np.pi)) result_container["results"][0]["responses"][i_rec]["parameters"]['spl_t0_freq'] = spl.tolist() # Save the updated JSON From e7763f496f9f5ac1dba1240fdc432ad6be0fef64 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 14:16:20 +0200 Subject: [PATCH 23/33] asd --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index c56339c..497a44f 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -136,7 +136,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: result_db[result_db None: c80 = pyrato.parameters.clarity(edc, 80) result_container["results"][0]["responses"][i_rec]["parameters"]['c80'] = c80.tolist() - d50 = pyrato.parameters.definition(edc, 50) + d50 = pyrato.parameters.definition(edc, 50) * 100 result_container["results"][0]["responses"][i_rec]["parameters"]['d50'] = d50.tolist() ts = center_time(edc)*1000 # in ms TODO replace by pyrato 1.1.0 version From 6e21b9d03b81362df80992ef515c5fc5563428c0 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 14:24:24 +0200 Subject: [PATCH 24/33] kkkk --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 497a44f..1659d78 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -163,7 +163,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: ts = center_time(edc)*1000 # in ms TODO replace by pyrato 1.1.0 version result_container["results"][0]["responses"][i_rec]["parameters"]['ts'] = ts.tolist() - spl = 10*np.log10(edc.time[..., 0]/1e-12/(4*np.pi)) + spl = 10*np.log10(edc.time[..., 0]/1e-12) result_container["results"][0]["responses"][i_rec]["parameters"]['spl_t0_freq'] = spl.tolist() # Save the updated JSON From cfaa824789b69663fc643f79564fa958d848cb48 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 14:30:25 +0200 Subject: [PATCH 25/33] add edt --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 1659d78..5b8ad2b 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -165,7 +165,10 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: spl = 10*np.log10(edc.time[..., 0]/1e-12) result_container["results"][0]["responses"][i_rec]["parameters"]['spl_t0_freq'] = spl.tolist() - + + edt = pyrato.parameters.reverberation_time_linear_regression(edc, 'EDT') + result_container["results"][0]["responses"][i_rec]["parameters"]['edt'] = edt.tolist() + # Save the updated JSON set_progress_and_save(100, result_container, json_file_path) From d547c851621465f36b75bbfac3b876343e0a0535 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 14:59:26 +0200 Subject: [PATCH 26/33] fix --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 5b8ad2b..f766e21 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -167,6 +167,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: result_container["results"][0]["responses"][i_rec]["parameters"]['spl_t0_freq'] = spl.tolist() edt = pyrato.parameters.reverberation_time_linear_regression(edc, 'EDT') + edt[edt==-np.inf] = -1 result_container["results"][0]["responses"][i_rec]["parameters"]['edt'] = edt.tolist() # Save the updated JSON From d8c40d5f4fefab58c416eb266bc04c1d865617a2 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 16:16:00 +0200 Subject: [PATCH 27/33] change limits --- example_settings/sparrowpy_setting.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json index 463c095..e0e8878 100644 --- a/example_settings/sparrowpy_setting.json +++ b/example_settings/sparrowpy_setting.json @@ -40,7 +40,7 @@ "type": "int", "display": "text", "min": 1, - "max": 100, + "max": 500, "step": 1, "default": 30, "endAdornment": "" From c273e20a39a3e1976da256e4367424ec1bf50041 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 16:31:00 +0200 Subject: [PATCH 28/33] fix edc conversion bug --- .../sparrowpy_interface.py | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index f766e21..7e63c15 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -131,11 +131,13 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON dynamic_range_db = 100 - result_db = 10*np.log10(etc_radiosity.time/1e-12) - limit = np.max(result_db) - dynamic_range_db - result_db[result_db pf.TimeData: + """Convert energy time curve into energy decay curve. + + Parameters + ---------- + etc : pf.TimeData + energy time curve of cshape (..., n_bands). + lower_frequency_cutoffs : np.ndarray + lower cutoff frequencies from the frequency bands of shape (n_bands). + upper_frequency_cutoffs : np.ndarray + lower cutoff frequencies from the frequency bands of shape (n_bands). + + Results + ------- + edc : pf.TimeData + Resulting energy decay curve. + """ + full_frequency_range = np.max( + upper_frequency_cutoffs) - np.min( + lower_frequency_cutoffs) + bandwidth = upper_frequency_cutoffs - lower_frequency_cutoffs + + etc_eq = etc * np.sqrt(bandwidth/full_frequency_range) + edc = pyrato.edc.schroeder_integration(etc_eq, is_energy=True) + return edc + def _import_room_geometry(json_file_path, patch_length): """Import room geometry and absorption coefficients. From f832f6ce9becd5f093f5edf9f2ef4e922f6e3d7c Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 16:38:11 +0200 Subject: [PATCH 29/33] fix --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index 7e63c15..b8d9f56 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -209,7 +209,7 @@ def etc_to_edc( lower_frequency_cutoffs) bandwidth = upper_frequency_cutoffs - lower_frequency_cutoffs - etc_eq = etc * np.sqrt(bandwidth/full_frequency_range) + etc_eq = etc * (bandwidth/full_frequency_range) edc = pyrato.edc.schroeder_integration(etc_eq, is_energy=True) return edc From b7c63abcdacf51f2288958cda1a37b154bd00580 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 16:51:12 +0200 Subject: [PATCH 30/33] add sound_power_W --- example_settings/sparrowpy_setting.json | 12 ++++++++++++ .../sparrowpy_interface/sparrowpy_interface.py | 3 +++ sparrowpy_method/tests/test_input_sparrowpy.json | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json index e0e8878..8d918a4 100644 --- a/example_settings/sparrowpy_setting.json +++ b/example_settings/sparrowpy_setting.json @@ -55,6 +55,18 @@ "step": 0.01, "default": 3, "endAdornment": "m" + }, + { + "name": "Source Power", + "id": "sound_power_W", + "type": "float", + "display": "text", + "min": 0, + "max": 10, + "step": 0.01, + "default": 0.0796, + "endAdornment": "W" } + ] } \ No newline at end of file diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index b8d9f56..ebd6720 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -57,6 +57,7 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: etc_duration_s = simulation_settings['etc_duration_s'] max_reflection_order = simulation_settings['max_reflection_order'] patch_length = simulation_settings['patch_length'] + sound_power_W = simulation_settings['sound_power_W'] # Read source and receiver positions source_coords = pf.Coordinates( @@ -127,6 +128,8 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: etc_radiosity = radiosity.collect_energy_receiver_mono( receivers=receiver_coords, direct_sound=True) + # apply sound power + etc_radiosity = etc_radiosity * sound_power_W print('calculating room parameters and writing results...') set_progress_and_save(95, result_container, json_file_path) # Write results back to JSON diff --git a/sparrowpy_method/tests/test_input_sparrowpy.json b/sparrowpy_method/tests/test_input_sparrowpy.json index 1dc435a..19113b5 100644 --- a/sparrowpy_method/tests/test_input_sparrowpy.json +++ b/sparrowpy_method/tests/test_input_sparrowpy.json @@ -14,7 +14,8 @@ "etc_time_resolution_s": 0.001, "etc_duration_s": 0.1, "max_reflection_order": 20, - "patch_length": 3 + "patch_length": 3, + "sound_power_W": 1 }, "results": [ { From d492d98ac5ff2bd8b78752f11d34cd849a61c97a Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 21:22:12 +0200 Subject: [PATCH 31/33] etc duration lim fix --- example_settings/sparrowpy_setting.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json index 8d918a4..0b57d75 100644 --- a/example_settings/sparrowpy_setting.json +++ b/example_settings/sparrowpy_setting.json @@ -29,7 +29,7 @@ "type": "float", "display": "text", "min": 0.1, - "max": 4, + "max": 10, "step": 0.1, "default": 1, "endAdornment": "s" From 9b426d7114e0bedb5b0587ec065d90cb3fb76504 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Tue, 12 May 2026 21:47:42 +0200 Subject: [PATCH 32/33] asda --- sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py index ebd6720..c831eb1 100644 --- a/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -154,9 +154,11 @@ def _sparrowpy_method(self, json_file_path: str | Path) -> None: } ) t20 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T20') + t20[t20==-np.inf] = 0 result_container["results"][0]["responses"][i_rec]["parameters"]['t20'] = t20.tolist() t30 = pyrato.parameters.reverberation_time_linear_regression(edc, 'T30') + t30[t30==-np.inf] = 0 result_container["results"][0]["responses"][i_rec]["parameters"]['t30'] = t30.tolist() c80 = pyrato.parameters.clarity(edc, 80) From 332c0fb5d9b7b82525d07c7ada878c22577d7624 Mon Sep 17 00:00:00 2001 From: Anne Heimes Date: Mon, 11 May 2026 16:21:19 +0200 Subject: [PATCH 33/33] add sparrowpy simulation interface --- example_settings/sparrowpy_setting.json | 72 + methods-config.json | 10 + sparrowpy_method/Dockerfile | 22 + sparrowpy_method/LICENSE | 21 + sparrowpy_method/pyproject.toml | 78 + .../sparrowpy_interface/__cli__.py | 19 + .../sparrowpy_interface/__init__.py | 8 + .../sparrowpy_interface/__main__.py | 5 + .../sparrowpy_interface/definition.py | 92 + .../sparrowpy_interface.py | 458 ++++ sparrowpy_method/tests/conftest.py | 68 + sparrowpy_method/tests/test_definition.py | 37 + sparrowpy_method/tests/test_fixtures.py | 23 + .../tests/test_input_sparrowpy.json | 57 + .../tests/test_room_sparrowpy.geo | 51 + .../tests/test_room_sparrowpy.msh | 2116 +++++++++++++++++ sparrowpy_method/tests/test_sparrowpy_cli.py | 36 + .../tests/test_sparrowpy_interface_class.py | 70 + sparrowpy_method/todos.md | 10 + 19 files changed, 3253 insertions(+) create mode 100644 example_settings/sparrowpy_setting.json create mode 100644 sparrowpy_method/Dockerfile create mode 100644 sparrowpy_method/LICENSE create mode 100644 sparrowpy_method/pyproject.toml create mode 100644 sparrowpy_method/sparrowpy_interface/__cli__.py create mode 100644 sparrowpy_method/sparrowpy_interface/__init__.py create mode 100644 sparrowpy_method/sparrowpy_interface/__main__.py create mode 100644 sparrowpy_method/sparrowpy_interface/definition.py create mode 100644 sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py create mode 100644 sparrowpy_method/tests/conftest.py create mode 100644 sparrowpy_method/tests/test_definition.py create mode 100644 sparrowpy_method/tests/test_fixtures.py create mode 100644 sparrowpy_method/tests/test_input_sparrowpy.json create mode 100644 sparrowpy_method/tests/test_room_sparrowpy.geo create mode 100644 sparrowpy_method/tests/test_room_sparrowpy.msh create mode 100644 sparrowpy_method/tests/test_sparrowpy_cli.py create mode 100644 sparrowpy_method/tests/test_sparrowpy_interface_class.py create mode 100644 sparrowpy_method/todos.md diff --git a/example_settings/sparrowpy_setting.json b/example_settings/sparrowpy_setting.json new file mode 100644 index 0000000..0b57d75 --- /dev/null +++ b/example_settings/sparrowpy_setting.json @@ -0,0 +1,72 @@ +{ + "type": "simulationSettings", + "options": [ + { + "name": "Speed of sound", + "id": "speed_of_sound", + "type": "float", + "display": "text", + "min": 100, + "max": 500, + "default": 343, + "step": 1, + "endAdornment": "m/s" + }, + { + "name": "ETC time resolution", + "id": "etc_time_resolution_s", + "type": "float", + "display": "text", + "min": 0.0001, + "max": 0.1, + "step": 0.001, + "default": 0.001, + "endAdornment": "s" + }, + { + "name": "ETC duration", + "id": "etc_duration_s", + "type": "float", + "display": "text", + "min": 0.1, + "max": 10, + "step": 0.1, + "default": 1, + "endAdornment": "s" + }, + { + "name": "Maximum reflection order", + "id": "max_reflection_order", + "type": "int", + "display": "text", + "min": 1, + "max": 500, + "step": 1, + "default": 30, + "endAdornment": "" + }, + { + "name": "Patch size", + "id": "patch_length", + "type": "float", + "display": "text", + "min": 0.01, + "max": 10, + "step": 0.01, + "default": 3, + "endAdornment": "m" + }, + { + "name": "Source Power", + "id": "sound_power_W", + "type": "float", + "display": "text", + "min": 0, + "max": 10, + "step": 0.01, + "default": 0.0796, + "endAdornment": "W" + } + + ] +} \ No newline at end of file diff --git a/methods-config.json b/methods-config.json index 2683ca6..d82c366 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": "sparrowpy", + "containerImage": "sparrowpy_image:latest", + "envVars": {}, + "label": "Acoustic Radiance Transfer assuming diffuse reflections", + "settings":"sparrowpy_setting.json", + "entryFile":"sparrowpy_interface.py", + "repositoryURL":"https://github.com/sparrow-acoustics/sparrowpy", + "documentationURL":"https://sparrowpy.readthedocs.io/en/stable" + }, { "simulationType": "MyNewMethod", "containerImage": "mynewmethod_image:latest", diff --git a/sparrowpy_method/Dockerfile b/sparrowpy_method/Dockerfile new file mode 100644 index 0000000..5660806 --- /dev/null +++ b/sparrowpy_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 sparrowpy_method /app/sparrowpy_method + +# Install the method package +RUN pip install --no-cache-dir /app/sparrowpy_method + +WORKDIR /app/sparrowpy_method + +# Default command to run the containerized sparrowpy method +CMD ["python", "-m", "sparrowpy_interface"] diff --git a/sparrowpy_method/LICENSE b/sparrowpy_method/LICENSE new file mode 100644 index 0000000..3e7d213 --- /dev/null +++ b/sparrowpy_method/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026, The sparrowpy developers + +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/sparrowpy_method/pyproject.toml b/sparrowpy_method/pyproject.toml new file mode 100644 index 0000000..d228921 --- /dev/null +++ b/sparrowpy_method/pyproject.toml @@ -0,0 +1,78 @@ +[project] +name = "sparrowpy_interface" +version = "0.1.0" +description = "Sound Propagation with Acoustic Radiosity for Realistic Outdoor Worlds" +requires-python = ">=3.11,<3.15" +authors = [ + { name = "Anne Heimes", email = "ahe@akustik.rwth-aachen.de" }, +] +keywords = [ + "acoustic simulation", + "geometrical acoustics", + "acoustic radiance transfer", + "brdf", + "scattering", +] + +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>=1.23.0", + "sparrowpy>=1", + "requests", + "gmsh", + "trimesh", + "pyfar", + "pyrato", +] + +[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 = ["sparrowpy_interface[deploy,tests,docs]"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["sparrowpy_interface"] + +[project.scripts] +sparrowpy_interface = "sparrowpy_interface:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/sparrowpy_method/sparrowpy_interface/__cli__.py b/sparrowpy_method/sparrowpy_interface/__cli__.py new file mode 100644 index 0000000..29a0063 --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/__cli__.py @@ -0,0 +1,19 @@ +"""CLI module for sparrowpy method.""" +import os +from .sparrowpy_interface import sparrowpyMethod + + +def main() -> None: + """Run the sparrowpy 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 sparrowpy method with JSON_PATH={json_file_path}") + sparrowpy_method_object = sparrowpyMethod(json_file_path) + sparrowpy_method_object.run_simulation() + + # Save the results to a separate file + sparrowpy_method_object.save_results() + + print("sparrowpy container finished.") diff --git a/sparrowpy_method/sparrowpy_interface/__init__.py b/sparrowpy_method/sparrowpy_interface/__init__.py new file mode 100644 index 0000000..f6f01d9 --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/__init__.py @@ -0,0 +1,8 @@ +"""sparrowpyMethod package.""" +from .__main__ import main +from .sparrowpy_interface import sparrowpyMethod + +__all__ = [ + "main", + "sparrowpyMethod" +] diff --git a/sparrowpy_method/sparrowpy_interface/__main__.py b/sparrowpy_method/sparrowpy_interface/__main__.py new file mode 100644 index 0000000..c6c687c --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/__main__.py @@ -0,0 +1,5 @@ +"""Main module for sparrowpy method.""" +from .__cli__ import main + +if __name__ == "__main__": + main() diff --git a/sparrowpy_method/sparrowpy_interface/definition.py b/sparrowpy_method/sparrowpy_interface/definition.py new file mode 100644 index 0000000..ebd5739 --- /dev/null +++ b/sparrowpy_method/sparrowpy_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 execution 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/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py new file mode 100644 index 0000000..c831eb1 --- /dev/null +++ b/sparrowpy_method/sparrowpy_interface/sparrowpy_interface.py @@ -0,0 +1,458 @@ +"""Module implementing a CHORAS interface for sparrowpy. +""" +import json +from pathlib import Path + +from .definition import SimulationMethod +import sparrowpy +import gmsh +import pyfar as pf +import numpy as np +import trimesh +import pyrato + + +class sparrowpyMethod(SimulationMethod): + """Interface class to run the sparrowpy method. + + The class implements method to run the calculations for the + sparrowpy 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 sparrowpy 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._sparrowpy_method(self.input_json_path) + + def _sparrowpy_method(self, json_file_path: str | Path) -> None: + """ + Run sparrowpy simulation for acoustic wave propagation. + + Args: + json_file_path: Path to the JSON configuration file + """ + print('extract simulation settings...') + # Load the input JSON file + with open(json_file_path, "r") as json_file: + result_container = json.load(json_file) + + # extract simulation settings + frequencies = result_container['results'][0]['frequencies'] + n_bands = len(frequencies) + simulation_settings = result_container["simulationSettings"] + etc_time_resolution_s = simulation_settings['etc_time_resolution_s'] + speed_of_sound = simulation_settings['speed_of_sound'] + etc_duration_s = simulation_settings['etc_duration_s'] + max_reflection_order = simulation_settings['max_reflection_order'] + patch_length = simulation_settings['patch_length'] + sound_power_W = simulation_settings['sound_power_W'] + + # Read source and receiver positions + source_coords = pf.Coordinates( + result_container["results"][0]["sourceX"], + result_container["results"][0]["sourceY"], + result_container["results"][0]["sourceZ"], + ) + n_receivers = len(result_container["results"][0]["responses"]) + receiver_coords = pf.Coordinates(np.zeros((n_receivers)), 0, 0) + cart = receiver_coords.cartesian + for i_rec in range(n_receivers): + rec = result_container["results"][0]["responses"][i_rec] + cart[i_rec, 0] = rec["x"] + cart[i_rec, 1] = rec["y"] + cart[i_rec, 2] = rec["z"] + receiver_coords.cartesian = cart + + print('extract geometry...') + set_progress_and_save(5, result_container, json_file_path) + # read walls and triangular patches + ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering, + ) = _import_room_geometry(json_file_path, patch_length) + + radiosity = sparrowpy.DirectionalRadiosityFast( + walls_points, + walls_normal, + walls_up_vector, + patches_points, + n_patches, + patch_to_wall_ids, + ) + + print('set materials...') + set_progress_and_save(10, result_container, json_file_path) + # apply materials + incoming = pf.Coordinates(0, 0, 1, weights=1) + outgoing = pf.Coordinates(0, 0, 1, weights=1) + for ii, jj in enumerate(material_to_walls): + brdf = sparrowpy.brdf.create_from_scattering( + incoming, outgoing, + pf.FrequencyData(scattering[ii], frequencies), + pf.FrequencyData(alphas[ii], frequencies), + ) + radiosity.set_wall_brdf(jj, brdf, incoming, outgoing) + + # run simulation + print('bake geometry...') + set_progress_and_save(15, result_container, json_file_path) + radiosity.bake_geometry() + + print('initialize source...') + set_progress_and_save(40, result_container, json_file_path) + radiosity.init_source_energy(source_coords) + + print('compute energy exchange...') + set_progress_and_save(65, result_container, json_file_path) + radiosity.calculate_energy_exchange( + speed_of_sound=speed_of_sound, + etc_time_resolution=etc_time_resolution_s, + etc_duration=etc_duration_s, + max_reflection_order=max_reflection_order) + + print('collect energy at receiver...') + set_progress_and_save(90, result_container, json_file_path) + etc_radiosity = radiosity.collect_energy_receiver_mono( + receivers=receiver_coords, direct_sound=True) + + # apply sound power + etc_radiosity = etc_radiosity * sound_power_W + print('calculating room parameters and writing results...') + set_progress_and_save(95, result_container, json_file_path) + # Write results back to JSON + dynamic_range_db = 100 + + frequency_range = (float(np.min(frequencies)), float(np.max(frequencies))) + f_center, f_lower, f_upper = pf.constants.fractional_octave_frequencies_exact( + 1, frequency_range) + assert np.all(np.abs(f_center-frequencies)/frequencies < 1e-2) + for i_rec in range(n_receivers): + edc = etc_to_edc(etc_radiosity[i_rec, :], f_lower, f_upper) + edc_db = 10*np.log10(edc.time/1e-12) + limit = np.max(edc_db) - dynamic_range_db + edc_db[edc_db pf.TimeData: + """Convert energy time curve into energy decay curve. + + Parameters + ---------- + etc : pf.TimeData + energy time curve of cshape (..., n_bands). + lower_frequency_cutoffs : np.ndarray + lower cutoff frequencies from the frequency bands of shape (n_bands). + upper_frequency_cutoffs : np.ndarray + lower cutoff frequencies from the frequency bands of shape (n_bands). + + Results + ------- + edc : pf.TimeData + Resulting energy decay curve. + """ + full_frequency_range = np.max( + upper_frequency_cutoffs) - np.min( + lower_frequency_cutoffs) + bandwidth = upper_frequency_cutoffs - lower_frequency_cutoffs + + etc_eq = etc * (bandwidth/full_frequency_range) + edc = pyrato.edc.schroeder_integration(etc_eq, is_energy=True) + return edc + + +def _import_room_geometry(json_file_path, patch_length): + """Import room geometry and absorption coefficients. + + The geometry is read from a .geo file specified in the JSON input file. + The absorption coefficients are directly read from the JSON file. + + Parameters + ---------- + json_file_path : str + Path to the JSON file containing room geometry and absorption + coefficients. + + + Raises + ------ + ValueError + If absorption coefficients for any surface are not found in the + input JSON file. + """ + + with open(json_file_path, 'r') as f: + import json + input_data = json.load(f) + + frequencies = input_data['results'][0]['frequencies'] + n_bands = len(frequencies) + + # initialize gmsh and load the geometry file + gmsh.initialize() + geometry_file = input_data['geo_path'] + gmsh.open(geometry_file) + + # Read the content of the Geo file + with open(geometry_file, 'r') as file: + geo_content = file.readlines() + + # If an lc is given in the geo file, we want to compensate for this + lc_value = 1 # set to 1 by default + for line in geo_content: + if "lc =" in line: + lc_value = float(line.split('=')[1].strip().strip(';')) + print("Extracted value:", lc_value) + break + + gmsh.option.setNumber('Mesh.MeshSizeFactor', patch_length/lc_value) + + # generate 2d surface mesh + dim = 2 # 2D surfaces + gmsh.model.mesh.generate(dim) + + # get all named surfaces in the geometry + surface_group_tags = gmsh.model.getPhysicalGroups(dim=dim) + surface_group_names = [ + gmsh.model.getPhysicalName(dim, tag) + for (dim, tag) in surface_group_tags + ] + + # get all nodes of the surface mesh + node_tags_all, coords_all, _ = gmsh.model.mesh.getNodes() + coords = coords_all.reshape((len(node_tags_all), 3)) + + # get the material names from absorption coefficient input + absorption_names = list(input_data['absorption_coefficients'].keys()) + + # check if absorption coefficient data are available for all surfaces + for material_name in surface_group_names: + if material_name not in absorption_names: + raise ValueError( + "Absorption coefficients for surface " + f"'{material_name}' not found in input JSON file.") + + # create materials + alphas = [] + scatterings = [] + material_to_walls = [] + for material_name in absorption_names: + alphas.append(np.array(input_data['absorption_coefficients'][material_name])) + scatterings.append(np.ones_like(frequencies)) + + # materials + indies_material = [] + for ii, s_name in enumerate(surface_group_names): + if material_name == s_name: + indies_material.append(ii) + material_to_walls.append(indies_material) + + + # get the element type for surface mesh + element_type = gmsh.model.mesh.getElementType("Triangle", 1, True) + + room_center = np.mean(coords, axis=0) + + alphas = [] + walls_points = [] + walls_normal = [] + walls_up_vector = [] + patches_points = [] + n_patches = 0 + patch_to_wall_ids = [] + for i, surface_name in enumerate(surface_group_names): + dim_tags = gmsh.model.getEntitiesForPhysicalName(surface_name) + dim, tag = dim_tags[0] + + face_nodes = gmsh.model.mesh.getElementFaceNodes( + element_type, 3, tag=tag, ) + faces = np.reshape(face_nodes, (len(face_nodes) // 3, 3)) + + # extract wall information + mesh = trimesh.Trimesh(coords, faces-1) + wall_points = np.unique(mesh.bounding_box.vertices, axis=0, ) + wall_idx = [] + for p in wall_points: + wall_idx.append(np.argmin(np.sum(np.abs((mesh.vertices-p)), axis=1))) + wall_points = np.unique(mesh.vertices[wall_idx], axis=0, ) + + # flip normals to the center + wall_normal = np.median(mesh.face_normals, axis=0) + normal_dimension_mask = np.abs(wall_normal)>1e-3 + surface_center = np.mean(wall_points, axis=0) + pointing_inwards = np.sign((room_center-surface_center)[normal_dimension_mask]) == np.sign(wall_normal[normal_dimension_mask]) + if not np.all(pointing_inwards): + wall_normal *= -1 + elif not np.any(pointing_inwards): + raise ValueError('Flipping normals inwards did not work.') + + # calculate wall up vector + if np.abs(wall_normal[2]) > 1e-2: + wall_up_vector = [1, 0, 0] + else: + wall_up_vector = [0, 0, 1] + + walls_points.append(wall_points) + walls_normal.append(wall_normal) + walls_up_vector.append(wall_up_vector) + + # write patches + n_patches_wall = faces.shape[0] + for jj in range(n_patches_wall): + patch_to_wall_ids.append(i) + n_patches += n_patches_wall + patches_points.append(coords[faces-1, :]) + + alpha = np.array(input_data['absorption_coefficients'][surface_name].split(', '), dtype=float) + alphas.append(alpha) + + # finalizing gmsh + gmsh.finalize() + + # save wall information + walls_points = np.array(walls_points) + walls_normal = np.array(walls_normal) + walls_up_vector = np.array(walls_up_vector) + patches_points = np.concatenate(patches_points) + + return ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scatterings) + + +# copy pasted from pyrato +def center_time(energy_decay_curve): + r""" + Calculate the room-acoustic center time (:math:`T_s`). + + The center time :math:`T_s` is the time of the centroid of the squared + impulse response. It quantifies the balance between early and late + sound energy [#isoTs]_. + + The parameter is defined as + + .. math:: + + T_s = + \frac{ + \displaystyle \int_{0}^{\infty} t \cdot p^2(t)\,\mathrm{d}t + }{ + \displaystyle \int_{0}^{\infty} p^2(t)\,\mathrm{d}t + } + + where :math:`p(t)` is the room impulse response sound pressure. + + Using the energy decay curve :math:`e(t)`, the parameter can be + computed efficiently via the EDC identity as + + .. math:: + + T_s = + \frac{ + \displaystyle \int_{0}^{\infty} e(t)\,\mathrm{d}t + }{ + e(0) + }. + + Parameters + ---------- + energy_decay_curve : pyfar.TimeData + Energy decay curve of the room impulse response. The EDC must + start at time zero and must have equal time spacing. + + Returns + ------- + center_time : numpy.ndarray + Center time (:math:`T_s`) in seconds, + shaped according to the channel shape of the input EDC. + + References + ---------- + .. [#isoTs] ISO 3382, Acoustics — Measurement of the reverberation + time of rooms with reference to other acoustical parameters. + """ + + if not isinstance(energy_decay_curve, pf.TimeData): + raise TypeError( + "energy_decay_curve must be a pyfar.TimeData or derived object.") + + if not np.isclose(energy_decay_curve.times[0], 0.0): + raise ValueError("energy_decay_curve must start at time zero.") + + if np.any(energy_decay_curve.time[..., 0] == 0): + raise ValueError( + "Initial energy of energy_decay_curve must not be zero.") + + dt = np.diff(energy_decay_curve.times) + if not np.allclose(dt, dt[0]): + raise ValueError( + "energy_decay_curve must have equal time spacing.") + + sampling_interval = dt[0] + initial_energy = energy_decay_curve.time[..., 0] + center_time = ( + np.nansum(energy_decay_curve.time, axis=-1) + * sampling_interval + / initial_energy + ) + + return center_time \ No newline at end of file diff --git a/sparrowpy_method/tests/conftest.py b/sparrowpy_method/tests/conftest.py new file mode 100644 index 0000000..1b85a68 --- /dev/null +++ b/sparrowpy_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_sparrowpy.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_sparrowpy.geo") + msh_file = os.path.join( + default_data_path(), "test_room_sparrowpy.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_sparrowpy.geo") + input_tmp['msh_path'] = os.path.join( + tmpdirname, "test_room_sparrowpy.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("sparrowpy_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/sparrowpy_method/tests/test_definition.py b/sparrowpy_method/tests/test_definition.py new file mode 100644 index 0000000..59f388a --- /dev/null +++ b/sparrowpy_method/tests/test_definition.py @@ -0,0 +1,37 @@ +"""Test the SimulationMethod base class for sparrowpy method.""" +import pytest +from unittest.mock import patch +from pathlib import Path + +from sparrowpy_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/sparrowpy_method/tests/test_fixtures.py b/sparrowpy_method/tests/test_fixtures.py new file mode 100644 index 0000000..0eb2fde --- /dev/null +++ b/sparrowpy_method/tests/test_fixtures.py @@ -0,0 +1,23 @@ +"""Test fixtures for sparrowpy 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/sparrowpy_method/tests/test_input_sparrowpy.json b/sparrowpy_method/tests/test_input_sparrowpy.json new file mode 100644 index 0000000..19113b5 --- /dev/null +++ b/sparrowpy_method/tests/test_input_sparrowpy.json @@ -0,0 +1,57 @@ +{ + "geo_path": "test_room_sparrowpy.geo", + "msh_path": "test_room_sparrowpy.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": { + "speed_of_sound": 343.2, + "etc_time_resolution_s": 0.001, + "etc_duration_s": 0.1, + "max_reflection_order": 20, + "patch_length": 3, + "sound_power_W": 1 + }, + "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/sparrowpy_method/tests/test_room_sparrowpy.geo b/sparrowpy_method/tests/test_room_sparrowpy.geo new file mode 100644 index 0000000..db900ed --- /dev/null +++ b/sparrowpy_method/tests/test_room_sparrowpy.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/sparrowpy_method/tests/test_room_sparrowpy.msh b/sparrowpy_method/tests/test_room_sparrowpy.msh new file mode 100644 index 0000000..bbcdc1f --- /dev/null +++ b/sparrowpy_method/tests/test_room_sparrowpy.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 diff --git a/sparrowpy_method/tests/test_sparrowpy_cli.py b/sparrowpy_method/tests/test_sparrowpy_cli.py new file mode 100644 index 0000000..db45dac --- /dev/null +++ b/sparrowpy_method/tests/test_sparrowpy_cli.py @@ -0,0 +1,36 @@ +"""Test the sparrowpy method CLI.""" +import os +import json +import pytest + +from sparrowpy_interface import main, sparrowpyMethod + + +def test_sparrowpy_method_cli(mock_requests_post, create_temporary_input_file): + """Test the sparrowpy 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) + + # 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_sparrowpy_method_cli_missing_json_path(mock_requests_post): + """Test the sparrowpy 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/sparrowpy_method/tests/test_sparrowpy_interface_class.py b/sparrowpy_method/tests/test_sparrowpy_interface_class.py new file mode 100644 index 0000000..6d2100b --- /dev/null +++ b/sparrowpy_method/tests/test_sparrowpy_interface_class.py @@ -0,0 +1,70 @@ + +from sparrowpy_interface import sparrowpyMethod +import json +import sparrowpy +import numpy.testing as npt +import numpy as np + +from sparrowpy_interface.sparrowpy_interface import _import_room_geometry + + +def test_simple_method(create_temporary_input_file): + + sparrowpy_method_object = sparrowpyMethod(create_temporary_input_file) + sparrowpy_method_object.run_simulation() + + 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 len(results) > 0 + + # test no inf + for i in range(6): + assert np.min(results[i]['data']) > -np.inf + + + +def test_import_room_geometry(create_temporary_input_file): + ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering, + ) = _import_room_geometry( + create_temporary_input_file, patch_length=5) + + # create radiosity object + radiosity = sparrowpy.DirectionalRadiosityFast( + walls_points, + walls_normal, + walls_up_vector, + patches_points, + n_patches, + patch_to_wall_ids, + ) + + # check geometry stuff + radiosity.check() + + # check material stuff + npt.assert_equal(np.squeeze(material_to_walls), np.arange(6)) + npt.assert_equal(np.array(alphas).shape, (6, 6)) + npt.assert_equal(np.array(scattering).shape, (6, 6)) + npt.assert_array_less(np.array(alphas), 1) + npt.assert_allclose(np.array(scattering), 1) + + +def test_import_room_geometry_normals(create_temporary_input_file): + ( + walls_points, walls_normal, walls_up_vector, + patches_points, n_patches, patch_to_wall_ids, + material_to_walls, alphas, scattering, + ) = _import_room_geometry( + create_temporary_input_file, patch_length=5) + + npt.assert_almost_equal(walls_normal[0], [0, 0, 1]) # floor + npt.assert_almost_equal(walls_normal[2], [0, 0, -1]) # ceiling + npt.assert_almost_equal(walls_normal[3], [0, 1, 0]) # wall2 + npt.assert_almost_equal(walls_normal[4], [1, 0, 0]) # wall3 diff --git a/sparrowpy_method/todos.md b/sparrowpy_method/todos.md new file mode 100644 index 0000000..dfc53c8 --- /dev/null +++ b/sparrowpy_method/todos.md @@ -0,0 +1,10 @@ +# todos + +- [x] add progress bar +- [x] make normals point inside + + +## open + +- convex surfaces? +- \ No newline at end of file