1use std::sync::Arc;
4
5use sim_kernel::{Cx, Result, Symbol, Value};
6use sim_lib_interference_solve::{
7 LossClass, Observable, ProjectionCertificate, ReductionRule, ScalarProjection, ScalarSample,
8};
9use sim_lib_numbers_tensor::{Tensor, TensorLocation, TypedTensorStorage, domains};
10
11use crate::{
12 PhasorFieldDescriptor, SamplingCertificateDescriptor,
13 citizen::{RecordCitizenSpec, decode_record, encode_field, encode_record, invalid, next_field},
14 evidence::SamplingCertificateDescriptor as SamplingDescriptor,
15 tensor_bridge::{decode_tensor, tensor_eq, tensor_expr},
16};
17
18#[derive(Clone, Debug, PartialEq)]
20pub struct ProjectionRequestDescriptor {
21 pub observable: Symbol,
23 pub angular_time_rad: Option<f64>,
25 pub phase_floor: f64,
27 pub target_rows: usize,
29 pub target_columns: usize,
31 pub reduction: Symbol,
33}
34
35impl ProjectionRequestDescriptor {
36 pub fn new(
38 observable: Observable,
39 phase_floor: f64,
40 target_rows: usize,
41 target_columns: usize,
42 reduction: ReductionRule,
43 ) -> Result<Self> {
44 let (observable, angular_time_rad) = encode_observable(observable);
45 let value = Self {
46 observable,
47 angular_time_rad,
48 phase_floor,
49 target_rows,
50 target_columns,
51 reduction: reduction_symbol(reduction),
52 };
53 value.validate()?;
54 Ok(value)
55 }
56
57 pub fn observable(&self) -> Result<Observable> {
59 decode_observable(&self.observable, self.angular_time_rad)
60 }
61
62 pub fn reduction(&self) -> Result<ReductionRule> {
64 decode_reduction(&self.reduction)
65 }
66}
67
68impl RecordCitizenSpec for ProjectionRequestDescriptor {
69 const FIELDS: &'static [&'static str] = &[
70 "observable",
71 "angular-time-rad",
72 "phase-floor",
73 "target-rows",
74 "target-columns",
75 "reduction",
76 ];
77
78 fn encode_fields(&self, _cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
79 Ok(vec![
80 encode_field(&self.observable),
81 encode_field(&self.angular_time_rad),
82 encode_field(&self.phase_floor),
83 encode_field(&self.target_rows),
84 encode_field(&self.target_columns),
85 encode_field(&self.reduction),
86 ])
87 }
88
89 fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
90 let mut fields = fields.into_iter();
91 let value = Self {
92 observable: next_field(cx, &mut fields, "observable")?,
93 angular_time_rad: next_field(cx, &mut fields, "angular-time-rad")?,
94 phase_floor: next_field(cx, &mut fields, "phase-floor")?,
95 target_rows: next_field(cx, &mut fields, "target-rows")?,
96 target_columns: next_field(cx, &mut fields, "target-columns")?,
97 reduction: next_field(cx, &mut fields, "reduction")?,
98 };
99 value.validate()?;
100 Ok(value)
101 }
102
103 fn example() -> Self {
104 Self::new(Observable::Amplitude, 0.0, 2, 2, ReductionRule::Detail)
105 .expect("example projection request")
106 }
107
108 fn validate(&self) -> Result<()> {
109 let observable = self.observable()?;
110 let reduction = self.reduction()?;
111 if !self.phase_floor.is_finite() || self.phase_floor < 0.0 {
112 return Err(invalid(
113 "ProjectionRequest",
114 "phase floor must be finite and non-negative",
115 ));
116 }
117 if self.target_rows == 0 || self.target_columns == 0 {
118 return Err(invalid(
119 "ProjectionRequest",
120 "target dimensions must be non-zero",
121 ));
122 }
123 compatible_reduction(observable, reduction)
124 }
125}
126
127impl_record_citizen!(
128 ProjectionRequestDescriptor,
129 "interference/ProjectionRequest",
130 6
131);
132
133#[derive(Clone, Debug, PartialEq)]
135pub struct ProjectionCertificateDescriptor {
136 pub source_rows: usize,
138 pub source_columns: usize,
140 pub target_rows: usize,
142 pub target_columns: usize,
144 pub footprint_min_rows: usize,
146 pub footprint_max_rows: usize,
148 pub footprint_min_columns: usize,
150 pub footprint_max_columns: usize,
152 pub observable: Symbol,
154 pub angular_time_rad: Option<f64>,
156 pub phase_floor: f64,
158 pub reduction: Symbol,
160 pub loss_class: Symbol,
162 pub source_sampling: SamplingCertificateDescriptor,
164 pub mask_count: usize,
166}
167
168impl ProjectionCertificateDescriptor {
169 pub fn from_certificate(certificate: ProjectionCertificate) -> Self {
171 let source = certificate.source_dimensions();
172 let target = certificate.target_dimensions();
173 let footprint = certificate.footprint();
174 let (observable, angular_time_rad) = encode_observable(certificate.observable());
175 Self {
176 source_rows: source.rows(),
177 source_columns: source.columns(),
178 target_rows: target.rows(),
179 target_columns: target.columns(),
180 footprint_min_rows: footprint.min_rows(),
181 footprint_max_rows: footprint.max_rows(),
182 footprint_min_columns: footprint.min_columns(),
183 footprint_max_columns: footprint.max_columns(),
184 observable,
185 angular_time_rad,
186 phase_floor: certificate.phase_floor(),
187 reduction: reduction_symbol(certificate.rule()),
188 loss_class: loss_symbol(certificate.loss_class()),
189 source_sampling: SamplingDescriptor::from_certificate(
190 certificate.source_sampling_certificate(),
191 ),
192 mask_count: certificate.mask_count(),
193 }
194 }
195
196 pub fn observable(&self) -> Result<Observable> {
198 decode_observable(&self.observable, self.angular_time_rad)
199 }
200
201 pub fn reduction(&self) -> Result<ReductionRule> {
203 decode_reduction(&self.reduction)
204 }
205}
206
207impl RecordCitizenSpec for ProjectionCertificateDescriptor {
208 const FIELDS: &'static [&'static str] = &[
209 "source-rows",
210 "source-columns",
211 "target-rows",
212 "target-columns",
213 "footprint-min-rows",
214 "footprint-max-rows",
215 "footprint-min-columns",
216 "footprint-max-columns",
217 "observable",
218 "angular-time-rad",
219 "phase-floor",
220 "reduction",
221 "loss-class",
222 "source-sampling",
223 "mask-count",
224 ];
225
226 fn encode_fields(&self, cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
227 Ok(vec![
228 encode_field(&self.source_rows),
229 encode_field(&self.source_columns),
230 encode_field(&self.target_rows),
231 encode_field(&self.target_columns),
232 encode_field(&self.footprint_min_rows),
233 encode_field(&self.footprint_max_rows),
234 encode_field(&self.footprint_min_columns),
235 encode_field(&self.footprint_max_columns),
236 encode_field(&self.observable),
237 encode_field(&self.angular_time_rad),
238 encode_field(&self.phase_floor),
239 encode_field(&self.reduction),
240 encode_field(&self.loss_class),
241 encode_record(cx, &self.source_sampling)?,
242 encode_field(&self.mask_count),
243 ])
244 }
245
246 fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
247 let mut fields = fields.into_iter();
248 let source_rows = next_field(cx, &mut fields, "source-rows")?;
249 let source_columns = next_field(cx, &mut fields, "source-columns")?;
250 let target_rows = next_field(cx, &mut fields, "target-rows")?;
251 let target_columns = next_field(cx, &mut fields, "target-columns")?;
252 let footprint_min_rows = next_field(cx, &mut fields, "footprint-min-rows")?;
253 let footprint_max_rows = next_field(cx, &mut fields, "footprint-max-rows")?;
254 let footprint_min_columns = next_field(cx, &mut fields, "footprint-min-columns")?;
255 let footprint_max_columns = next_field(cx, &mut fields, "footprint-max-columns")?;
256 let observable = next_field(cx, &mut fields, "observable")?;
257 let angular_time_rad = next_field(cx, &mut fields, "angular-time-rad")?;
258 let phase_floor = next_field(cx, &mut fields, "phase-floor")?;
259 let reduction = next_field(cx, &mut fields, "reduction")?;
260 let loss_class = next_field(cx, &mut fields, "loss-class")?;
261 let source_sampling = decode_record(
262 cx,
263 fields
264 .next()
265 .ok_or_else(|| invalid("ProjectionCertificate", "missing source sampling"))?,
266 "source-sampling",
267 )?;
268 let mask_count = next_field(cx, &mut fields, "mask-count")?;
269 let value = Self {
270 source_rows,
271 source_columns,
272 target_rows,
273 target_columns,
274 footprint_min_rows,
275 footprint_max_rows,
276 footprint_min_columns,
277 footprint_max_columns,
278 observable,
279 angular_time_rad,
280 phase_floor,
281 reduction,
282 loss_class,
283 source_sampling,
284 mask_count,
285 };
286 value.validate()?;
287 Ok(value)
288 }
289
290 fn example() -> Self {
291 sample_projection().certificate
292 }
293
294 fn validate(&self) -> Result<()> {
295 if self.source_rows == 0
296 || self.source_columns == 0
297 || self.target_rows == 0
298 || self.target_columns == 0
299 || self.target_rows > self.source_rows
300 || self.target_columns > self.source_columns
301 {
302 return Err(invalid(
303 "ProjectionCertificate",
304 "target dimensions must be non-zero and no larger than source dimensions",
305 ));
306 }
307 let expected_footprint = (
308 self.source_rows / self.target_rows,
309 self.source_rows.div_ceil(self.target_rows),
310 self.source_columns / self.target_columns,
311 self.source_columns.div_ceil(self.target_columns),
312 );
313 if (
314 self.footprint_min_rows,
315 self.footprint_max_rows,
316 self.footprint_min_columns,
317 self.footprint_max_columns,
318 ) != expected_footprint
319 {
320 return Err(invalid(
321 "ProjectionCertificate",
322 "detector footprint does not match the integer axis partitions",
323 ));
324 }
325 let observable = self.observable()?;
326 let reduction = self.reduction()?;
327 compatible_reduction(observable, reduction)?;
328 if !self.phase_floor.is_finite() || self.phase_floor < 0.0 {
329 return Err(invalid(
330 "ProjectionCertificate",
331 "phase floor must be finite and non-negative",
332 ));
333 }
334 self.source_sampling.to_certificate()?;
335 let expected_loss =
336 if self.source_rows == self.target_rows && self.source_columns == self.target_columns {
337 loss_symbol(LossClass::Lossless)
338 } else {
339 loss_symbol(LossClass::DetectorIntegration)
340 };
341 if self.loss_class != expected_loss {
342 return Err(invalid(
343 "ProjectionCertificate",
344 "loss class does not match source and target dimensions",
345 ));
346 }
347 let cells = self
348 .target_rows
349 .checked_mul(self.target_columns)
350 .ok_or_else(|| invalid("ProjectionCertificate", "target cell count overflowed"))?;
351 if self.mask_count > cells || (observable != Observable::Phase && self.mask_count != 0) {
352 return Err(invalid(
353 "ProjectionCertificate",
354 "mask count is incompatible with the target or observable",
355 ));
356 }
357 Ok(())
358 }
359}
360
361impl_record_citizen!(
362 ProjectionCertificateDescriptor,
363 "interference/ProjectionCertificate",
364 15
365);
366
367#[derive(Clone)]
369pub struct ScalarProjectionDescriptor {
370 pub rows: usize,
372 pub columns: usize,
374 pub values: Tensor,
376 pub mask: Vec<bool>,
378 pub certificate: ProjectionCertificateDescriptor,
380}
381
382impl ScalarProjectionDescriptor {
383 pub fn from_projection(projection: &ScalarProjection) -> Result<Self> {
385 let rows = projection.rows();
386 let columns = projection.columns();
387 let mut values = Vec::with_capacity(projection.samples().len());
388 let mut mask = Vec::with_capacity(projection.samples().len());
389 for sample in projection.samples() {
390 match sample {
391 ScalarSample::Value(value) => {
392 values.push(*value);
393 mask.push(false);
394 }
395 ScalarSample::Masked => {
396 values.push(0.0);
397 mask.push(true);
398 }
399 }
400 }
401 let tensor = Tensor::from_storage(
402 vec![rows, columns],
403 domains::f64(),
404 Arc::new(TypedTensorStorage::<f64>::new(values)),
405 )?;
406 let value = Self {
407 rows,
408 columns,
409 values: tensor,
410 mask,
411 certificate: ProjectionCertificateDescriptor::from_certificate(
412 projection.certificate(),
413 ),
414 };
415 value.validate()?;
416 Ok(value)
417 }
418}
419
420impl core::fmt::Debug for ScalarProjectionDescriptor {
421 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
422 formatter
423 .debug_struct("ScalarProjectionDescriptor")
424 .field("rows", &self.rows)
425 .field("columns", &self.columns)
426 .field("shape", &self.values.shape())
427 .field("dtype", &self.values.dtype())
428 .field("location", &self.values.location())
429 .field("mask", &self.mask)
430 .field("certificate", &self.certificate)
431 .finish()
432 }
433}
434
435impl PartialEq for ScalarProjectionDescriptor {
436 fn eq(&self, other: &Self) -> bool {
437 self.rows == other.rows
438 && self.columns == other.columns
439 && tensor_eq(&self.values, &other.values)
440 && self.mask == other.mask
441 && self.certificate == other.certificate
442 }
443}
444
445impl RecordCitizenSpec for ScalarProjectionDescriptor {
446 const FIELDS: &'static [&'static str] = &["rows", "columns", "values", "mask", "certificate"];
447
448 fn encode_fields(&self, cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
449 Ok(vec![
450 encode_field(&self.rows),
451 encode_field(&self.columns),
452 tensor_expr(cx, &self.values)?,
453 encode_field(&self.mask),
454 encode_record(cx, &self.certificate)?,
455 ])
456 }
457
458 fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
459 let mut fields = fields.into_iter();
460 let rows = next_field(cx, &mut fields, "rows")?;
461 let columns = next_field(cx, &mut fields, "columns")?;
462 let values = decode_tensor(
463 cx,
464 fields
465 .next()
466 .ok_or_else(|| invalid("Projection", "missing values Tensor"))?,
467 "values",
468 )?;
469 let mask = next_field(cx, &mut fields, "mask")?;
470 let certificate = decode_record(
471 cx,
472 fields
473 .next()
474 .ok_or_else(|| invalid("Projection", "missing certificate"))?,
475 "certificate",
476 )?;
477 let value = Self {
478 rows,
479 columns,
480 values,
481 mask,
482 certificate,
483 };
484 value.validate()?;
485 Ok(value)
486 }
487
488 fn example() -> Self {
489 sample_projection()
490 }
491
492 fn validate(&self) -> Result<()> {
493 let cells = self
494 .rows
495 .checked_mul(self.columns)
496 .ok_or_else(|| invalid("Projection", "rows * columns overflowed"))?;
497 if self.rows == 0
498 || self.columns == 0
499 || self.values.shape() != [self.rows, self.columns]
500 || self.values.len() != cells
501 {
502 return Err(invalid(
503 "Projection",
504 "Tensor shape must exactly equal non-zero rows and columns",
505 ));
506 }
507 if self.values.dtype() != &domains::f64() && self.values.dtype() != &domains::f32() {
508 return Err(invalid(
509 "Projection",
510 "scalar Tensor dtype must be numbers/f32 or numbers/f64",
511 ));
512 }
513 if self.mask.len() != cells {
514 return Err(invalid(
515 "Projection",
516 "mask length must equal rows * columns",
517 ));
518 }
519 self.certificate.validate()?;
520 if self.certificate.target_rows != self.rows
521 || self.certificate.target_columns != self.columns
522 || self.certificate.mask_count != self.mask.iter().filter(|masked| **masked).count()
523 {
524 return Err(invalid(
525 "Projection",
526 "certificate dimensions or mask count do not match the projection",
527 ));
528 }
529 if self.values.location() == TensorLocation::Host {
530 let paired = PhasorFieldDescriptor::new(
531 self.rows,
532 self.columns,
533 self.values.clone(),
534 self.values.clone(),
535 )?;
536 let mut cx = bare_cx();
537 let host = paired.materialize_host(&mut cx)?;
538 for (index, masked) in self.mask.iter().copied().enumerate() {
539 if masked && host.real()[index] != 0.0 {
540 return Err(invalid(
541 "Projection",
542 "masked scalar Tensor cells must contain canonical zero",
543 ));
544 }
545 }
546 }
547 Ok(())
548 }
549}
550
551impl_record_citizen!(ScalarProjectionDescriptor, "interference/Projection", 5);
552
553fn compatible_reduction(observable: Observable, reduction: ReductionRule) -> Result<()> {
554 let compatible = matches!(
555 (observable, reduction),
556 (_, ReductionRule::Detail)
557 | (
558 Observable::Real
559 | Observable::Imaginary
560 | Observable::Phase
561 | Observable::Instant { .. },
562 ReductionRule::DetectorComplexMean
563 )
564 | (Observable::Amplitude, ReductionRule::DetectorScalarAreaMean)
565 | (
566 Observable::MagnitudeSquared,
567 ReductionRule::DetectorMagnitudeSquaredAreaMean
568 )
569 );
570 compatible.then_some(()).ok_or_else(|| {
571 invalid(
572 "Projection",
573 "observable and detector rule are incompatible",
574 )
575 })
576}
577
578fn encode_observable(observable: Observable) -> (Symbol, Option<f64>) {
579 match observable {
580 Observable::Real => (observable_symbol("real"), None),
581 Observable::Imaginary => (observable_symbol("imaginary"), None),
582 Observable::Amplitude => (observable_symbol("amplitude"), None),
583 Observable::Phase => (observable_symbol("phase"), None),
584 Observable::MagnitudeSquared => (observable_symbol("magnitude-squared"), None),
585 Observable::Instant { wt } => (observable_symbol("instant"), Some(wt)),
586 }
587}
588
589fn decode_observable(symbol: &Symbol, angular_time: Option<f64>) -> Result<Observable> {
590 let value = if *symbol == observable_symbol("real") {
591 Observable::Real
592 } else if *symbol == observable_symbol("imaginary") {
593 Observable::Imaginary
594 } else if *symbol == observable_symbol("amplitude") {
595 Observable::Amplitude
596 } else if *symbol == observable_symbol("phase") {
597 Observable::Phase
598 } else if *symbol == observable_symbol("magnitude-squared") {
599 Observable::MagnitudeSquared
600 } else if *symbol == observable_symbol("instant") {
601 let wt = angular_time
602 .filter(|wt| wt.is_finite())
603 .ok_or_else(|| invalid("Projection", "instant requires finite angular time"))?;
604 return Ok(Observable::Instant { wt });
605 } else {
606 return Err(invalid(
607 "Projection",
608 format!("unknown observable {symbol}"),
609 ));
610 };
611 if angular_time.is_some() {
612 return Err(invalid(
613 "Projection",
614 "angular time is allowed only for instant",
615 ));
616 }
617 Ok(value)
618}
619
620fn reduction_symbol(reduction: ReductionRule) -> Symbol {
621 Symbol::qualified(
622 "interference",
623 match reduction {
624 ReductionRule::Detail => "detail",
625 ReductionRule::DetectorComplexMean => "detector-complex-mean",
626 ReductionRule::DetectorScalarAreaMean => "detector-scalar-area-mean",
627 ReductionRule::DetectorMagnitudeSquaredAreaMean => {
628 "detector-magnitude-squared-area-mean"
629 }
630 },
631 )
632}
633
634fn decode_reduction(symbol: &Symbol) -> Result<ReductionRule> {
635 [
636 ReductionRule::Detail,
637 ReductionRule::DetectorComplexMean,
638 ReductionRule::DetectorScalarAreaMean,
639 ReductionRule::DetectorMagnitudeSquaredAreaMean,
640 ]
641 .into_iter()
642 .find(|rule| reduction_symbol(*rule) == *symbol)
643 .ok_or_else(|| invalid("Projection", format!("unknown reduction rule {symbol}")))
644}
645
646fn loss_symbol(loss: LossClass) -> Symbol {
647 Symbol::qualified(
648 "interference",
649 match loss {
650 LossClass::Lossless => "lossless",
651 LossClass::DetectorIntegration => "detector-integration",
652 },
653 )
654}
655
656fn observable_symbol(name: &str) -> Symbol {
657 Symbol::qualified("interference", name.to_owned())
658}
659
660pub(crate) fn sample_projection() -> ScalarProjectionDescriptor {
661 use sim_lib_interference_core::SamplingCertificate;
662 use sim_lib_interference_solve::project;
663
664 let problem = crate::records::sample_problem()
665 .to_problem()
666 .expect("example problem");
667 let plane = crate::records::sample_plane()
668 .to_plane()
669 .expect("example plane");
670 let field = sim_lib_interference_solve::HostPhasorField::from_component_planes(
671 2,
672 2,
673 vec![1.0, 0.0, -1.0, 0.5],
674 vec![0.0, 1.0, 0.0, -0.5],
675 )
676 .expect("example field");
677 let sampling = SamplingCertificate::measure(&problem, &plane).expect("example sampling");
678 let projection = project(&field, sampling, Observable::Phase, 0.0).expect("example projection");
679 ScalarProjectionDescriptor::from_projection(&projection).expect("example runtime projection")
680}
681
682fn bare_cx() -> Cx {
683 Cx::new(
684 Arc::new(sim_kernel::EagerPolicy),
685 Arc::new(sim_kernel::DefaultFactory),
686 )
687}