Skip to content

Commit 9edd904

Browse files
Improve radar physics fidelity and model validation
1 parent 2b05bc0 commit 9edd904

20 files changed

Lines changed: 1589 additions & 562 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
| Feature | Details |
6363
|---------|---------|
6464
| **Ground Clutter** | Weibull distribution, σ⁰ coefficients |
65-
| **Sea Clutter** | **GIT Model**, Douglas sea states (1-6) |
65+
| **Sea Clutter** | **NRL five-parameter model**, sea states 0-6, HH/VV |
6666
| **Rain Physics** | ITU-R P.838 Attenuation + Marshall-Palmer Clutter |
6767
| **MTI Filtering** | Velocity threshold, slow-mover rejection |
6868

@@ -260,4 +260,4 @@ If you use RadarSim in academic work, please cite:
260260

261261
<p align="center">
262262
<b>Built with ❤️ for the Radar Community</b>
263-
</p>
263+
</p>

docs/MODEL_FIDELITY.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Model Fidelity and Intended Use
2+
3+
RadarSim separates fast scenario-level analysis from signal-level processing. Results are
4+
meaningful only when the selected model, its inputs, and its validation domain match the
5+
question being studied. A model name or literature citation is not, by itself, evidence that a
6+
result is valid.
7+
8+
## Fidelity levels
9+
10+
### Scenario level
11+
12+
The scenario engine advances target truth states, evaluates line-of-sight geometry, computes
13+
mean received power and SNR, samples target RCS, and draws detections from a statistical
14+
detector model. It is intended for Monte Carlo detection, tracking, coverage, and engagement
15+
studies where individual ADC samples are not required.
16+
17+
The detector is a square-law detector with a threshold derived from probability of false alarm.
18+
For non-fluctuating targets, conditional integrated power is evaluated with the non-central
19+
chi-square distribution. Swerling I through IV returns are averaged over their gamma RCS
20+
distributions. Slow cases retain an RCS realization across the integration interval; fast cases
21+
decorrelate pulse to pulse.
22+
23+
### Signal level
24+
25+
The pulse-Doppler processor synthesizes complex baseband samples at the output of range
26+
compression and processes a coherent pulse interval with MTI, slow-time windowing, and a
27+
Doppler FFT. This is a parametric range-compressed model. It is not yet a receiver-front-end or
28+
raw-ADC model and must not be used to study quantization, saturation, phase noise, oscillator
29+
instability, fractional-delay filters, or analogue impairments.
30+
31+
### Imaging level
32+
33+
SAR and ISAR processing is currently experimental. Image products are useful for algorithm and
34+
interface development, but are not radiometrically calibrated products. Quantitative resolution,
35+
PSLR, ISLR, geolocation, or image-quality claims require acquisition-geometry and point-target
36+
validation before release.
37+
38+
## Parameter authority
39+
40+
Scenario files are the authority for radar, receiver, atmosphere, target, and simulation
41+
parameters. The loader rejects non-physical values and transfers the following quantities to the
42+
runtime model:
43+
44+
- carrier frequency, peak power, antenna gain and azimuth/elevation beamwidth;
45+
- PRF, pulse width, receiver noise bandwidth, noise figure, system temperature and losses;
46+
- probability of false alarm and number of integrated pulses;
47+
- atmospheric temperature, pressure and water-vapour density;
48+
- clutter enablement, terrain class, sea state and rain rate;
49+
- target kinematics, mean RCS, Swerling case and onboard jammer power.
50+
51+
Values omitted by a scenario use documented defaults. A saved run must record both supplied and
52+
resolved values so that the result can be reproduced.
53+
54+
## Verification and validation policy
55+
56+
Each quantitative model must have:
57+
58+
1. a mathematical definition and an identified source;
59+
2. units, conventions, assumptions, and a stated validity domain;
60+
3. independent reference points that were not generated by the implementation under test;
61+
4. boundary, monotonicity, conservation, and stochastic-distribution tests where applicable;
62+
5. an uncertainty statement and known limitations;
63+
6. traceability from scenario input through runtime state to exported result.
64+
65+
Verification establishes that the equations were implemented correctly. Validation establishes
66+
that the selected equations adequately represent the intended real system. Both are required for
67+
a model to be described as validated. This policy follows the credibility structure of
68+
NASA-STD-7009B and its separation of implementation verification, solution verification,
69+
validation, input pedigree, uncertainty characterization, and result robustness.
70+
71+
## Baseline references
72+
73+
- NASA-STD-7009B, *Standard for Models and Simulations*, 2024.
74+
- IEEE Std 686-2017, *IEEE Standard Radar Definitions*.
75+
- M. I. Skolnik, *Radar Handbook*, 3rd edition, 2008.
76+
- M. A. Richards, *Fundamentals of Radar Signal Processing*, 2nd edition, 2014.
77+
- P. Swerling, “Probability of Detection for Fluctuating Targets,” IRE Transactions on
78+
Information Theory, 1960, DOI: 10.1109/TIT.1960.1057561.
79+
- W. J. Albersheim, “A Closed-Form Approximation to Robertson's Detection Characteristics,”
80+
Proceedings of the IEEE, 1981, DOI: 10.1109/PROC.1981.12082.
81+
- ITU-R P.676-13, *Attenuation by atmospheric gases and related effects*, 2022.
82+
- ITU-R P.838-3, *Specific attenuation model for rain for use in prediction methods*, 2005.
83+
- V. Gregers-Hansen and R. Mital, "An Improved Empirical Model for Radar Sea Clutter
84+
Reflectivity," IEEE Transactions on Aerospace and Electronic Systems, 2012,
85+
DOI: 10.1109/TAES.2012.6324732.
86+
87+
## Known baseline limitations
88+
89+
- Gaseous attenuation uses the P.676-13 Annex 1 line-by-line equations for a homogeneous
90+
terrestrial path. Slant paths through height-dependent atmospheric profiles are not yet
91+
represented.
92+
- Land-clutter terrain-class coefficients remain an empirical screening model and require
93+
site-specific measurement or calibrated soil/vegetation inputs for quantitative use. Sea clutter
94+
uses the NRL model within its stated frequency, grazing-angle, and sea-state domain.
95+
- Scenario-level ECM does not yet model receiver saturation or waveform-dependent processing.
96+
- Pulse-Doppler synthesis starts from a range-compressed representation.
97+
- SAR/ISAR processing is not yet quantitatively validated.
98+
- Classifier training data is synthetic and must not be interpreted as operational recognition
99+
performance.
100+
101+
These limitations must remain visible in documentation and releases until the corresponding
102+
validation evidence exists.

docs/physics.md

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ Where $\theta$ is the 3-dB beamwidth in degrees.
5959

6060
## Atmospheric Attenuation
6161

62-
### ITU-R P.676-12 Model
62+
### ITU-R P.676-13 Model
6363

64-
RadarSim implements the ITU-R Recommendation P.676-12 for atmospheric gas attenuation, covering frequencies up to 1000 GHz.
64+
RadarSim implements the Annex 1 line-by-line oxygen and water-vapour equations from
65+
Recommendation ITU-R P.676-13 over 1-1000 GHz. The spectroscopic line tables, pressure
66+
broadening, oxygen interference terms, dry-air continuum, and water-vapour continuum are
67+
evaluated for the scenario temperature, dry-air pressure, and water-vapour density.
6568

6669
### Total Attenuation
6770

@@ -79,15 +82,8 @@ Key absorption features:
7982
- **60 GHz complex**: Strong O₂ resonance (~15 dB/km at peak)
8083
- **118.75 GHz line**: Secondary O₂ absorption
8184

82-
```python
83-
# Normalized pressure and temperature
84-
rp = pressure_hpa / 1013.25
85-
rt = 288.0 / temperature_k
86-
87-
# 60 GHz peak
88-
if 57 < frequency_ghz < 63:
89-
gamma_o = 15.0 * rp * (rt ** 0.5)
90-
```
85+
The implementation is verified against the official ITU-R Study Group 3 validation workbook at
86+
12, 20, 60, 90, and 130 GHz, separately for oxygen and water vapour.
9187

9288
### Water Vapor Absorption
9389

@@ -109,11 +105,14 @@ Where:
109105

110106
| Freq (GHz) | $k_H$ | $\alpha_H$ |
111107
|------------|-------|------------|
112-
| 10 (X-Band)| 0.0101| 1.276 |
113-
| 20 (K-Band)| 0.0367| 1.154 |
114-
| 35 (Ka-Band)| 0.0751| 1.099 |
108+
| 10 (X-Band)| 0.01217| 1.2571 |
109+
| 20 (K-Band)| 0.09164| 1.0568 |
110+
| 35 (Ka-Band)| 0.3374| 0.9047 |
111+
112+
The implementation evaluates the P.838-3 curve-fit equations continuously from 1 to 1000 GHz
113+
and combines horizontal and vertical coefficients for path elevation and polarization tilt.
115114

116-
**Implementation:** `src/simulation/engine.py` step loop
115+
**Implementation:** `src/physics/rain.py::ITU_R_P838`
117116

118117
---
119118

src/advanced/eccm.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,7 @@ def calculate_sjnr_db(self, snr_db: float) -> float:
390390
"""
391391
Calculate Signal-to-Jamming-plus-Noise Ratio.
392392
393-
SJNR = SNR / (1 + J/S) [linear]
394-
SJNR_dB = SNR_dB - 10·log₁₀(1 + 10^(JSR_eff/10))
393+
SJNR = 1 / (1/SNR + J/S) [linear]
395394
396395
Args:
397396
snr_db: Signal-to-noise ratio [dB]
@@ -405,9 +404,8 @@ def calculate_sjnr_db(self, snr_db: float) -> float:
405404
if jsr_eff < -50:
406405
return snr_db # No significant jamming
407406

408-
jsr_linear = 10.0 ** (jsr_eff / 10.0)
409-
sjnr_db = snr_db - 10.0 * np.log10(1.0 + jsr_linear)
410-
return sjnr_db
407+
inverse_sjnr = 10.0 ** (-snr_db / 10.0) + 10.0 ** (jsr_eff / 10.0)
408+
return float(-10.0 * np.log10(inverse_sjnr))
411409

412410
def get_status(self) -> dict:
413411
"""Get combined ECCM status for UI."""
@@ -464,8 +462,7 @@ def validate_eccm() -> dict:
464462
# ── Test 3: SJNR with Jamming ──
465463
ctrl = ECCMController(center_freq_hz=10e9, n_freq_hops=10)
466464
ctrl.set_jamming_environment(True, jsr_db=20.0)
467-
sjnr_no_eccm = 20.0 - 10.0 * np.log10(1.0 + 10.0 ** (20.0 / 10.0))
468-
sjnr_with_eccm = ctrl.calculate_sjnr_db(snr_db=20.0)
465+
sjnr_no_eccm = -10.0 * np.log10(10.0 ** (-20.0 / 10.0) + 10.0 ** (20.0 / 10.0))
469466

470467
# Without ECCM, SJNR should be much lower
471468
ctrl.enable_frequency_agility()

src/io/exporter.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def export_scenario_to_yaml(
3636
"description": description
3737
or f"Exported on {datetime.now().strftime('%Y-%m-%d %H:%M')}",
3838
"version": "1.0",
39+
"duration_seconds": max(float(engine.current_time), float(engine.dt)),
40+
"update_rate_hz": 1.0 / float(engine.dt),
3941
},
4042
"radar": _extract_radar_config(engine),
4143
"targets": _extract_targets(engine),
@@ -66,10 +68,24 @@ def _extract_radar_config(engine) -> Dict[str, Any]:
6668
radar = engine.radar
6769

6870
return {
71+
"name": str(radar.radar_id),
72+
"type": "pulse_doppler",
6973
"frequency_hz": float(radar.frequency_hz),
7074
"power_watts": float(radar.power_watts),
71-
"antenna_gain_db": float(radar.antenna_gain_db),
72-
"beamwidth_deg": float(radar.beamwidth_deg),
75+
"antenna": {
76+
"gain_db": float(radar.antenna_gain_db),
77+
"beamwidth_az_deg": float(radar.to_dict()["beamwidth_deg"]),
78+
"beamwidth_el_deg": float(radar.to_dict()["beamwidth_el_deg"]),
79+
"polarization_tilt_deg": float(radar.polarization_tilt_deg),
80+
},
81+
"receiver": {
82+
"noise_figure_db": float(radar.noise_figure_db),
83+
"bandwidth_hz": float(radar.receiver_bandwidth_hz),
84+
"system_temperature_k": float(radar.system_temperature_k),
85+
},
86+
"prf_hz": float(radar.prf_hz),
87+
"pulse_width_s": float(radar.pulse_width_s),
88+
"system_losses_db": float(radar.system_losses_db),
7389
"scan_rate_rpm": float(radar.scan_rate_rpm),
7490
"position": {
7591
"x_m": float(radar.position[0]),
@@ -85,19 +101,19 @@ def _extract_targets(engine) -> list:
85101

86102
for t in engine.targets:
87103
target_data = {
88-
"id": int(t.target_id),
104+
"name": f"Target_{int(t.target_id)}",
89105
"type": str(getattr(t, "target_type", "aircraft")),
90-
"position_m": {
91-
"x": float(t.position[0]),
92-
"y": float(t.position[1]),
93-
"z": float(t.position[2]) if len(t.position) > 2 else 0.0,
106+
"initial_position": {
107+
"x_m": float(t.position[0]),
108+
"y_m": float(t.position[1]),
109+
"z_m": float(t.position[2]) if len(t.position) > 2 else 0.0,
94110
},
95-
"velocity_mps": {
96-
"vx": float(t.velocity[0]),
97-
"vy": float(t.velocity[1]),
98-
"vz": float(t.velocity[2]) if len(t.velocity) > 2 else 0.0,
111+
"velocity": {
112+
"vx_mps": float(t.velocity[0]),
113+
"vy_mps": float(t.velocity[1]),
114+
"vz_mps": float(t.velocity[2]) if len(t.velocity) > 2 else 0.0,
99115
},
100-
"rcs_m2": float(t.rcs_m2),
116+
"rcs_m2": float(t.rcs_mean),
101117
"swerling_model": (
102118
int(t.swerling_model.value)
103119
if hasattr(t.swerling_model, "value")
@@ -107,10 +123,14 @@ def _extract_targets(engine) -> list:
107123

108124
# Optional jammer settings
109125
if hasattr(t, "jammer_active") and t.jammer_active:
110-
target_data["jammer"] = {
111-
"active": True,
112-
"power_watts": float(getattr(t, "jammer_power", 1000)),
113-
}
126+
target_data["has_ecm"] = True
127+
target_data["ecm_type"] = str(getattr(t, "ecm_type", "noise_barrage"))
128+
target_data["ecm_power_watts"] = float(
129+
getattr(t, "jammer_power_watts", 1000)
130+
)
131+
target_data["ecm_bandwidth_hz"] = float(
132+
getattr(t, "jammer_bandwidth_hz", 100e6)
133+
)
114134

115135
targets.append(target_data)
116136

@@ -121,9 +141,14 @@ def _extract_environment(engine) -> Dict[str, Any]:
121141
"""Extract environment settings from engine."""
122142
env = {
123143
"enable_atmospheric": bool(engine.enable_atmospheric),
144+
"temperature_c": float(engine.atmospheric_temperature_c),
145+
"pressure_hpa": float(engine.atmospheric_pressure_hpa),
146+
"water_vapor_gpm3": float(engine.water_vapor_density_g_m3),
147+
"rain_rate_mm_hr": float(engine.rain_rate_mm_hr),
124148
"clutter": {
125149
"enabled": bool(getattr(engine, "clutter_enabled", False)),
126150
"terrain_type": str(getattr(engine, "terrain_type", "rural")),
151+
"sea_state": int(getattr(engine, "sea_state", 0)),
127152
},
128153
}
129154

@@ -140,8 +165,10 @@ def _extract_environment(engine) -> Dict[str, Any]:
140165
def _extract_simulation_params(engine) -> Dict[str, Any]:
141166
"""Extract simulation parameters from engine."""
142167
params = {
143-
"dt": float(engine.dt),
144-
"detection_threshold_db": float(engine.detection_threshold_db),
168+
"enable_atmospheric_loss": bool(engine.enable_atmospheric),
169+
"enable_clutter": bool(engine.clutter_enabled),
170+
"pfa": float(engine.probability_false_alarm),
171+
"pulses_integrated": int(engine.pulses_integrated),
145172
}
146173

147174
# Advanced features

0 commit comments

Comments
 (0)