Skip to main content

sim_lib_interference_runtime/
projection_records.rs

1//! Projection requests, certificates, masks, and scalar Tensor records.
2
3use 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/// Shape-checked request for scalar observation and optional detector reduction.
19#[derive(Clone, Debug, PartialEq)]
20pub struct ProjectionRequestDescriptor {
21    /// Observable kind.
22    pub observable: Symbol,
23    /// Angular time, present only for `interference/instant`.
24    pub angular_time_rad: Option<f64>,
25    /// Non-negative amplitude floor used for phase masks.
26    pub phase_floor: f64,
27    /// Requested target rows.
28    pub target_rows: usize,
29    /// Requested target columns.
30    pub target_columns: usize,
31    /// Detector reduction rule.
32    pub reduction: Symbol,
33}
34
35impl ProjectionRequestDescriptor {
36    /// Builds and validates a projection request.
37    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    /// Returns the checked observable.
58    pub fn observable(&self) -> Result<Observable> {
59        decode_observable(&self.observable, self.angular_time_rad)
60    }
61
62    /// Returns the checked detector rule.
63    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/// Citizen descriptor for immutable detector and sampling provenance.
134#[derive(Clone, Debug, PartialEq)]
135pub struct ProjectionCertificateDescriptor {
136    /// Source rows.
137    pub source_rows: usize,
138    /// Source columns.
139    pub source_columns: usize,
140    /// Target rows.
141    pub target_rows: usize,
142    /// Target columns.
143    pub target_columns: usize,
144    /// Minimum source rows represented by one target cell.
145    pub footprint_min_rows: usize,
146    /// Maximum source rows represented by one target cell.
147    pub footprint_max_rows: usize,
148    /// Minimum source columns represented by one target cell.
149    pub footprint_min_columns: usize,
150    /// Maximum source columns represented by one target cell.
151    pub footprint_max_columns: usize,
152    /// Observable kind.
153    pub observable: Symbol,
154    /// Angular time for an instantaneous observable.
155    pub angular_time_rad: Option<f64>,
156    /// Phase mask amplitude floor.
157    pub phase_floor: f64,
158    /// Detector reduction rule.
159    pub reduction: Symbol,
160    /// `interference/lossless` or `interference/detector-integration`.
161    pub loss_class: Symbol,
162    /// Unchanged source sampling truth.
163    pub source_sampling: SamplingCertificateDescriptor,
164    /// Number of masked target cells.
165    pub mask_count: usize,
166}
167
168impl ProjectionCertificateDescriptor {
169    /// Projects a domain certificate without losing sampling evidence.
170    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    /// Returns the checked observable.
197    pub fn observable(&self) -> Result<Observable> {
198        decode_observable(&self.observable, self.angular_time_rad)
199    }
200
201    /// Returns the checked reduction rule.
202    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/// One-Tensor scalar projection with an explicit phase mask.
368#[derive(Clone)]
369pub struct ScalarProjectionDescriptor {
370    /// Target rows.
371    pub rows: usize,
372    /// Target columns.
373    pub columns: usize,
374    /// Canonical scalar Tensor, with masked cells stored as zero.
375    pub values: Tensor,
376    /// Row-major phase mask.
377    pub mask: Vec<bool>,
378    /// Inseparable projection and sampling evidence.
379    pub certificate: ProjectionCertificateDescriptor,
380}
381
382impl ScalarProjectionDescriptor {
383    /// Projects a dependency-free scalar result into one typed `f64` Tensor.
384    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}