Skip to main content

Simulating many rooms

The Treble SDK makes it easy to generate large, diverse, and high quality room-acoustic datasets. This page demonstrates the integration of several methods from the Treble SDK into large-scale dataset generation.

Generating rooms

The Treble SDK includes multiple methods to automatically generate rooms with different shapes, and generate multiple rooms in a randomized way. Here, we will use these functions to set up a total of 200 different rooms, of which 40 each are shoebox, L-shaped, T-shaped, U-shaped, and angled shoebox rooms, respectively.

generated_room_defs = {}
n_rooms_per_shape = 40

# Generate shoebox rooms
generated_room_defs["Shoebox"] = treble.GeometryDefinitionGenerator.create_random_shoebox_rooms(
n_rooms=n_rooms_per_shape, x_range=(2, 5), y_range=(2, 5), z_range=(2, 3), join_wall_layers=False
)

# Generate L-shaped rooms
generated_room_defs["L"] = treble.GeometryDefinitionGenerator.create_random_L_shaped_rooms(
n_rooms=n_rooms_per_shape,
a_side_range=(2, 4),
b_side_range=(2, 4),
c_side_range=(2, 3),
d_side_range=(2, 3),
height_range=(2, 3),
join_wall_layers=False,
)

# Generate T-shaped rooms
generated_room_defs["T"] = treble.GeometryDefinitionGenerator.create_random_T_shaped_rooms(
n_rooms=n_rooms_per_shape,
a_side_range=(1.5, 3),
b_side_range=(2, 4),
c_side_range=(1.5, 3),
d_side_range=(1.5, 3),
height_range=(2, 3),
join_wall_layers=False,
)

# Generate U-shaped rooms
generated_room_defs["U"] = treble.GeometryDefinitionGenerator.create_random_U_shaped_rooms(
n_rooms=n_rooms_per_shape,
a_side_range=(1.5, 3),
b_side_range=(1.5, 3),
c_side_range=(1.5, 3),
d_side_range=(1.5, 3),
height_range=(2, 3),
join_wall_layers=False,
)

# Generate angled shoebox rooms
generated_room_defs["angled"] = treble.GeometryDefinitionGenerator.create_random_angled_shoebox_rooms(
n_rooms=n_rooms_per_shape,
base_side_range=(2, 5),
depth_range=(2, 4),
left_angle_range=(75, 105),
right_angle_range=(75, 105),
height_range=(2, 3),
join_wall_layers=False,
)

Randomly placing furniture

Although the Treble SDK includes methods to manually inject geometry objects like furniture into rooms, this procedure can become difficult and time consuming for large datasets. Instead, we will use the random injection of objects to randomly populate all 200 generated rooms with furniture.

First, we specify which furniture objects we want to add, and how those should be oriented. For example, we want to add tables and ensure that they are aligned with the coordinate axes, corresponding to rotation angles that are multiples of 90 degrees. At the same time, we specify minimum distances from other objects and walls to avoid intersections with them.

# When randomly placing geometry components, rotation_settings are specified as a ComponentAnglePool.
# A ComponentAnglePool specifies which orientations a geometry component can have.
# If no rotation_settings is given, the component will be placed with a random orientation.
whole_angles = treble.ComponentAnglePool([0, 90, 180, 270])
half_angles = treble.ComponentAnglePool([0, 45, 90, 135, 180, 225, 270, 315])

# Define the objects and quantities that we want to place in the rooms.
placements = [
# We want 2 tables, and they can be any table from the library.
# preferred_count is the max count of the component that will be placed.
# This does not guarantee that this many objects will appear, for example if space is limited.
treble.GeometryComponentPlacement(
components=tsdk.geometry_component_library.query(group="table"),
preferred_count=2,
rotation_settings=whole_angles,
min_dist_from_objects=0.25,
min_dist_from_walls=0.25,
),
# 4 chairs.
treble.GeometryComponentPlacement(
components=tsdk.geometry_component_library.query(group="chair"),
preferred_count=2,
rotation_settings=half_angles,
min_dist_from_objects=0.25,
min_dist_from_walls=0.25,
),
# 1 sofa.
treble.GeometryComponentPlacement(
components=tsdk.geometry_component_library.query(group="sofa"),
preferred_count=1,
rotation_settings=whole_angles,
min_dist_from_objects=0.25,
min_dist_from_walls=0.25,
),
# 2 big boxes.
treble.GeometryComponentPlacement(
components=treble.GeometryComponentGenerator.create_box(
treble.BoundingBox.from_points(-0.5, 0.5, -0.5, 0.5, 0, 1)
),
preferred_count=2,
# You can specify minimum distance from other components and walls.
min_dist_from_objects=0.5,
min_dist_from_walls=0.5,
),
# 5 small boxes.
treble.GeometryComponentPlacement(
components=treble.GeometryComponentGenerator.create_box(
treble.BoundingBox.from_points(-0.1, 0.1, -0.1, 0.1, 0, 0.2)
),
preferred_count=5,
min_dist_from_objects=0.02,
),
]

Once we are happy with the GeometryComponentPlacement for all furniture objects, we can populate the empty rooms using populate_with_geometry_components(). The furnished room can then be added to the project with project.add_model() to make it usable in the simulations.

from tqdm import tqdm 

generated_rooms = {}
for room_type in generated_room_defs:
print(f"==== Populating {room_type} rooms with objects. ====")
for room_idx, room in enumerate(tqdm(generated_room_defs[room_type])):
# Populate with objects
room.populate_with_geometry_components(
components=placements, selection_algorithm=treble.ComponentSelectionAlgorithm.random
)

# Add room to project
model = project.add_model(f"{room_type}_{room_idx}", room)
generated_rooms[model.id] = model

Randomly placing sources and receivers

For large datasets, sources and receivers are often placed randomly in rooms. However, with full randomization, sources or receivers could accidentally be placed inside furniture objects or too close to surfaces. Therefore, the Treble SDK includes methods to generate random valid positions or valid grids, while maintaining specifiable minimum distances to walls, furniture, or other sources and receivers.

We start by setting up three directional sources with a speech directivity pattern per room. They will be placed at random valid points using the method generate_valid_points() from the PointsGenerator class. We set the following rules with a PointRuleset:

  • All sources must have a distance of at least 0.50.5 m from all surfaces in the model including furniture objects.
  • All sources must have a distance of at least 0.50.5 m from each other.
info

Note: generate_valid_points() aims to generate the desired number of points. However, if the specified rules are too strict, or the model is too small, the function may not succeed and will return fewer points instead.

The source orientation (azimuth and elevation) is randomly drawn from uniform distributions in this example.

import numpy as np

# Speech sources should be 0.5 m away from surfaces, and 0.5 m from each other
pg = treble.PointsGenerator()
sources_ruleset = treble.PointRuleset(min_dist_from_surface=0.5, min_dist_from_other_points=0.5)

# Get speech directivity
speech_directivity = tsdk.source_directivity_library.query(name="Speech")[0]

speech_sources = {}
speech_sources_pos = {}

for room in generated_rooms.values():
# Generate valid points
valid_points = pg.generate_valid_points(
model=room,
max_count=3,
ruleset=sources_ruleset,
z_range=(0.5, 1.5),
)
speech_sources_pos[room.id] = valid_points

sources_this_room = [
treble.Source.make_directive(
position=pos,
orientation=treble.Rotation(
azimuth=np.random.uniform(-180, 180), elevation=np.random.uniform(-30, 30)
),
label=f"Speech_Src{idx}",
source_directivity=speech_directivity,
)
for idx, pos in enumerate(valid_points)
]

speech_sources[room.id] = sources_this_room

After setting up the sources of our simulation, we will proceed and place a uniform grid of receivers around them throughout the room. We will use spatial receivers with Ambisonics order 2, but the same functionality can be used for any receiver type.

To this end, we will use the method generate_valid_grid() from the PointsGenerator class. As our dataset includes rooms of different sizes, we will use a variable grid resolution on the xx-yy-plane. If a 11 m grid resolution does not yield any valid points, we will halve the resolution until it is dense enough for the room size to yield any. Along the zz-axis, our grid will have a 0.50.5 m resolution, and it will only cover the range from z=0.5z=0.5 until z=1.5z=1.5. Finally, we will also enforce the following rules:

  • For a given grid resolution on the xx-yy-plane, all grid points must be at least half of that distance away from all surfaces in the room including furniture objects.
  • For a given grid resolution on the xx-yy-plane, all grid points must be at least half of that distance away from the previously generated sound sources.
receivers = {}
for room in tqdm(generated_rooms.values()):
# Generate valid grid
res = 1
receiver_pos = []
while not receiver_pos:
receiver_pos = pg.generate_valid_grid(
model=room,
x_res=res,
y_res=res,
z_res=0.5,
z_range=(0.5, 1.5 + 1e-15),
existing_points=speech_sources_pos[room.id],
min_dist_surf=0.5 * res,
min_dist_existing_points=0.5 * res,
)

res /= 2

# Create Treble Receiver objects
receivers_this_room = [
treble.Receiver.make_spatial(position=pos, ambisonics_order=2, label=f"Grid_Rcv{idx}")
for idx, pos in enumerate(receiver_pos)
]

receivers[room.id] = receivers_this_room

Assigning materials

The Treble SDK allows users to freely assign individual materials to all model surfaces including furniture objects. In this example, we will randomly assign materials from the material library such that the Sabine estimate for the model averaged over all octave bands lies between 0.40.4 s and 11 s. For applications, in which the user requires more flexibility, custom materials can also be defined from absorption coefficients, reflection coefficients, or impedances.

import random

sabine_range = (0.4, 1.0)

# Get list with all materials
materials = tsdk.material_library.get()

# Remove materials that are user-generated
database_materials = [material for material in materials if material["organizationId"] == None]

material_assignments = {}
sabine_estimates = {}
for room in tqdm(generated_rooms.values()):
# Assign random materials until average Sabine estimate is inside the specified range
successful_assignment = False
while not successful_assignment:
# Wait for model to finish processing
__ = room.wait_for_model_processing()

# Random material assignment
material_assignment = [
treble.MaterialAssignment(layer, random.choice(database_materials)) for layer in room.layer_names
]

# Calculate Sabine estimate for random materials
sabine_est = room.calculate_sabine_estimate(material_assignment)

# If average over frequency bands is within range specified above, assignment was successful
if np.mean(sabine_est) >= sabine_range[0] and np.mean(sabine_est) <= sabine_range[1]:
successful_assignment = True

material_assignments[room.id] = material_assignment
sabine_estimates[room.id] = np.mean(sabine_est)

Running simulations and working with results

Now that we set up room models, sources, receivers, and material assignments, we can combine everything and initialise the simulations. We will set up hybrid simulations with a crossover frequency of 500500 Hz.

sim_type = treble.SimulationType.hybrid
crossover_frequency = 500

# Set up simulations with generated rooms
sim_defs = []
for room in generated_rooms.values():
sim_def = treble.SimulationDefinition(
name=room.name,
simulation_type=sim_type,
model=room,
crossover_frequency=crossover_frequency,
ir_length=sabine_estimates[room.id],
receiver_list=receivers[room.id],
source_list=speech_sources[room.id],
material_assignment=material_assignments[room.id],
simulation_settings=treble.SimulationSettings(ambisonics_order=2),
)

sim_defs.append(sim_def)

# Add simulations to project
sims_generated = project.add_simulations(sim_defs)

We can start all simulations at the same time:

project.start_simulations()

This will start as many simulations as our concurrency tier allows, while queuing the remaining ones.

Once the simulation have completed, the results can be downloaded:

from pathlib import Path

data_dir = Path("data")
project.download_results(data_dir, rename_rule=treble.ResultRenameRule.by_label)

We can then continue and plot the results for one of the simulations:

results_sim0 = sims_generated[0].get_results_object(data_dir / sims_generated[0].name)
results_sim0.plot()