sim_lib_interference_core/
certificate.rs1use crate::{Emitter, InterferenceError, InterferenceProblem, SamplingPlane};
4
5#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct SamplingThresholds {
14 pub resolved_min_samples_per_wavelength: f64,
16 pub marginal_min_samples_per_wavelength: f64,
18 pub resolved_max_envelope_fraction_per_cell: f64,
20 pub marginal_max_envelope_fraction_per_cell: f64,
22}
23
24impl SamplingThresholds {
25 pub fn new(
27 resolved_min_samples_per_wavelength: f64,
28 marginal_min_samples_per_wavelength: f64,
29 resolved_max_envelope_fraction_per_cell: f64,
30 marginal_max_envelope_fraction_per_cell: f64,
31 ) -> Result<Self, InterferenceError> {
32 Self {
33 resolved_min_samples_per_wavelength,
34 marginal_min_samples_per_wavelength,
35 resolved_max_envelope_fraction_per_cell,
36 marginal_max_envelope_fraction_per_cell,
37 }
38 .validate()
39 }
40
41 pub fn validate(self) -> Result<Self, InterferenceError> {
43 positive_threshold(
44 "resolved-min-samples-per-wavelength",
45 self.resolved_min_samples_per_wavelength,
46 )?;
47 positive_threshold(
48 "marginal-min-samples-per-wavelength",
49 self.marginal_min_samples_per_wavelength,
50 )?;
51 non_negative_threshold(
52 "resolved-max-envelope-fraction-per-cell",
53 self.resolved_max_envelope_fraction_per_cell,
54 )?;
55 non_negative_threshold(
56 "marginal-max-envelope-fraction-per-cell",
57 self.marginal_max_envelope_fraction_per_cell,
58 )?;
59 if self.resolved_min_samples_per_wavelength < self.marginal_min_samples_per_wavelength
60 || self.resolved_max_envelope_fraction_per_cell
61 > self.marginal_max_envelope_fraction_per_cell
62 {
63 return Err(InterferenceError::InconsistentSamplingThresholds {
64 resolved_min_samples_per_wavelength: self.resolved_min_samples_per_wavelength,
65 marginal_min_samples_per_wavelength: self.marginal_min_samples_per_wavelength,
66 resolved_max_envelope_fraction_per_cell: self
67 .resolved_max_envelope_fraction_per_cell,
68 marginal_max_envelope_fraction_per_cell: self
69 .marginal_max_envelope_fraction_per_cell,
70 });
71 }
72 Ok(self)
73 }
74}
75
76impl Default for SamplingThresholds {
77 fn default() -> Self {
78 Self {
79 resolved_min_samples_per_wavelength: 8.0,
80 marginal_min_samples_per_wavelength: 4.0,
81 resolved_max_envelope_fraction_per_cell: 0.05,
82 marginal_max_envelope_fraction_per_cell: 0.10,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub enum SamplingPolicy {
90 #[default]
92 Strict,
93 Annotate,
95}
96
97impl SamplingPolicy {
98 pub fn admit(self, certificate: &SamplingCertificate) -> Result<(), InterferenceError> {
100 if self == Self::Strict && certificate.verdict != SamplingVerdict::Resolved {
101 Err(InterferenceError::SamplingRefused {
102 certificate: *certificate,
103 })
104 } else {
105 Ok(())
106 }
107 }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum SamplingVerdict {
113 Resolved,
115 Marginal,
117 Aliased,
119}
120
121#[derive(Clone, Copy, Debug, PartialEq)]
130pub struct SamplingCertificate {
131 pub thresholds: SamplingThresholds,
133 pub wavelength_m: f64,
135 pub samples_per_wavelength_u: f64,
137 pub samples_per_wavelength_v: f64,
139 pub samples_per_power_fringe_u: f64,
141 pub samples_per_power_fringe_v: f64,
143 pub nearest_point_source_distance_m: Option<f64>,
147 pub max_envelope_fraction_per_cell: f64,
149 pub verdict: SamplingVerdict,
151}
152
153impl SamplingCertificate {
154 pub fn measure(
159 problem: &InterferenceProblem,
160 plane: &SamplingPlane,
161 ) -> Result<Self, InterferenceError> {
162 Self::measure_with_thresholds(problem, plane, SamplingThresholds::default())
163 }
164
165 pub fn measure_with_thresholds(
167 problem: &InterferenceProblem,
168 plane: &SamplingPlane,
169 thresholds: SamplingThresholds,
170 ) -> Result<Self, InterferenceError> {
171 let thresholds = thresholds.validate()?;
172 let wavelength_m = finite_metric("wavelength-m", problem.wavelength_metres())?;
173 let samples_per_wavelength_u = finite_metric(
174 "samples-per-wavelength-u",
175 wavelength_m / plane.cell_size_u_m(),
176 )?;
177 let samples_per_wavelength_v = finite_metric(
178 "samples-per-wavelength-v",
179 wavelength_m / plane.cell_size_v_m(),
180 )?;
181 let samples_per_power_fringe_u = samples_per_wavelength_u / 2.0;
182 let samples_per_power_fringe_v = samples_per_wavelength_v / 2.0;
183 let nearest_point_source_distance_m = nearest_point_source_distance(problem, plane)?;
184 let max_envelope_fraction_per_cell = match nearest_point_source_distance_m {
185 Some(distance) => conservative_envelope_change(distance, plane)?,
186 None => 0.0,
187 };
188
189 let minimum_carrier_samples = samples_per_wavelength_u.min(samples_per_wavelength_v);
190 let verdict = if minimum_carrier_samples >= thresholds.resolved_min_samples_per_wavelength
191 && max_envelope_fraction_per_cell <= thresholds.resolved_max_envelope_fraction_per_cell
192 {
193 SamplingVerdict::Resolved
194 } else if minimum_carrier_samples >= thresholds.marginal_min_samples_per_wavelength
195 && max_envelope_fraction_per_cell <= thresholds.marginal_max_envelope_fraction_per_cell
196 {
197 SamplingVerdict::Marginal
198 } else {
199 SamplingVerdict::Aliased
200 };
201
202 Ok(Self {
203 thresholds,
204 wavelength_m,
205 samples_per_wavelength_u,
206 samples_per_wavelength_v,
207 samples_per_power_fringe_u,
208 samples_per_power_fringe_v,
209 nearest_point_source_distance_m,
210 max_envelope_fraction_per_cell,
211 verdict,
212 })
213 }
214}
215
216fn positive_threshold(name: &'static str, value: f64) -> Result<(), InterferenceError> {
217 if value.is_finite() && value > 0.0 {
218 Ok(())
219 } else {
220 Err(InterferenceError::InvalidSamplingThreshold { name, value })
221 }
222}
223
224fn non_negative_threshold(name: &'static str, value: f64) -> Result<(), InterferenceError> {
225 if value.is_finite() && value >= 0.0 {
226 Ok(())
227 } else {
228 Err(InterferenceError::InvalidSamplingThreshold { name, value })
229 }
230}
231
232fn finite_metric(name: &'static str, value: f64) -> Result<f64, InterferenceError> {
233 value
234 .is_finite()
235 .then_some(value)
236 .ok_or(InterferenceError::NonFiniteSamplingMetric { name, value })
237}
238
239fn nearest_point_source_distance(
240 problem: &InterferenceProblem,
241 plane: &SamplingPlane,
242) -> Result<Option<f64>, InterferenceError> {
243 let mut nearest: Option<f64> = None;
244 for source in &problem.sources {
245 let Emitter::Point { position, .. } = source else {
246 continue;
247 };
248 let [origin_x, origin_y, origin_z] = plane.origin().coordinates_metres();
249 let [source_x, source_y, source_z] = position.coordinates_metres();
250 let displacement = [
251 source_x - origin_x,
252 source_y - origin_y,
253 source_z - origin_z,
254 ];
255 if displacement.iter().any(|component| !component.is_finite()) {
256 return Err(InterferenceError::NonFiniteSamplingMetric {
257 name: "point-source-plane-displacement-m",
258 value: displacement
259 .into_iter()
260 .find(|component| !component.is_finite())
261 .unwrap_or(f64::NAN),
262 });
263 }
264 let [ux, uy, uz] = plane.u_axis().components();
265 let [vx, vy, vz] = plane.v_axis().components();
266 let [nx, ny, nz] = plane.normal().components();
267 let projected_u = displacement[0] * ux + displacement[1] * uy + displacement[2] * uz;
268 let projected_v = displacement[0] * vx + displacement[1] * vy + displacement[2] * vz;
269 let projected_normal = displacement[0] * nx + displacement[1] * ny + displacement[2] * nz;
270 let outside_u = outside_extent_distance(projected_u, plane.extent_u().get());
271 let outside_v = outside_extent_distance(projected_v, plane.extent_v().get());
272 let distance = finite_metric(
273 "nearest-point-source-distance-m",
274 outside_u.hypot(outside_v).hypot(projected_normal),
275 )?;
276 nearest = Some(nearest.map_or(distance, |current| current.min(distance)));
277 }
278 Ok(nearest)
279}
280
281fn outside_extent_distance(coordinate: f64, extent: f64) -> f64 {
282 if coordinate < 0.0 {
283 -coordinate
284 } else if coordinate > extent {
285 coordinate - extent
286 } else {
287 0.0
288 }
289}
290
291fn conservative_envelope_change(
292 distance: f64,
293 plane: &SamplingPlane,
294) -> Result<f64, InterferenceError> {
295 let cell_diagonal = finite_metric(
296 "sampling-cell-diagonal-m",
297 plane.cell_size_u_m().hypot(plane.cell_size_v_m()),
298 )?;
299 let value = cell_diagonal / distance;
300 if distance == 0.0 || !value.is_finite() {
301 Err(InterferenceError::UnboundedSamplingEnvelope {
302 nearest_point_source_distance_m: distance,
303 cell_diagonal_m: cell_diagonal,
304 })
305 } else {
306 Ok(value)
307 }
308}