Skip to main content

sim_lib_interference_core/
error.rs

1//! Stable public diagnostics for interference-domain validation.
2
3use std::fmt;
4
5use crate::{SamplingCertificate, WorkMetric};
6
7/// An invalid value at an interference-domain boundary.
8#[derive(Clone, Debug, PartialEq)]
9pub enum InterferenceError {
10    /// A quantity violated its finite, sign, or zero contract.
11    InvalidQuantity {
12        /// Stable public quantity name.
13        name: &'static str,
14        /// Rejected caller-supplied value.
15        value: f64,
16    },
17    /// A direction was zero-length or had a non-finite component.
18    InvalidDirection {
19        /// Rejected x component.
20        x: f64,
21        /// Rejected y component.
22        y: f64,
23        /// Rejected z component.
24        z: f64,
25    },
26    /// A coherent problem was given no sources.
27    EmptySourceSet,
28    /// A source identity was empty.
29    EmptySourceId,
30    /// More than one source used the same stable identity.
31    DuplicateSourceId {
32        /// Repeated source identity.
33        id: String,
34    },
35    /// A finite input combination produced a non-finite propagation value.
36    NonFinitePropagation {
37        /// Source being evaluated.
38        source_id: String,
39        /// Stable name for the derived value.
40        name: &'static str,
41        /// Rejected derived value.
42        value: f64,
43    },
44    /// A point-source sample entered its excluded singular region.
45    SingularPointSample {
46        /// Point-source identity.
47        source_id: String,
48        /// Sample distance from the point source.
49        distance_metres: f64,
50        /// Inclusive exclusion radius.
51        singularity_radius_metres: f64,
52    },
53    /// A sample was behind a forward-plane emitter.
54    BehindForwardPlane {
55        /// Forward-plane source identity.
56        source_id: String,
57        /// Negative signed distance from the source plane.
58        signed_distance_metres: f64,
59    },
60    /// Sampling-plane axes were not orthogonal within the public tolerance.
61    NonOrthogonalSamplingAxes {
62        /// Dot product of the normalized axes.
63        dot_product: f64,
64        /// Largest accepted absolute dot product.
65        max_abs_dot_product: f64,
66    },
67    /// A sampling-plane dimension was zero.
68    ZeroSamplingDimension {
69        /// Stable dimension name (`rows` or `columns`).
70        name: &'static str,
71    },
72    /// The sampling-plane cell count could not be represented.
73    SamplingCellCountOverflow {
74        /// Requested row count.
75        rows: usize,
76        /// Requested column count.
77        columns: usize,
78    },
79    /// A derived sampling-plane cell size was not positive and finite.
80    InvalidSamplingCellSize {
81        /// Stable axis name (`u` or `v`).
82        axis: &'static str,
83        /// Rejected derived cell size in metres.
84        value: f64,
85    },
86    /// A requested sampling cell lies outside the plane.
87    SamplingCellOutOfBounds {
88        /// Requested row.
89        row: usize,
90        /// Requested column.
91        column: usize,
92        /// Plane row count.
93        rows: usize,
94        /// Plane column count.
95        columns: usize,
96    },
97    /// A finite sampling input combination produced a non-finite metric.
98    NonFiniteSamplingMetric {
99        /// Stable metric name.
100        name: &'static str,
101        /// Rejected derived value.
102        value: f64,
103    },
104    /// A sampling classification threshold was not finite and in-range.
105    InvalidSamplingThreshold {
106        /// Stable threshold field name.
107        name: &'static str,
108        /// Rejected caller-supplied value.
109        value: f64,
110    },
111    /// Sampling thresholds did not order resolved before marginal.
112    InconsistentSamplingThresholds {
113        /// Comfortable carrier samples per wavelength.
114        resolved_min_samples_per_wavelength: f64,
115        /// Minimum carrier samples per wavelength.
116        marginal_min_samples_per_wavelength: f64,
117        /// Comfortable envelope fraction.
118        resolved_max_envelope_fraction_per_cell: f64,
119        /// Maximum marginal envelope fraction.
120        marginal_max_envelope_fraction_per_cell: f64,
121    },
122    /// Strict sampling policy rejected a non-resolved certificate.
123    SamplingRefused {
124        /// Complete measurements and thresholds that caused refusal.
125        certificate: SamplingCertificate,
126    },
127    /// A point source touched the plane or exceeded finite envelope range.
128    UnboundedSamplingEnvelope {
129        /// Nearest distance from a point source to the finite plane.
130        nearest_point_source_distance_m: f64,
131        /// Full diagonal of one sampling cell.
132        cell_diagonal_m: f64,
133    },
134    /// Checked arithmetic could not represent a work-estimate metric.
135    WorkEstimateOverflow {
136        /// Metric whose derivation overflowed.
137        metric: WorkMetric,
138    },
139    /// A work estimate exceeded one explicit budget field.
140    WorkBudgetExceeded {
141        /// Metric whose limit was exceeded.
142        metric: WorkMetric,
143        /// Requested work.
144        estimate: u64,
145        /// Configured maximum.
146        limit: u64,
147    },
148}
149
150impl fmt::Display for InterferenceError {
151    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
152        match self {
153            Self::InvalidQuantity { name, value } => {
154                write!(formatter, "invalid quantity `{name}`: {value:?}")
155            }
156            Self::InvalidDirection { x, y, z } => {
157                write!(formatter, "invalid unit direction: [{x:?}, {y:?}, {z:?}]")
158            }
159            Self::EmptySourceSet => formatter.write_str("a source set cannot be empty"),
160            Self::EmptySourceId => formatter.write_str("a source id cannot be empty"),
161            Self::DuplicateSourceId { id } => {
162                write!(formatter, "duplicate source id `{id}`")
163            }
164            Self::NonFinitePropagation {
165                source_id,
166                name,
167                value,
168            } => write!(
169                formatter,
170                "source `{source_id}` produced non-finite `{name}`: {value:?}"
171            ),
172            Self::SingularPointSample {
173                source_id,
174                distance_metres,
175                singularity_radius_metres,
176            } => write!(
177                formatter,
178                "sample is singular for point source `{source_id}`: distance \
179                 {distance_metres:?} m is at or inside radius \
180                 {singularity_radius_metres:?} m"
181            ),
182            Self::BehindForwardPlane {
183                source_id,
184                signed_distance_metres,
185            } => write!(
186                formatter,
187                "sample is behind forward-plane source `{source_id}`: signed \
188                 distance {signed_distance_metres:?} m"
189            ),
190            Self::NonOrthogonalSamplingAxes {
191                dot_product,
192                max_abs_dot_product,
193            } => write!(
194                formatter,
195                "sampling axes are not orthogonal: dot product \
196                 {dot_product:?} exceeds {max_abs_dot_product:?}"
197            ),
198            Self::ZeroSamplingDimension { name } => {
199                write!(formatter, "sampling dimension `{name}` must be non-zero")
200            }
201            Self::SamplingCellCountOverflow { rows, columns } => write!(
202                formatter,
203                "sampling cell count overflows usize: {rows} rows by {columns} columns"
204            ),
205            Self::InvalidSamplingCellSize { axis, value } => write!(
206                formatter,
207                "sampling cell size on `{axis}` must be positive and finite: {value:?} m"
208            ),
209            Self::SamplingCellOutOfBounds {
210                row,
211                column,
212                rows,
213                columns,
214            } => write!(
215                formatter,
216                "sampling cell ({row}, {column}) is outside {rows} rows by {columns} columns"
217            ),
218            Self::NonFiniteSamplingMetric { name, value } => {
219                write!(
220                    formatter,
221                    "sampling metric `{name}` is non-finite: {value:?}"
222                )
223            }
224            Self::InvalidSamplingThreshold { name, value } => {
225                write!(formatter, "invalid sampling threshold `{name}`: {value:?}")
226            }
227            Self::InconsistentSamplingThresholds {
228                resolved_min_samples_per_wavelength,
229                marginal_min_samples_per_wavelength,
230                resolved_max_envelope_fraction_per_cell,
231                marginal_max_envelope_fraction_per_cell,
232            } => write!(
233                formatter,
234                "sampling thresholds are inconsistent: resolved carrier minimum \
235                 {resolved_min_samples_per_wavelength:?} must be at least marginal \
236                 minimum {marginal_min_samples_per_wavelength:?}, and resolved \
237                 envelope maximum {resolved_max_envelope_fraction_per_cell:?} must \
238                 not exceed marginal maximum {marginal_max_envelope_fraction_per_cell:?}"
239            ),
240            Self::SamplingRefused { certificate } => write!(
241                formatter,
242                "strict sampling refused {:?}: carrier samples/wavelength \
243                 u={:?}, v={:?}; power-fringe samples/period u={:?}, v={:?}; \
244                 envelope fraction/cell={:?}",
245                certificate.verdict,
246                certificate.samples_per_wavelength_u,
247                certificate.samples_per_wavelength_v,
248                certificate.samples_per_power_fringe_u,
249                certificate.samples_per_power_fringe_v,
250                certificate.max_envelope_fraction_per_cell,
251            ),
252            Self::UnboundedSamplingEnvelope {
253                nearest_point_source_distance_m,
254                cell_diagonal_m,
255            } => write!(
256                formatter,
257                "point-source envelope bound is not finite: nearest plane distance \
258                 {nearest_point_source_distance_m:?} m, cell diagonal \
259                 {cell_diagonal_m:?} m"
260            ),
261            Self::WorkEstimateOverflow { metric } => {
262                write!(formatter, "work estimate for {metric} overflowed u64")
263            }
264            Self::WorkBudgetExceeded {
265                metric,
266                estimate,
267                limit,
268            } => write!(
269                formatter,
270                "work budget exceeded for {metric}: estimate {estimate}, limit {limit}"
271            ),
272        }
273    }
274}
275
276impl std::error::Error for InterferenceError {}