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 less computationally intesive, 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(category="Wood")
perforated_panel = tsdk.material_library.get(category="Perforated panels")
acoustic_ceiling = tsdk.material_library.get(category="Porous")
gypsum = tsdk.material_library.get(category="Gypsum")
rigid = tsdk.material_library.get(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. Cardioid and dipole sources
  3. Directive sources

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

The SDK includes a number of predefined directive sources that can be accessed through the source_directivity_library.

Users can also upload their own source directivities to the source_directivity_library by supplying a .clf file to define the directivity. Uploaded source directivites are shared across all projects within an organization.

For now lets use a source directivity included with the SDK

# Lets find the Genelec 8020A loudspeaker directivity.
directivity = tsdk.source_directivity_library.query(name='8020')
dd.as_table(directivity)
genelec = directivity[0]

It's possible to visualize the directivity pattern using the plot functions of the directivity object.

genelec.plot_spl_on_axis()
genelec.plot_directivity_pattern()

Next we define our source with the directivity pattern.

source_type = treble.SourceType.directive
source_properties = treble.SourceProperties(
azimuth_angle=90.0,
elevation_angle=10.0,
source_directivity=genelec
)
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 (DRTF import is covered in another tutorial).
  2. After doing a simulation with a device/spatial receiver, the device rendering can be done retrospectively in the SDK (Local device rendering is covered 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.
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" / "TutorialSimulation"
simulation.download_results(destination_directory=download_directory)

Simulation objects and Simulation Definitions

It is crucial to understand the difference between Simulation objects and SimulationDefinition objects.

SimulationDefinition:

  • Contains all the data needed to create a Simulation, acting as a blueprint.
  • Can be used to create multiple Simulations in Treble.
  • Cannot be started, does not contain any results, and does not exist on Treble's servers.
  • Exists only in your local Python session.
  • Can be edited.

Simulation:

  • Created from a SimulationDefinition.
  • Can be started, can have results, and exists on Treble's servers.
  • It's properties (name, type, ir length, sources, receivers, etc) is considered read-only after creation.

The following code attempts to clarify some of these differences:

# Lets display what simulations are available at each step:
print("Initial list of simulations available at Treble")
dd.as_table(p.get_simulations())

# Lets create our simulation definition again.
sim_def = treble.SimulationDefinition(
name="Test simulation",
simulation_type=treble.SimulationType.ga,
model=room,
crossover_frequency=720,
ir_length=0.1,
receiver_list=[rec_1, rec_2],
source_list=[source],
material_assignment=material_assignment
)

# A simulation has not been created at Treble since a SimulationDefinition is just a local data structure.
print("A simulation has not been created at Treble since a SimulationDefinition is just a local data structure.")
dd.as_table(p.get_simulations())

# Using this simulation definition we can create a simulation.
sim = p.add_simulation(sim_def)

# Now we have create a simulation at Treble and it will show up in the list of simulations for the project.
dd.as_table(p.get_simulations())
# The simulation can interacted with, f.ex. started, results downloaded, etc. 
# But any properties of the simulation cannot be changed.

# Let's say that you forgot to change the IR length from 0.1 to 0.5.
# We can do this by removing the original simulation, updating the definition and creating it again.
sim.delete()
# It is good practice to remove the variable assignment.
sim = None

# The simulation should now be removed from the list.
dd.as_table(p.get_simulations())

# Edit the IR length in the simulation definition.
sim_def.ir_length = 0.5
sim = p.add_simulation(sim_def)
# Now we have a simulation with 0.5 IR length.
dd.as_table(p.get_simulations())
# Lets say we want to create a simulation based on the one we just created but change the position of receiver "01".
# We can get a SimulationDefinition object with the data from sim using the get_simulation_definition method.
sim_def2 = sim.get_simulation_definition()

sim_def2.name = "Test simulation 2"
sim_def2.receiver_list[0]['x'] = 1.5
sim2 = p.add_simulation(sim_def2)

dd.as_table(p.get_simulations())