Noise Propagation Simulation

This section demonstrates how to simulate the propagation of noise in an urban environment using a point source, obstacles (e.g., buildings), and optional vegetation (e.g., trees). The simulate_noise function models the attenuation of noise based on geometry, absorption, reflections, and environmental parameters.

# Install required packages (uncomment if needed)
# !pip install pyarrow objectnat
# Import necessary libraries
import geopandas as gpd
from shapely.geometry import Point
from objectnat import simulate_noise

1. Define Noise Source

# Define the starting point(s) of the noise source, and their parameters 

start_p = gpd.GeoDataFrame(data=[[90, 3000], [80, 200]],
                           geometry=[Point(30.27060176, 59.93546846), Point(30.27213917, 59.93575345)],
                           columns=['source_noise_db', 'geometric_mean_freq_hz'], crs=4326)

2. Load Obstacle and Tree Data

Load obstacle (building) and tree layers from local files, and project them to the same CRS used for simulation.

# Load obstacle geometries (e.g., buildings)
obstacles = gpd.read_parquet('examples_data/buildings.parquet')

# Load vegetation geometries (e.g., trees)
trees = gpd.read_parquet('examples_data/trees.parquet')

3. Run Noise Simulation

Simulate the propagation of noise using a point source with specified parameters. The simulation accounts for:

  • Obstacles and their absorption

  • Tree-based scattering

  • Environmental factors like air temperature

  • Sound reflection and attenuation with distance

# Run the simulation
noise = simulate_noise(
    source_points=start_p,
    obstacles=obstacles,
    # Alternatively use these args if not specified per-point
    # source_noise_db=90,  # Initial noise level in decibels
    # geometric_mean_freq_hz=2000,  # Frequency of the noise signal
    standart_absorb_ratio=0.05,  # Default absorption coefficient for obstacles
    trees=trees,  # Vegetation data
    tree_resolution=4,  # Resolution of vegetation scattering
    air_temperature=20,  # Air temperature in °C
    target_noise_db=40,  # Simulation stops at this minimum noise level
    db_sim_step=1,  # Step size in decibel for simulation granularity
    reflection_n=2,  # Number of allowed reflections
    dead_area_r=5  # Radius of reflection-free dead zones (in meters)
)

4. Visualize the Result

Visualize the noise propagation result on a map using a color scale that reflects noise levels (in dB).

# Visualize the result using the 'RdYlGn_r' colormap and a fixed lower bound
noise.explore(column='noise_level', cmap='RdYlGn_r', vmin=40, tiles='cartodb positron')

5. Create a Simplified Noise Frame (No Full Simulation)

This section builds a simplified noise exposure map (noise “frame”) across a territory using only geometric visibility and sound decay, without performing full wave-based simulation.

Here’s what happens step-by-step:

  1. A 500-meter buffer area is created around a central point of interest.

  2. A drive street graph is downloaded and converted into a GeoDataFrame.

  3. From the surrounding buildings, a random subset is sampled to act as noise sources (e.g., noisy buildings).

  4. Sound source attributes (noise level and frequency) are assigned to graph edges and buildings.

  5. All sources are combined into a single GeoDataFrame and passed to calculate_simplified_noise_frame.

  6. The result is a noise-level envelope (polygon map), where each area shows the maximum estimated noise level.

# Install package for getting drive graph (uncomment if needed)
# !pip install iduedu
from iduedu import get_drive_graph
from objectnat import graph_to_gdf, calculate_simplified_noise_frame
import pandas as pd

# Define the center of the area of interest
mid_point = gpd.GeoSeries([Point(30.2706, 59.9354)], crs=4326)

# Create a 500-meter buffer around the center
area_of_interest = mid_point.to_crs(32636).buffer(500)

# Get drivable road network and convert it to GeoDataFrame
polygon4326 = area_of_interest.to_crs(4326).iloc[0]
local_drive_graph = get_drive_graph(polygon=polygon4326, retain_all=True)
graph_edges = graph_to_gdf(local_drive_graph, edges=True, nodes=False).to_crs(start_p.crs)

# Clip buildings to the area and randomly sample 2% of them as "noisy buildings"
obstacles_in_area = obstacles.clip(area_of_interest, keep_geom_type=True)
sample_size = max(1, int(0.02 * len(obstacles_in_area)))
obstacles_in_area = obstacles_in_area.sample(n=sample_size, random_state=42).to_crs(start_p.crs)
obstacles_in_area["source_noise_db"] = 90

# Assign road edges as line sources of noise
graph_edges["source_noise_db"] = 75

# Combine all sources into one GeoDataFrame
noise_sources = pd.concat([start_p, graph_edges, obstacles_in_area], ignore_index=True)
noise_sources["geometric_mean_freq_hz"] = 2000

# Calculate simplified noise distribution (set use_tqdm=False to disable progress bar)
noise_frame = calculate_simplified_noise_frame(
    noise_sources,
    obstacles,
    air_temperature=20,
)
# Visualize simplified noise frame
noise_frame.explore(column='noise_level', cmap='RdYlGn_r', vmax=120, vmin=40, tiles='cartodb positron')

Section for GIF creation

import matplotlib.pyplot as plt
import matplotlib.animation as animation

import numpy as np
from matplotlib import cm


def create_noise_animation(gdf_noise, gdf_obstacles, start_p, buffer_p, gdf_trees=None,
                           output_file="noise_animation.gif"):
    if gdf_trees is None:
        gdf_trees = gpd.GeoDataFrame()

    bounds = start_p.unary_union.buffer(buffer_p).bounds
    minx, miny, maxx, maxy = bounds
    vmin = gdf_noise['noise_level'].min()
    vmax = gdf_noise['noise_level'].max()
    cmap = cm.plasma

    fig, ax = plt.subplots(figsize=(10, 10))
    plt.subplots_adjust(left=0.05, right=0.95, top=0.95, bottom=0.05)

    def update_frame(frame):
        ax.clear()
        ax.set_xlim(minx, maxx)
        ax.set_ylim(miny, maxy)

        gdf_trees.plot(ax=ax, edgecolor='green', facecolor='none', linewidth=3)
        gdf_obstacles.plot(ax=ax, facecolor='gray')

        gdf_noise[gdf_noise['noise_level'] > frame].plot(ax=ax, column='noise_level', cmap=cmap, alpha=0.8, vmin=vmin,
                                                         vmax=vmax)
        gdf_noise[gdf_noise['noise_level'] == frame].plot(ax=ax, column='noise_level', cmap=cmap, alpha=1, vmin=vmin,
                                                          vmax=vmax)
        gdf_noise[gdf_noise['noise_level'] == frame - 1].plot(ax=ax, column='noise_level', cmap=cmap, alpha=0.5,
                                                              vmin=vmin, vmax=vmax)
        gdf_noise[gdf_noise['noise_level'] < frame - 1].plot(ax=ax, column='noise_level', cmap=cmap, alpha=0.3,
                                                             vmin=vmin, vmax=vmax)

        ax.set_title(f"Noise Level: {frame} dB", fontsize=20)
        ax.set_axis_off()

    frames = np.arange(gdf_noise['noise_level'].max(), gdf_noise['noise_level'].min() - 1, -1)
    ani = animation.FuncAnimation(fig, update_frame, frames=frames, repeat=False)
    ani.save(output_file, writer='imagemagick', fps=15)

    plt.close()


# Call the function to create the noise animation, using the noise, obstacles, and trees data
# Fill in the buffer_p parameter close to the value specified in the logs when running the simulation.
create_noise_animation(noise, obstacles, start_p, 350, trees)