1use sim_lib_interference_core::{
4 Emitter, FieldAmplitude, Hertz, InterferenceError, InterferenceProblem, Point3M,
5 PositiveMetres, Radians, ScalarMedium, SourceSet, UnitVector3,
6};
7
8use crate::ScenarioError;
9use crate::scenario_admission::{
10 IdPlan, allocate_sources, require_dimension, require_limit, sources_from, sources_from_fallible,
11};
12
13pub const ABSOLUTE_MAX_SCENARIO_SOURCES: usize = 1_000_000;
15pub const ABSOLUTE_MAX_GENERATED_ID_BYTES: usize = 1_024;
17pub const ABSOLUTE_MAX_TOTAL_ID_BYTES: usize = 64 * 1_024 * 1_024;
19pub const STRICT_MAX_SPACING_WAVELENGTHS: f64 = 0.5;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct ScenarioLimits {
25 pub(crate) max_sources: usize,
26 pub(crate) max_generated_id_bytes: usize,
27 pub(crate) max_total_id_bytes: usize,
28}
29
30impl ScenarioLimits {
31 pub fn new(
33 max_sources: usize,
34 max_generated_id_bytes: usize,
35 max_total_id_bytes: usize,
36 ) -> Result<Self, ScenarioError> {
37 require_limit("max-sources", max_sources, ABSOLUTE_MAX_SCENARIO_SOURCES)?;
38 require_limit(
39 "max-generated-id-bytes",
40 max_generated_id_bytes,
41 ABSOLUTE_MAX_GENERATED_ID_BYTES,
42 )?;
43 require_limit(
44 "max-total-id-bytes",
45 max_total_id_bytes,
46 ABSOLUTE_MAX_TOTAL_ID_BYTES,
47 )?;
48 Ok(Self {
49 max_sources,
50 max_generated_id_bytes,
51 max_total_id_bytes,
52 })
53 }
54
55 pub fn max_sources(self) -> usize {
57 self.max_sources
58 }
59
60 pub fn max_generated_id_bytes(self) -> usize {
62 self.max_generated_id_bytes
63 }
64
65 pub fn max_total_id_bytes(self) -> usize {
67 self.max_total_id_bytes
68 }
69}
70
71impl Default for ScenarioLimits {
72 fn default() -> Self {
73 Self {
74 max_sources: 4_096,
75 max_generated_id_bytes: 96,
76 max_total_id_bytes: 256 * 1_024,
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub enum AperturePolicy {
84 #[default]
86 Strict,
87 Annotate,
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum ScenarioKind {
94 TwoPoint,
96 CounterPropagatingPlanes,
98 PhasedArray,
100 DiscreteAperture,
102}
103
104#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct ElementSpacingWavelengths {
107 pub u: Option<f64>,
109 pub v: Option<f64>,
111}
112
113impl ElementSpacingWavelengths {
114 pub fn maximum(self) -> Option<f64> {
116 match (self.u, self.v) {
117 (Some(u), Some(v)) => Some(u.max(v)),
118 (Some(value), None) | (None, Some(value)) => Some(value),
119 (None, None) => None,
120 }
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq)]
126pub struct ScenarioCertificate {
127 pub kind: ScenarioKind,
129 pub source_count: usize,
131 pub total_source_amplitude: f64,
135 pub amplitude_per_source: f64,
137 pub element_spacing_wavelengths: ElementSpacingWavelengths,
139 pub aperture_policy: Option<AperturePolicy>,
141}
142
143#[derive(Clone, Debug, PartialEq)]
145pub struct NamedScenario {
146 problem: InterferenceProblem,
147 certificate: ScenarioCertificate,
148}
149
150impl NamedScenario {
151 pub fn problem(&self) -> &InterferenceProblem {
153 &self.problem
154 }
155
156 pub fn certificate(&self) -> ScenarioCertificate {
158 self.certificate
159 }
160
161 pub fn into_parts(self) -> (InterferenceProblem, ScenarioCertificate) {
163 (self.problem, self.certificate)
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq)]
169pub struct ScenarioBuilder {
170 frequency: Hertz,
171 medium: ScalarMedium,
172 singularity_radius: PositiveMetres,
173 limits: ScenarioLimits,
174}
175
176impl ScenarioBuilder {
177 pub fn new(
179 frequency: Hertz,
180 medium: ScalarMedium,
181 singularity_radius: PositiveMetres,
182 limits: ScenarioLimits,
183 ) -> Self {
184 Self {
185 frequency,
186 medium,
187 singularity_radius,
188 limits,
189 }
190 }
191
192 pub fn two_point(
194 self,
195 first: Point3M,
196 second: Point3M,
197 amplitude_per_source: FieldAmplitude,
198 relative_phase: Radians,
199 ) -> Result<NamedScenario, ScenarioError> {
200 let plan = IdPlan::admit("scenario/two-point/", 2, self.limits)?;
201 let total_amplitude = checked_total_amplitude(amplitude_per_source, 2)?;
202 let phases = [Radians::new(0.0)?, relative_phase];
203 let positions = [first, second];
204 let sources = sources_from(plan, |index, id| Emitter::Point {
205 id,
206 position: positions[index],
207 amplitude_at_reference: amplitude_per_source,
208 phase: phases[index],
209 })?;
210 self.finish(
211 ScenarioKind::TwoPoint,
212 sources,
213 total_amplitude,
214 amplitude_per_source.get(),
215 ElementSpacingWavelengths { u: None, v: None },
216 None,
217 )
218 }
219
220 pub fn counter_propagating_planes(
222 self,
223 centre: Point3M,
224 axis: UnitVector3,
225 source_separation: PositiveMetres,
226 amplitude_per_source: FieldAmplitude,
227 relative_phase: Radians,
228 ) -> Result<NamedScenario, ScenarioError> {
229 let plan = IdPlan::admit("scenario/counter-plane/", 2, self.limits)?;
230 let total_amplitude = checked_total_amplitude(amplitude_per_source, 2)?;
231 let mut sources = allocate_sources(plan.count)?;
232 let half_separation = source_separation.get() / 2.0;
233 let first = offset_point(centre, axis, -half_separation)?;
234 let second = offset_point(centre, axis, half_separation)?;
235 let [x, y, z] = axis.components();
236 let reverse = UnitVector3::new(-x, -y, -z)?;
237 sources.push(Emitter::ForwardPlane {
238 id: plan.id(0),
239 through: first,
240 direction: axis,
241 amplitude: amplitude_per_source,
242 phase: Radians::new(0.0)?,
243 });
244 sources.push(Emitter::ForwardPlane {
245 id: plan.id(1),
246 through: second,
247 direction: reverse,
248 amplitude: amplitude_per_source,
249 phase: relative_phase,
250 });
251 self.finish(
252 ScenarioKind::CounterPropagatingPlanes,
253 sources,
254 total_amplitude,
255 amplitude_per_source.get(),
256 ElementSpacingWavelengths { u: None, v: None },
257 None,
258 )
259 }
260
261 #[allow(clippy::too_many_arguments)]
263 pub fn phased_array(
264 self,
265 centre: Point3M,
266 element_axis: UnitVector3,
267 elements: usize,
268 spacing: PositiveMetres,
269 total_amplitude: FieldAmplitude,
270 first_phase: Radians,
271 progressive_phase: Radians,
272 policy: AperturePolicy,
273 ) -> Result<NamedScenario, ScenarioError> {
274 require_dimension("elements", elements)?;
275 let plan = IdPlan::admit("scenario/phased-array/", elements, self.limits)?;
276 let reported_spacing = self.spacing((elements > 1).then_some(spacing), None, policy)?;
277 let amplitude = normalized_amplitude(total_amplitude, elements)?;
278 let sources = sources_from_fallible(plan, |index, id| {
279 let offset = centred_offset(index, elements, spacing.get())?;
280 Ok(Emitter::Point {
281 id,
282 position: offset_point(centre, element_axis, offset)?,
283 amplitude_at_reference: amplitude,
284 phase: Radians::new(first_phase.get() + index as f64 * progressive_phase.get())?,
285 })
286 })?;
287 self.finish(
288 ScenarioKind::PhasedArray,
289 sources,
290 total_amplitude.get(),
291 amplitude.get(),
292 reported_spacing,
293 Some(policy),
294 )
295 }
296
297 #[allow(clippy::too_many_arguments)]
299 pub fn discrete_aperture(
300 self,
301 centre: Point3M,
302 u_axis: UnitVector3,
303 v_axis: UnitVector3,
304 rows: usize,
305 columns: usize,
306 spacing_u: PositiveMetres,
307 spacing_v: PositiveMetres,
308 total_amplitude: FieldAmplitude,
309 phase: Radians,
310 policy: AperturePolicy,
311 ) -> Result<NamedScenario, ScenarioError> {
312 require_dimension("rows", rows)?;
313 require_dimension("columns", columns)?;
314 let count = rows
315 .checked_mul(columns)
316 .ok_or(ScenarioError::SourceCountOverflow { rows, columns })?;
317 let plan = IdPlan::admit("scenario/aperture/", count, self.limits)?;
318 require_orthogonal_axes(u_axis, v_axis)?;
319 let spacing = self.spacing(
320 (columns > 1).then_some(spacing_u),
321 (rows > 1).then_some(spacing_v),
322 policy,
323 )?;
324 let amplitude = normalized_amplitude(total_amplitude, count)?;
325 let sources = sources_from_fallible(plan, |index, id| {
326 let row = index / columns;
327 let column = index % columns;
328 let offset_u = centred_offset(column, columns, spacing_u.get())?;
329 let offset_v = centred_offset(row, rows, spacing_v.get())?;
330 let along_u = offset_point(centre, u_axis, offset_u)?;
331 Ok(Emitter::Point {
332 id,
333 position: offset_point(along_u, v_axis, offset_v)?,
334 amplitude_at_reference: amplitude,
335 phase,
336 })
337 })?;
338 self.finish(
339 ScenarioKind::DiscreteAperture,
340 sources,
341 total_amplitude.get(),
342 amplitude.get(),
343 spacing,
344 Some(policy),
345 )
346 }
347
348 fn spacing(
349 self,
350 u: Option<PositiveMetres>,
351 v: Option<PositiveMetres>,
352 policy: AperturePolicy,
353 ) -> Result<ElementSpacingWavelengths, ScenarioError> {
354 let wavelength = self.medium.speed().get() / self.frequency.get();
355 let spacing = ElementSpacingWavelengths {
356 u: relative_spacing(u, wavelength)?,
357 v: relative_spacing(v, wavelength)?,
358 };
359 for (axis, value) in [("u", spacing.u), ("v", spacing.v)] {
360 if let Some(value) = value
361 && policy == AperturePolicy::Strict
362 && value > STRICT_MAX_SPACING_WAVELENGTHS
363 {
364 return Err(ScenarioError::SparseAperture {
365 axis,
366 spacing_wavelengths: value,
367 maximum_wavelengths: STRICT_MAX_SPACING_WAVELENGTHS,
368 });
369 }
370 }
371 Ok(spacing)
372 }
373
374 fn finish(
375 self,
376 kind: ScenarioKind,
377 sources: Vec<Emitter>,
378 total_source_amplitude: f64,
379 amplitude_per_source: f64,
380 element_spacing_wavelengths: ElementSpacingWavelengths,
381 aperture_policy: Option<AperturePolicy>,
382 ) -> Result<NamedScenario, ScenarioError> {
383 let source_count = sources.len();
384 let problem = InterferenceProblem::new(
385 self.frequency,
386 self.medium,
387 SourceSet::new(sources)?,
388 self.singularity_radius,
389 );
390 Ok(NamedScenario {
391 problem,
392 certificate: ScenarioCertificate {
393 kind,
394 source_count,
395 total_source_amplitude,
396 amplitude_per_source,
397 element_spacing_wavelengths,
398 aperture_policy,
399 },
400 })
401 }
402}
403
404fn normalized_amplitude(
405 total: FieldAmplitude,
406 count: usize,
407) -> Result<FieldAmplitude, ScenarioError> {
408 FieldAmplitude::new(total.get() / count as f64).map_err(ScenarioError::from)
409}
410
411fn checked_total_amplitude(per_source: FieldAmplitude, count: usize) -> Result<f64, ScenarioError> {
412 FieldAmplitude::new(per_source.get() * count as f64)
413 .map(FieldAmplitude::get)
414 .map_err(ScenarioError::from)
415}
416
417fn centred_offset(index: usize, count: usize, spacing_metres: f64) -> Result<f64, ScenarioError> {
418 let offset = (index as f64 - (count - 1) as f64 / 2.0) * spacing_metres;
419 if offset.is_finite() {
420 Ok(offset)
421 } else {
422 Err(InterferenceError::InvalidQuantity {
423 name: "scenario-offset-m",
424 value: offset,
425 }
426 .into())
427 }
428}
429
430fn offset_point(
431 point: Point3M,
432 axis: UnitVector3,
433 distance_metres: f64,
434) -> Result<Point3M, ScenarioError> {
435 let [x, y, z] = point.coordinates_metres();
436 let [axis_x, axis_y, axis_z] = axis.components();
437 Point3M::from_metres(
438 x + distance_metres * axis_x,
439 y + distance_metres * axis_y,
440 z + distance_metres * axis_z,
441 )
442 .map_err(ScenarioError::from)
443}
444
445fn require_orthogonal_axes(u_axis: UnitVector3, v_axis: UnitVector3) -> Result<(), ScenarioError> {
446 let [ux, uy, uz] = u_axis.components();
447 let [vx, vy, vz] = v_axis.components();
448 let dot_product = ux * vx + uy * vy + uz * vz;
449 if dot_product.abs() > sim_lib_interference_core::SAMPLING_AXIS_ORTHOGONALITY_TOLERANCE {
450 Err(InterferenceError::NonOrthogonalSamplingAxes {
451 dot_product,
452 max_abs_dot_product: sim_lib_interference_core::SAMPLING_AXIS_ORTHOGONALITY_TOLERANCE,
453 }
454 .into())
455 } else {
456 Ok(())
457 }
458}
459
460fn relative_spacing(
461 spacing: Option<PositiveMetres>,
462 wavelength_metres: f64,
463) -> Result<Option<f64>, ScenarioError> {
464 let Some(spacing) = spacing else {
465 return Ok(None);
466 };
467 let relative = spacing.get() / wavelength_metres;
468 if relative.is_finite() && relative > 0.0 {
469 Ok(Some(relative))
470 } else {
471 Err(InterferenceError::InvalidQuantity {
472 name: "scenario-spacing-wavelengths",
473 value: relative,
474 }
475 .into())
476 }
477}
478
479#[cfg(test)]
480#[path = "scenario_tests.rs"]
481mod tests;