Script per il calcolo della direzione di volo per minimizzare l'asimmetria derivante da BRDF
pip install astral tzdata
python3 optimal_flight_azimuth.py --lat 43.7696 --lon 11.2558 \
--date 2026-07-22 --time 11:46 --tz Europe/Rome
Data/ora locale: 2026-07-22 11:46:00+02:00
Posizione: lat=43.7696, lon=11.2558
Sole: azimuth=132.17 deg, elevazione=59.20 deg (zenith=30.80 deg)
FOV assunto: 24.75 deg | parametri RPV: k=1.0, Theta=-0.15, h=0.1
--- Risultato ---
Azimuth di volo OTTIMALE (minima asimmetria BRDF): 132.0 deg (reciproco: 312.0 deg) -> escursione prevista: 1.8%
Azimuth di volo PEGGIORE (massima asimmetria BRDF): 42.0 deg (reciproco: 222.0 deg) -> escursione prevista: 20.8%
il parametro teta indica l'asimmetria della funzione di fase ovvero quanto e' preferenziale il backscattering rispetto al forward scattering da parte della superficie (teta = 0 isotropia, valori minori di zero privilegia backascattering)
il parametro H indica quanto e' largo il picco di hotspot o meglio quanto gradualmente il picco si presenta sul bordo
/////////////////////////////////////////////////////////////////////////////////////////////
#!/usr/bin/env python3
"""
optimal_flight_azimuth.py
Calcola l'azimuth di volo che MINIMIZZA l'asimmetria across-track dovuta
all'effetto BRDF hotspot/anti-hotspot, data una data, un'ora e una
posizione geografica (lat/lon).
Principio
---------
L'asimmetria hotspot/anti-hotspot e' massima quando l'asse across-track
(perpendicolare alla linea di volo) giace nel piano principale solare,
cioe' quando la linea di volo e' perpendicolare all'azimuth del sole.
E' MINIMA quando la linea di volo e' invece allineata con l'azimuth del
sole (o il suo reciproco) - "vola con il sole davanti o dietro, non di
lato". Questo script:
1. Calcola la posizione del sole (azimuth, elevazione) per la data/ora/
posizione fornite, con la libreria 'astral'.
2. Usa il modello BRDF semi-empirico RPV (lo stesso di
brdf_hotspot_validation.py) per stimare quantitativamente l'ampiezza
dell'asimmetria across-track attesa per OGNI possibile azimuth di volo
(0-179 gradi, dato che una linea di volo e' bidirezionale e quindi il
problema ha periodo 180 gradi), tenendo conto del FOV del sensore.
3. Riporta l'azimuth ottimale (minima asimmetria), quello peggiore
(massima asimmetria) e un grafico dell'ampiezza attesa in funzione
dell'azimuth di volo scelto, cosi' puoi vedere anche quanto e'
'largo' il minimo (cioe' quanto puoi discostarti dall'ottimo restando
comunque in una zona a basso impatto).
Uso
---
python3 optimal_flight_azimuth.py --lat 43.7696 --lon 11.2558 \\
--date 2026-07-22 --time 11:46 --tz Europe/Rome
# con FOV/parametri RPV personalizzati:
python3 optimal_flight_azimuth.py --lat 43.7243059 --lon 10.2791424 \\
--date 2026-07-10 --time 11:47 --tz Europe/Rome \\
--fov 24.75 --rpv-theta -0.15 --rpv-h 0.1
"""
import argparse
from datetime import datetime
from zoneinfo import ZoneInfo
import numpy as np
from astral import LocationInfo
from astral.sun import elevation as sun_elevation_fn, azimuth as sun_azimuth_fn
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# ---------------------------------------------------------------------
# Modello RPV (identico a brdf_hotspot_validation.py)
# ---------------------------------------------------------------------
def rpv_reflectance(theta_i_deg, theta_r_deg, phi_deg, k=1.0, theta_hg=-0.15, h=0.1, rho0=1.0):
ti = np.radians(theta_i_deg)
tr = np.radians(theta_r_deg)
phi = np.radians(phi_deg)
cos_ti, cos_tr = np.cos(ti), np.cos(tr)
sin_ti, sin_tr = np.sin(ti), np.sin(tr)
cos_g = cos_ti * cos_tr + sin_ti * sin_tr * np.cos(phi)
cos_g = np.clip(cos_g, -1.0, 1.0)
F = (1 - theta_hg**2) / (1 + 2 * theta_hg * cos_g + theta_hg**2) ** 1.5
tan_ti, tan_tr = np.tan(ti), np.tan(tr)
G = np.sqrt(np.clip(tan_ti**2 + tan_tr**2 - 2 * tan_ti * tan_tr * np.cos(phi), 0, None))
H = 1 + (1 - h) / (1 + G / h)
M = (cos_ti * cos_tr * (cos_ti + cos_tr)) ** (k - 1)
return rho0 * M * F * H
def brdf_amplitude_for_flight_azimuth(flight_az, sun_az, sun_zenith, fov_deg,
k=1.0, theta_hg=-0.15, h=0.1, n_samples=480):
"""
Per un dato azimuth di volo, calcola l'ampiezza (escursione normalizzata
max-min) del fattore BRDF atteso sullo swath, dato il FOV del sensore.
"""
across_1 = (flight_az + 90) % 360
across_2 = (flight_az - 90) % 360
half_fov = fov_deg / 2.0
col = np.arange(n_samples)
center = (n_samples - 1) / 2.0
view_zenith = np.abs((col - center) / center) * half_fov
offset = col - center
view_az = np.where(offset < 0, across_1, across_2)
phi = view_az - sun_az
R = rpv_reflectance(sun_zenith, view_zenith, phi, k=k, theta_hg=theta_hg, h=h)
R_norm = R / np.mean(R)
return R_norm.max() - R_norm.min(), R_norm
# ---------------------------------------------------------------------
# Posizione solare
# ---------------------------------------------------------------------
def get_sun_position(lat, lon, date_str, time_str, tz_str):
tz = ZoneInfo(tz_str)
hh, mm = [int(v) for v in time_str.split(":")]
y, m, d = [int(v) for v in date_str.split("-")]
dt = datetime(y, m, d, hh, mm, tzinfo=tz)
loc = LocationInfo(latitude=lat, longitude=lon)
el = sun_elevation_fn(loc.observer, dt)
az = sun_azimuth_fn(loc.observer, dt)
return dt, az, el
# ---------------------------------------------------------------------
# Analisi principale
# ---------------------------------------------------------------------
def analyze(lat, lon, date_str, time_str, tz_str, fov_deg, rpv_k, rpv_theta, rpv_h, out_path):
dt, sun_az, sun_el = get_sun_position(lat, lon, date_str, time_str, tz_str)
sun_zenith = 90 - sun_el
print(f"Data/ora locale: {dt}")
print(f"Posizione: lat={lat}, lon={lon}")
print(f"Sole: azimuth={sun_az:.2f} deg, elevazione={sun_el:.2f} deg (zenith={sun_zenith:.2f} deg)")
if sun_el <= 0:
print("\n[attenzione] Il sole e' sotto l'orizzonte a quest'ora: nessun volo possibile/sensato.")
return
print(f"FOV assunto: {fov_deg:.2f} deg | parametri RPV: k={rpv_k}, Theta={rpv_theta}, h={rpv_h}")
# scansione di tutti gli azimuth di volo possibili (0-179, periodo 180 gradi)
azimuths = np.arange(0, 180, 0.5)
amplitudes = np.array([
brdf_amplitude_for_flight_azimuth(az, sun_az, sun_zenith, fov_deg, rpv_k, rpv_theta, rpv_h)[0]
for az in azimuths
])
best_idx = np.argmin(amplitudes)
worst_idx = np.argmax(amplitudes)
best_az = azimuths[best_idx]
worst_az = azimuths[worst_idx]
print("\n--- Risultato ---")
print(f"Azimuth di volo OTTIMALE (minima asimmetria BRDF): {best_az:.1f} deg "
f"(reciproco: {(best_az+180)%360:.1f} deg) -> escursione prevista: {100*amplitudes[best_idx]:.1f}%")
print(f"Azimuth di volo PEGGIORE (massima asimmetria BRDF): {worst_az:.1f} deg "
f"(reciproco: {(worst_az+180)%360:.1f} deg) -> escursione prevista: {100*amplitudes[worst_idx]:.1f}%")
print(f"\n(per confronto: l'azimuth ottimale teorico e' semplicemente l'azimuth del sole stesso, "
f"{sun_az:.1f} deg mod 180 = {sun_az % 180:.1f} deg - 'vola con il sole davanti o dietro')")
# ampiezza per un eventuale azimuth di volo specifico gia' pianificato
# (utile come confronto, mostrata solo nel grafico)
fig, ax = plt.subplots(figsize=(9, 5.5))
ax.plot(azimuths, 100 * amplitudes, color="darkred", linewidth=1.8)
ax.axvline(best_az, color="green", linestyle="--", label=f"ottimale: {best_az:.1f} deg ({100*amplitudes[best_idx]:.1f}%)")
ax.axvline(worst_az, color="crimson", linestyle="--", label=f"peggiore: {worst_az:.1f} deg ({100*amplitudes[worst_idx]:.1f}%)")
ax.set_xlabel("Azimuth di volo (gradi, periodo 180°)")
ax.set_ylabel("Escursione BRDF attesa sullo swath (%)")
ax.set_title(f"Ampiezza asimmetria BRDF attesa in funzione dell'azimuth di volo\n"
f"{dt.strftime('%Y-%m-%d %H:%M %Z')} @ lat={lat}, lon={lon} "
f"(sole: az={sun_az:.1f}°, el={sun_el:.1f}°)")
ax.legend(fontsize=9)
ax.grid(alpha=0.3)
fig.tight_layout()
fig.savefig(out_path, dpi=150)
plt.close(fig)
print(f"\nGrafico salvato in: {out_path}")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--lat", type=float, required=True, help="Latitudine (gradi decimali)")
parser.add_argument("--lon", type=float, required=True, help="Longitudine (gradi decimali)")
parser.add_argument("--date", required=True, help="Data locale, formato YYYY-MM-DD")
parser.add_argument("--time", required=True, help="Ora locale, formato HH:MM")
parser.add_argument("--tz", default="Europe/Rome", help="Timezone IANA (default: Europe/Rome)")
parser.add_argument("--fov", type=float, default=24.75, help="FOV across-track del sensore, gradi (default: 24.75)")
parser.add_argument("--rpv-k", type=float, default=1.0, help="Parametro RPV k (default 1.0)")
parser.add_argument("--rpv-theta", type=float, default=-0.15, help="Parametro RPV Theta (default -0.15)")
parser.add_argument("--rpv-h", type=float, default=0.1, help="Parametro RPV h (default 0.1)")
parser.add_argument("--out", default="optimal_flight_azimuth.png", help="Percorso del grafico di output")
args = parser.parse_args()
analyze(args.lat, args.lon, args.date, args.time, args.tz,
args.fov, args.rpv_k, args.rpv_theta, args.rpv_h, args.out)
if __name__ == "__main__":
main()





