297 lines
13 KiB
Python
297 lines
13 KiB
Python
"""
|
||
Модель шумового загрязнения от воздушных судов (v1.1)
|
||
|
||
Физическая основа
|
||
─────────────────
|
||
Шум распространяется сферически. Уровень шума определяется
|
||
реальным 3D-расстоянием R (гипотенуза) от самолёта до наблюдателя.
|
||
|
||
На карте отображается горизонтальный катет D:
|
||
|
||
самолёт ●
|
||
|\
|
||
H | \ R ← граница зоны
|
||
| \
|
||
земля ●───●─────● наблюдатель
|
||
D
|
||
|
||
D = √(R² − H²), если H < R, иначе 0
|
||
|
||
Пример (H = 3.5 км):
|
||
R=2 км → нет (2² < 3.5²)
|
||
R=5 км → D = √(25−12.25) = 3.57 км (круг)
|
||
R=7 км → D = 6.06 км, кольцо от 3.57 до 6.06 км
|
||
R=11 км → D = 10.43 км, кольцо от 6.06 до 10.43 км
|
||
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
КАЛИБРОВОЧНЫЕ ПАРАМЕТРЫ (редактируй здесь)
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
NOISE_ZONES — три концентрических зоны вдоль траектории.
|
||
Каждая зона описывает «рукав» определённой ширины рядом с треком.
|
||
|
||
Поля каждой зоны:
|
||
id - уникальный идентификатор (используется в JS)
|
||
label - отображаемое название в легенде
|
||
dist_km - внешняя граница зоны от трека (км)
|
||
Зона 0 рисуется от 0 до dist_km[0],
|
||
Зона 1 — от dist_km[0] до dist_km[1], и т.д.
|
||
color - цвет заливки (hex)
|
||
opacity - базовая прозрачность при полной активации (0.0–1.0)
|
||
Итоговая прозрачность умножается на altitude_factor
|
||
|
||
ALTITUDE_BANDS — как высота влияет на ширину зон.
|
||
max_alt_m - верхняя граница диапазона высоты (метры)
|
||
width_factor - коэффициент ширины зоны (1.0 = полная, 0.0 = зона исчезает)
|
||
Диапазоны проверяются снизу вверх, берётся первый подходящий.
|
||
|
||
Пример калибровки:
|
||
Если реальные замеры показывают, что на высоте 500м зона 0–2км
|
||
слишком широкая — уменьши width_factor для диапазона max_alt_m=900.
|
||
"""
|
||
|
||
# ── Зоны шума ────────────────────────────────────────────────────
|
||
#
|
||
# Физическая модель (теорема Пифагора):
|
||
#
|
||
# самолёт ●
|
||
# |\
|
||
# H | \ R ← гипотенуза = реальное расстояние до наблюдателя
|
||
# | \
|
||
# земля ●───────●──────● наблюдатель
|
||
# проекция D ← катет = ширина зоны на карте
|
||
#
|
||
# D = √(R² − H²), если H < R, иначе 0
|
||
#
|
||
# Поля зоны:
|
||
# R_inner — внутренняя граница сферы (км); для первой зоны = 0
|
||
# R_outer — внешняя граница сферы (км)
|
||
# color — цвет заливки (hex)
|
||
# opacity — прозрачность (фиксированная, 0.0–1.0)
|
||
#
|
||
# Таблица соответствия:
|
||
# R < 2 км → критический шум 🔴
|
||
# R 2–5 км → сильный шум 🟠
|
||
# R 5–7 км → средний шум 🟡
|
||
# R 7–9 км → низкий шум 🟢
|
||
# R > 9 км → зона не рисуется
|
||
#
|
||
NOISE_ZONES = [
|
||
{
|
||
"id": "zone_critical",
|
||
"label": "Критический (R < 2 км)",
|
||
"R_inner": 0.0, # км — внутренняя граница сферы
|
||
"R_outer": 2.0, # км — внешняя граница сферы
|
||
"color": "#FF3333",
|
||
"opacity": 0.01,
|
||
},
|
||
{
|
||
"id": "zone_strong",
|
||
"label": "Сильный (R 2–5 км)",
|
||
"R_inner": 2.0,
|
||
"R_outer": 5.0,
|
||
"color": "#FF8800",
|
||
"opacity": 0.01,
|
||
},
|
||
{
|
||
"id": "zone_medium",
|
||
"label": "Средний (R 5–7 км)",
|
||
"R_inner": 5.0,
|
||
"R_outer": 7.0,
|
||
"color": "#FFCC00",
|
||
"opacity": 0.01,
|
||
},
|
||
{
|
||
"id": "zone_low",
|
||
"label": "Низкий (R 7–9 км)",
|
||
"R_inner": 7.0,
|
||
"R_outer": 9.0,
|
||
"color": "#88DD00",
|
||
"opacity": 0.01,
|
||
},
|
||
]
|
||
|
||
# ALTITUDE_BANDS больше не используется — ширина зоны теперь
|
||
# рассчитывается аналитически через теорему Пифагора в calc_horizontal_radius()
|
||
ALTITUDE_BANDS = [] # оставлен для обратной совместимости
|
||
|
||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
# КОНЕЦ КАЛИБРОВОЧНЫХ ПАРАМЕТРОВ
|
||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
# Справочные данные: типичные уровни шума у земли (дБ)
|
||
# Источник: стандартные авиационные данные
|
||
NOISE_AT_GROUND = {
|
||
"default": 85, # дефолт для неизвестного типа ВС
|
||
"B738": 88, # Boeing 737-800
|
||
"B77W": 90, # Boeing 777-300ER
|
||
"A320": 87, # Airbus A320
|
||
"A321": 88, # Airbus A321
|
||
"A333": 89, # Airbus A330-300
|
||
"A359": 86, # Airbus A350-900
|
||
"B763": 89, # Boeing 767-300
|
||
"SU95": 86, # Sukhoi Superjet 100
|
||
"E170": 84, # Embraer 170
|
||
"AT75": 80, # ATR 72
|
||
}
|
||
|
||
# Параметры модели
|
||
MAX_NOISE_RADIUS_KM = 3.0 # максимальный радиус шумовой зоны (км) на нулевой высоте
|
||
MIN_ALTITUDE_FT = 100 # минимальная высота для расчёта (фут)
|
||
MAX_ALTITUDE_FT = 40000 # максимальная высота (фут) — шум не слышен выше
|
||
NOISE_THRESHOLD_DB = 55 # порог шума (дБ), ниже которого зона не показывается
|
||
|
||
|
||
def altitude_to_noise_db(altitude_ft: float, aircraft_type: str = "default") -> float:
|
||
"""
|
||
Расчёт уровня шума на земле в зависимости от высоты (дБ)
|
||
|
||
Формула: L = L0 - 20*log10(h/h0) - α*h
|
||
где L0 - шум у земли, h - высота, h0 = 300 ft (опорная высота), α = коэф. затухания
|
||
"""
|
||
base_noise = NOISE_AT_GROUND.get(aircraft_type, NOISE_AT_GROUND["default"])
|
||
|
||
if altitude_ft <= MIN_ALTITUDE_FT:
|
||
return base_noise
|
||
|
||
if altitude_ft >= MAX_ALTITUDE_FT:
|
||
return 0.0
|
||
|
||
# Геометрическое затухание (обратный квадрат расстояния → 20 log)
|
||
import math
|
||
h0 = 300 # опорная высота в футах
|
||
geometric_attenuation = 20 * math.log10(altitude_ft / h0)
|
||
|
||
# Атмосферное поглощение (приблизительно 0.002 дБ/фут)
|
||
atmospheric_attenuation = 0.002 * altitude_ft
|
||
|
||
noise_db = base_noise - geometric_attenuation - atmospheric_attenuation
|
||
return max(0.0, noise_db)
|
||
|
||
|
||
def altitude_to_noise_radius_km(altitude_ft: float) -> float:
|
||
"""
|
||
Расчёт радиуса шумовой зоны (км) на основе высоты
|
||
Простая обратно-пропорциональная модель для визуализации
|
||
"""
|
||
if altitude_ft <= 0:
|
||
altitude_ft = 100
|
||
|
||
if altitude_ft >= MAX_ALTITUDE_FT:
|
||
return 0.0
|
||
|
||
# Радиус уменьшается с высотой (нелинейно)
|
||
radius = MAX_NOISE_RADIUS_KM * (1.0 - (altitude_ft / MAX_ALTITUDE_FT) ** 0.5)
|
||
return max(0.0, radius)
|
||
|
||
|
||
def altitude_to_color(altitude_ft: float) -> str:
|
||
"""
|
||
Цветовая кодировка по высоте:
|
||
- Красный (0–3000 ft): высокий шум
|
||
- Оранжевый (3000–10000 ft): средний шум
|
||
- Жёлтый (10000–25000 ft): низкий шум
|
||
- Зелёный (25000+ ft): минимальный шум
|
||
"""
|
||
if altitude_ft < 3000:
|
||
return "#FF0000" # красный - критический шум
|
||
elif altitude_ft < 10000:
|
||
return "#FF6600" # оранжевый - высокий шум
|
||
elif altitude_ft < 25000:
|
||
return "#FFAA00" # жёлтый - средний шум
|
||
else:
|
||
return "#00AA44" # зелёный - низкий шум
|
||
|
||
|
||
def altitude_to_noise_level(altitude_ft: float) -> str:
|
||
"""Текстовое описание уровня шума"""
|
||
if altitude_ft < 3000:
|
||
return "Критический"
|
||
elif altitude_ft < 10000:
|
||
return "Высокий"
|
||
elif altitude_ft < 25000:
|
||
return "Средний"
|
||
else:
|
||
return "Низкий"
|
||
|
||
|
||
def calculate_noise_opacity(altitude_ft: float) -> float:
|
||
"""Прозрачность шумовой зоны (0.1–0.6)"""
|
||
if altitude_ft >= MAX_ALTITUDE_FT:
|
||
return 0.0
|
||
opacity = 0.6 * (1.0 - altitude_ft / MAX_ALTITUDE_FT)
|
||
return max(0.05, min(0.6, opacity))
|
||
|
||
|
||
def calc_horizontal_radius(R_km: float, altitude_m: float) -> float:
|
||
"""
|
||
Горизонтальный радиус зоны на карте (катет) по теореме Пифагора.
|
||
|
||
R_km — радиус сферы шума (км), граница зоны
|
||
altitude_m — высота самолёта над землёй (метры)
|
||
|
||
Возвращает D в км, или 0 если самолёт выше границы зоны.
|
||
"""
|
||
import math
|
||
H = altitude_m / 1000.0 # переводим в км
|
||
if H >= R_km:
|
||
return 0.0
|
||
return math.sqrt(max(0.0, R_km**2 - H**2))
|
||
|
||
|
||
def calc_zone_radii_for_point(altitude_m: float) -> list:
|
||
"""
|
||
Для каждой зоны возвращает (D_inner, D_outer) в км на земле.
|
||
Если D_outer == 0 → зона не видна.
|
||
Если D_inner == 0 → зона рисуется как круг (без дырки).
|
||
"""
|
||
result = []
|
||
for zone in NOISE_ZONES:
|
||
d_inner = calc_horizontal_radius(zone["R_inner"], altitude_m) if zone["R_inner"] > 0 else 0.0
|
||
d_outer = calc_horizontal_radius(zone["R_outer"], altitude_m)
|
||
result.append({
|
||
"id": zone["id"],
|
||
"color": zone["color"],
|
||
"opacity": zone["opacity"],
|
||
"d_inner": round(d_inner, 4), # км, внутренняя граница на карте
|
||
"d_outer": round(d_outer, 4), # км, внешняя граница на карте
|
||
"visible": d_outer > 0.0,
|
||
})
|
||
return result
|
||
|
||
|
||
def get_noise_config() -> dict:
|
||
"""
|
||
Возвращает калибровочные параметры для фронтенда.
|
||
Вызывается через /api/noise-config — JS читает конфиг при старте.
|
||
"""
|
||
return {
|
||
"zones": NOISE_ZONES,
|
||
"altitude_bands": ALTITUDE_BANDS,
|
||
}
|
||
|
||
|
||
def get_altitude_width_factor(altitude_m: float) -> float:
|
||
"""Возвращает коэффициент ширины зоны для данной высоты (метры)."""
|
||
for band in ALTITUDE_BANDS:
|
||
if altitude_m <= band["max_alt_m"]:
|
||
return band["width_factor"]
|
||
return 0.0
|
||
|
||
|
||
def process_flight_for_map(flight_data: dict) -> dict:
|
||
"""
|
||
Обрабатывает данные одного рейса и добавляет шумовые характеристики
|
||
"""
|
||
altitude = flight_data.get("altitude", 0) or 0
|
||
aircraft_type = flight_data.get("aircraft_type", "default") or "default"
|
||
|
||
return {
|
||
**flight_data,
|
||
"noise_db": round(altitude_to_noise_db(altitude, aircraft_type), 1),
|
||
"noise_radius_km": round(altitude_to_noise_radius_km(altitude), 3),
|
||
"noise_color": altitude_to_color(altitude),
|
||
"noise_level": altitude_to_noise_level(altitude),
|
||
"noise_opacity": round(calculate_noise_opacity(altitude), 3),
|
||
}
|