Skip to main content

First Simulation

The following documentation is presented as Python code running inside a Jupyter Notebook. To run it yourself you can copy/type each individual cell or directly download the full notebook, including all required files.

In the previous tutorials we have created a project, defined a geometry and attached materials to the surfaces of that geometry. Then we are ready to run our first simulation. There are still a few things to consider and a few options to choose from in terms of source types, receiver types, simulation types and crossover frequencies.

A few things to keep in mind

  • The wave based solver is the most accurate solver
  • As frequency increases, it becomes more computationally demanding. Scaling roughly by frequency to the power of four.
  • However, the geometrical acoustics solver, which is in general much cheaper to use, becomes more accurate as frequency is increased.
  • The wave based solver is the most accurate solver but as frequency increases, the assumptions behind geometrical acoustics become closer to being valid.

Lets first define the project we're are working on and re-define what we have done in the previous notebooks and then we can move on to defining our source.

from pathlib import Path
import random

from treble_tsdk.tsdk import TSDK
from treble_tsdk import tsdk_namespace as treble
from treble_tsdk import display_data as dd

tsdk = TSDK()
project_name = "Tutorial"
p = tsdk.get_or_create_project(name=project_name)

room = treble.GeometryGenerator.create_l_shaped_room(
project=p,
model_name="l-shaped",
a_side=3,
b_side=3,
c_side=1,
d_side=1,
height_z=2.2,
join_wall_layers=False,
)

wooden = tsdk.material_library.get_by_category("Wood")
perforated_panel = tsdk.material_library.get_by_category("Perforated panels")
acoustic_ceiling = tsdk.material_library.get_by_category("Porous")
gypsum = tsdk.material_library.get_by_category("Gypsum")
rigid = tsdk.material_library.get_by_category("Rigid")

material_assignment = [
treble.MaterialAssignment(layer_name="lshape_wall_0", material=random.choice(rigid), scattering_coefficient=0.15),
treble.MaterialAssignment(layer_name="lshape_wall_1", material=random.choice(gypsum), scattering_coefficient=0.1),
treble.MaterialAssignment(layer_name="lshape_wall_2", material=random.choice(gypsum), scattering_coefficient=0.1),
treble.MaterialAssignment(layer_name="lshape_wall_3", material=random.choice(perforated_panel), scattering_coefficient=0.2),
treble.MaterialAssignment(layer_name="lshape_wall_4", material=random.choice(gypsum), scattering_coefficient=0.1),
treble.MaterialAssignment(layer_name="lshape_wall_5", material=random.choice(gypsum), scattering_coefficient=0.1),
treble.MaterialAssignment(layer_name="lshape_floor", material=random.choice(wooden), scattering_coefficient=0.1),
treble.MaterialAssignment(layer_name="lshape_ceiling", material=random.choice(acoustic_ceiling), scattering_coefficient=0.15),
]

Source Types


Currently we offer the possibility to model a few different kinds of source patterns:

  1. Omnidirectional source
  2. Directive speech source
  3. Directive loudspeaker source (Genelec 8020)

The directive sources can then be rotated with azimuth (counter-clockwise) and elevation angles in degrees.

We will add to the source library soon and also include the option of uploading a .clf file to define directivity.

For now, let us define a directive speech source.

source_type = treble.SourceType.directive_speech
source_properties = treble.SourceProperties(azimuth_angle=90.0, elevation_angle=10.0)
source = treble.Source(
x=1,
y=1,
z=1.5,
source_type=source_type,
label="Source_1",
source_properties=source_properties
)

Receiver Types


The Treble SDK offers three possible receiver types to store the wavefield in certain locations within the domain. This can be done, either directly with a point receiver (mono receiver), or by recording the ambisonics response of the neighbouring domain around the receiver (spatial receiver).

The result from the spatial receiver can be used to render multi-channel signals such as the binaural response or how a device with a microphone array would record the wavefield. There are a few ways that this can be done.

  1. The third receiver type is the device receiver which renders certain signals from the spatial receiver using a Device Related Transfer Function (DRTF) which can be imported with a .sofa file (more on that in another tutorial).
  2. After doing a simulation with a device/spatial receiver, the device rendering can be done retrospectively in the SDK (more on that in another tutorial).
  3. Rendering custom signals can be done manually by downloading the ambisonics channels of the spatial receiver and rendering the signals with some custom scripts.

For now, we can define a few mono receivers in the domain and later we'll plot the simulated impulse responses for those receivers.

receiver_type = treble.ReceiverType.mono
rec_1 = treble.Receiver(x=1, y=1.5, z=1, receiver_type=receiver_type, label="01")
rec_2 = treble.Receiver(x=1.5, y=3.5, z=1.5, receiver_type=receiver_type, label="02")

Simulation Definition


We have now defined all that needs to be defined for a simulation and we can thus move on and create a simulation.

A simulation in the Treble SDK is created using the SimulationDefinition class and an instance (or a list of instances) of that class can be added to the project.

The SimulationDefinition class takes in a few inputs which we have yet to define though

  • name: Basically just for bookkeeping and making it simpler to find the simulation again.
  • simulation_type: Defines which solver(s) to use. Can be either pure wave-based (DG), pure geometrical (GA), or a hybrid of both.
  • crossover_frequency: Is the maximum resolved frequency in a wave-based simulation. Either where the GA results will take over (hybrid simulation) or where the upper frequency limit of the end result lies (wave-based simulation).
  • ir_length/energy_decay_threshold: Refer to the termination criteria of the simulation. ir_length refers to how many seconds the resulting impulse response should be and the solvers will output exactly that length of IRs. energy_decay_threshold however, stops the simulations once the energy at the receiver locations has dropped below a certain threshold.

One thing to note is that to obtain exact impulse responses of the sources, we run a single simulation for each source. The cost of the simulations thus scales linearly with the number of sources in the simulation. The cost of receivers is however, negligible.

Now, let's define a simulation

sim_def = treble.SimulationDefinition(
name="My simulation",
simulation_type=treble.SimulationType.dg,
model=room,
crossover_frequency=720,
ir_length=0.1,
receiver_list=[rec_1, rec_2],
source_list=[source],
material_assignment=material_assignment
)

The simulation definition can be plotted to visualize source-receiver positions and there are simulation validation functions which provide warnings if the setup can potentially give weird results or even not run.

validation = sim_def.validate()
dd.as_tree(validation)

sim_def.plot()

Once we are happy with the simulation, we can add it to the project. As we add the simulation to the project we get a Simulation object which can be used to interact with the simulation, using methods like

  • Simulation.estimate() to estimate the cost of the simulation, or Simulation.wait_for_estimate() to wait until the model has been meshed and then return an estimated cost once it's available
  • Simulation.start() to run the simulation
  • Simulation.get_progress() to get an object describing the progress of the simulation, this can then be viewed in a table or tree
  • Simulation.as_live_progress() to follow the progress of the simulation live
  • Simulation.cancel() to cancel a simulation.
  • Simulation.download_results() to retrieve the results of the simulation to work with locally.

We are also working on a results module where you can work with the output directly

simulation = p.add_simulation(definition=sim_def)

The Simulation object is now stored in simulation and we can use it to initialize it and follow it's progress.

It's important to note that although a simulation is running, you are not bound to wait for it to finish. You can close this notebook and retrieve the relevant Simulation object later to retrieve the results. This is all happening in the cloud, putting no constraints on your own computer (aside from some initial uploading to the cloud).

simulation.start()
simulation.as_live_progress()
download_directory = Path.home() / "TSDK" / "TutorialSimualation"
simulation.download_results(destination_directory=download_directory)