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