Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions example_settings/pyroomacoustics_setting.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"type": "simulationSettings",
"options": [
{
"name": "Speed of sound",
"id": "c0",
"type": "float",
"display": "text",
Comment on lines +4 to +8
"min": 100,
"max": 500,
"default": 343,
"step": 1,
"endAdornment": "m/s"
},
{
"name": "Air density",
"id": "rho0",
"type": "float",
"display": "text",
"min": 0.001,
Comment on lines +15 to +20
"max": 3,
"default": 1.213,
"step": 0.001,
"endAdornment": "kg/m^3"
},
{
"name": "Image source order",
"id": "image_source_order",
"type": "integer",
"display": "text",
"min": 1,
"max": 1000,
"default": 2,
"step": 1
},
{
"name": "Sampling rate",
"id": "sampling_rate",
"type": "integer",
"display": "text",
"min": 1000,
"max": 192000,
"default": 8000,
"step": 1000,
"endAdornment": "Hz"
}
]
}
10 changes: 10 additions & 0 deletions methods-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,15 @@
"settings":"my_new_setting.json",
"repositoryURL":"",
"documentationURL":""
},
{
"simulationType": "Pyroomacoustics",
"containerImage": "pyroomacoustics_image:latest",
"envVars": {},
"label": "Pyroomacoustics",
"settings": "pyroomacoustics_setting.json",
"entryFile": "__main__.py",
"repositoryURL": "https://github.com/LCAV/pyroomacoustics",
"documentationURL": "https://pyroomacoustics.readthedocs.io"
}
]
36 changes: 36 additions & 0 deletions pyroomacoustics_method/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"gmsh",
"pyroomacoustics>=0.8.0",
"requests",
"pyfar>=0.8.0",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -73,3 +74,38 @@ pyroomacoustics_interface = "pyroomacoustics_interface:main"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 79
lint.ignore = [
"D200", # One-line docstring should fit on one line with quotes
"D202", # No blank lines allowed after function docstring
"D205", # 1 blank line required between summary line and description
"D401", # First line should be in imperative mood
"D404", # First word of the docstring should not be "This"
"B006", # Do not use mutable data structures for argument defaults
"B008", # Do not perform calls in argument defaults
"PT018", # Assertion should be broken down into multiple parts
"PT019", # Fixture `_` without value is injected as parameter

]
lint.select = [
"B", # bugbear extension
"ARG", # Remove unused function/method arguments
"C4", # Check for common security issues
"E", # PEP8 errors
"F", # Pyflakes
"W", # PEP8 warnings
"D", # Docstring guidelines
"NPY", # Check all numpy related deprecations
"D417", # Missing argument descriptions in the docstring
"PT", # Pytest style
"A", # Avoid builtin function and type shadowing
"ERA", # No commented out code
]

# Ignore missing docstrings in tests


[tool.ruff.lint.pydocstyle]
convention = "numpy"
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,49 @@
import gmsh
import numpy as np
import warnings
import os
from pathlib import Path
import pyfar as pf

# Support both package and script execution.
from .definition import SimulationMethod


class PyroomacousticsMethod(SimulationMethod):
def __init__(self, input_json_path: str | Path | None = None):
"""Interface class to run simulations using pyroomacoustics.

This interface class sets configures the simulation parameters,
source and receiver positions, as well as room geometry and boundary data.

On a successful simulation, the computed RIRs are exported to the input
data file.

Parameters
----------
input_json_path : str or Path, optional
Path to the simulation configuration file.

"""

def __init__(self, input_json_path: str | Path | None):
"""Initialize from configuration file.

Parameters
----------
input_json_path : str | Path | None, optional
The input configuration file path. Note that if ``None`` is
provided, the simulation will return an error. This input is only
allowed to support the case where the environment variable is not
set.
"""
super().__init__(input_json_path)

def run_simulation(self):
"""Run the simulation method for pyroomacoustics based on the JSON file.
def run_simulation(self) -> None:
"""Execute the simulation and export results to the configuration file.
"""

print("pyroomacoustics_method: starting simulation")


walls = import_room_geometry(self.input_json_path)

simulation_setup = setup_simulation(self.input_json_path, walls)
Expand All @@ -37,12 +63,12 @@ def run_simulation(self):
print("pyroomacoustics_method: simulation done!")


def read_json_input(json_file_path):
"""Read the input JSON file.
def read_json_input(json_file_path: str | Path) -> dict:
"""Read the input JSON file and return the content as a dictionary.

Parameters
----------
json_file_path : str
json_file_path : str | Path
Path of the input JSON file

Returns
Expand All @@ -58,15 +84,15 @@ def read_json_input(json_file_path):
return input_data


def import_room_geometry(json_file_path):
def import_room_geometry(json_file_path: str | Path) -> list[pra.Wall]:
"""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
json_file_path : str | Path
Path to the JSON file containing room geometry and absorption
coefficients.

Expand All @@ -87,11 +113,46 @@ def import_room_geometry(json_file_path):
import json
input_data = json.load(f)

frequencies = input_data['frequencies']
n_bands = len(frequencies)

# initialize gmsh and load the geometry file
gmsh.initialize()
try:
walls = _import_room_geometry(input_data)
finally:
gmsh.finalize()

return walls


def _import_room_geometry(input_data: dict) -> list[pra.Wall]:
"""Private import class for geometry and boundary conditions.

This private function ensures that gmsh is properly finalized after
geometry import, even if an error occurs during the import process.

The public function `import_room_geometry` is responsible for initializing
and finalizing gmsh.

Parameters
----------
input_data : dict
The input configuration data.

Returns
-------
list[pra.Wall]
List of walls defining the room geometry and boundary conditions for
all frequency bands.

Raises
------
ValueError
If absorption coefficients for any surface are not found in the
input JSON file.
"""


frequencies = input_data['results'][0]['frequencies']
geometry_file = input_data['geo_path']
gmsh.open(geometry_file)

Expand Down Expand Up @@ -138,11 +199,26 @@ def import_room_geometry(json_file_path):
element_type, 3, tag=tag)
faces = np.reshape(face_nodes, (len(face_nodes) // 3, 3))

absorption_coeffs_config = input_data[
'absorption_coefficients'][surface_name]
if isinstance(absorption_coeffs_config, str):
absorption_coeffs = np.array(
[
float(x.strip())
for x in absorption_coeffs_config.split(",")
],
dtype=float)
elif isinstance(absorption_coeffs_config, list):
absorption_coeffs = np.array(
absorption_coeffs_config, dtype=float)
elif isinstance(absorption_coeffs_config, np.ndarray):
absorption_coeffs = absorption_coeffs_config.astype(float)

material = pra.Material(
energy_absorption={
'description': surface_name,
'center_freqs': input_data['frequencies'],
'coeffs': input_data['absorption_coefficients'][surface_name],
'center_freqs': frequencies,
'coeffs': absorption_coeffs,
Comment on lines 217 to +221
}
)

Expand All @@ -155,13 +231,10 @@ def import_room_geometry(json_file_path):
for face in faces
)

# finalizing gmsh
gmsh.finalize()

return walls


def get_source_positions(input_data):
def get_source_positions(input_data: dict) -> np.ndarray:
"""Extract source positions from input data.

Parameters
Expand All @@ -181,7 +254,7 @@ def get_source_positions(input_data):
])


def get_receiver_positions(input_data):
def get_receiver_positions(input_data: dict) -> np.ndarray:
"""Extract receiver positions from input data.

Parameters
Expand Down Expand Up @@ -211,7 +284,7 @@ def get_receiver_positions(input_data):
return receiver_pos


def set_default_simulation_settings(input_data):
def set_default_simulation_settings(input_data: dict) -> dict:
"""Set default simulation settings if not provided in input data.

Parameters
Expand Down Expand Up @@ -260,7 +333,10 @@ def set_default_simulation_settings(input_data):
return input_data


def setup_simulation(json_file_path, walls):
def setup_simulation(
json_file_path: str | Path,
walls: list[pra.Wall]
) -> pra.Room:
"""Set up the pyroomacoustics simulation based on the JSON file.

Parameters
Expand All @@ -282,10 +358,18 @@ def setup_simulation(json_file_path, walls):
input_data = read_json_input(json_file_path)
extended_input_data = set_default_simulation_settings(input_data)

sampling_rate = extended_input_data['simulationSettings'].get('sampling_rate')
image_source_order = extended_input_data['simulationSettings'].get('image_source_order')
ray_tracing = extended_input_data['simulationSettings'].get('ray_tracing')
air_absorption = extended_input_data['simulationSettings'].get('air_absorption')
sampling_rate = extended_input_data["simulationSettings"].get(
"sampling_rate"
)
image_source_order = extended_input_data["simulationSettings"].get(
"image_source_order"
)
ray_tracing = bool(
extended_input_data["simulationSettings"].get("ray_tracing")
)
air_absorption = bool(
extended_input_data["simulationSettings"].get("air_absorption")
)

Comment on lines +370 to 373
room = pra.Room(
walls,
Expand All @@ -295,6 +379,19 @@ def setup_simulation(json_file_path, walls):
air_absorption=air_absorption,
)

frequencies = input_data["results"][0]["frequencies"]

room.octave_bands.base_freq = frequencies[0]
room.n_octave_bands = len(frequencies)

alpha, m_pyfar, _ = pf.constants.air_attenuation(
20,
frequencies,
relative_humidity=50/1e2)
m = np.squeeze(m_pyfar.freq)

room.air_absorption = m

# Add sources
source_pos = get_source_positions(input_data)
if source_pos.shape != (3,):
Expand All @@ -309,13 +406,16 @@ def setup_simulation(json_file_path, walls):
return room


def export_rir_to_input(json_file_path, rir):
def export_rir_to_input(
json_file_path: str | Path,
rir: list[list[np.ndarray]]
) -> None:
"""Export the computed RIRs to the input data structure.

Parameters
----------
input_data : dict
Input data as a dictionary.
json_file_path : str | Path
Path to the input JSON file.
rir : list of list of np.ndarray
Computed RIRs from pyroomacoustics.

Comment on lines +409 to 421
Expand All @@ -328,10 +428,8 @@ def export_rir_to_input(json_file_path, rir):
import json
input_data = json.load(f)

# num_receivers = len(input_data['results'][0]['responses'])

# for i in range(num_receivers):
input_data['results'][0]['responses'][0]['receiverResults'] = rir.tolist()
input_data["results"][0]["percentage"] = 100

with open(json_file_path, 'w') as f:
json.dump(input_data, f, indent=4)
Loading