Skip to main content

sim_lib_interference_runtime/
evidence.rs

1//! Truth-carrying sampling, work, solver, and complete-study records.
2
3use sim_kernel::{Cx, Result, Symbol, Value};
4use sim_lib_interference_core::{
5    SamplingCertificate, SamplingPolicy, SamplingVerdict, WorkEstimate,
6};
7use sim_lib_interference_solve::{HostPhasorField, SolveEvidence};
8use sim_lib_numbers_tensor::domains;
9
10use crate::{
11    PhasorFieldDescriptor, PlaneDescriptor, ProblemDescriptor,
12    citizen::{RecordCitizenSpec, decode_record, encode_field, encode_record, invalid, next_field},
13    records::{sample_plane, sample_problem},
14};
15
16/// Citizen descriptor for complete physical sampling evidence.
17#[derive(Clone, Debug, PartialEq)]
18pub struct SamplingCertificateDescriptor {
19    /// Comfortable carrier samples per wavelength.
20    pub resolved_min_samples_per_wavelength: f64,
21    /// Marginal carrier samples per wavelength.
22    pub marginal_min_samples_per_wavelength: f64,
23    /// Comfortable maximum fractional envelope change per cell.
24    pub resolved_max_envelope_fraction_per_cell: f64,
25    /// Marginal maximum fractional envelope change per cell.
26    pub marginal_max_envelope_fraction_per_cell: f64,
27    /// Carrier wavelength in metres.
28    pub wavelength_m: f64,
29    /// Carrier samples per wavelength on the `u` axis.
30    pub samples_per_wavelength_u: f64,
31    /// Carrier samples per wavelength on the `v` axis.
32    pub samples_per_wavelength_v: f64,
33    /// Squared-magnitude fringe samples on `u`.
34    pub samples_per_power_fringe_u: f64,
35    /// Squared-magnitude fringe samples on `v`.
36    pub samples_per_power_fringe_v: f64,
37    /// Nearest point-source distance, absent for plane-only problems.
38    pub nearest_point_source_distance_m: Option<f64>,
39    /// Conservative fractional `1/r` envelope change per cell.
40    pub max_envelope_fraction_per_cell: f64,
41    /// `interference/resolved`, `marginal`, or `aliased`.
42    pub verdict: Symbol,
43}
44
45impl SamplingCertificateDescriptor {
46    /// Projects a domain sampling certificate without dropping thresholds.
47    pub fn from_certificate(certificate: SamplingCertificate) -> Self {
48        Self {
49            resolved_min_samples_per_wavelength: certificate
50                .thresholds
51                .resolved_min_samples_per_wavelength,
52            marginal_min_samples_per_wavelength: certificate
53                .thresholds
54                .marginal_min_samples_per_wavelength,
55            resolved_max_envelope_fraction_per_cell: certificate
56                .thresholds
57                .resolved_max_envelope_fraction_per_cell,
58            marginal_max_envelope_fraction_per_cell: certificate
59                .thresholds
60                .marginal_max_envelope_fraction_per_cell,
61            wavelength_m: certificate.wavelength_m,
62            samples_per_wavelength_u: certificate.samples_per_wavelength_u,
63            samples_per_wavelength_v: certificate.samples_per_wavelength_v,
64            samples_per_power_fringe_u: certificate.samples_per_power_fringe_u,
65            samples_per_power_fringe_v: certificate.samples_per_power_fringe_v,
66            nearest_point_source_distance_m: certificate.nearest_point_source_distance_m,
67            max_envelope_fraction_per_cell: certificate.max_envelope_fraction_per_cell,
68            verdict: verdict_symbol(certificate.verdict),
69        }
70    }
71}
72
73/// Citizen descriptor for checked allocation and evaluation counts.
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct WorkEstimateDescriptor {
76    /// Sampling cells.
77    pub cells: u64,
78    /// Coherent emitters.
79    pub emitters: u64,
80    /// Cell/emitter evaluations.
81    pub emitter_evaluations: u64,
82    /// Peak host bytes.
83    pub host_bytes: u64,
84    /// Result bytes.
85    pub result_bytes: u64,
86    /// Seven-point certificate stencil work.
87    pub certificate_stencil_work: u64,
88}
89
90impl WorkEstimateDescriptor {
91    /// Projects a checked domain estimate.
92    pub fn from_estimate(estimate: WorkEstimate) -> Self {
93        Self {
94            cells: estimate.cells,
95            emitters: estimate.emitters,
96            emitter_evaluations: estimate.emitter_evaluations,
97            host_bytes: estimate.host_bytes,
98            result_bytes: estimate.result_bytes,
99            certificate_stencil_work: estimate.certificate_stencil_work,
100        }
101    }
102
103    /// Recomputes and checks every derived work dimension.
104    pub fn to_estimate(self) -> Result<WorkEstimate> {
105        let expected = WorkEstimate::new(self.cells, self.emitters)
106            .map_err(|error| invalid("WorkEstimate", format!("{error:?}")))?;
107        let actual = Self::from_estimate(expected);
108        if actual != self {
109            return Err(invalid(
110                "WorkEstimate",
111                "derived counts or byte sizes are inconsistent",
112            ));
113        }
114        Ok(expected)
115    }
116}
117
118impl RecordCitizenSpec for WorkEstimateDescriptor {
119    const FIELDS: &'static [&'static str] = &[
120        "cells",
121        "emitters",
122        "emitter-evaluations",
123        "host-bytes",
124        "result-bytes",
125        "certificate-stencil-work",
126    ];
127
128    fn encode_fields(&self, _cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
129        Ok(vec![
130            encode_field(&self.cells),
131            encode_field(&self.emitters),
132            encode_field(&self.emitter_evaluations),
133            encode_field(&self.host_bytes),
134            encode_field(&self.result_bytes),
135            encode_field(&self.certificate_stencil_work),
136        ])
137    }
138
139    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
140        let mut fields = fields.into_iter();
141        let value = Self {
142            cells: next_field(cx, &mut fields, "cells")?,
143            emitters: next_field(cx, &mut fields, "emitters")?,
144            emitter_evaluations: next_field(cx, &mut fields, "emitter-evaluations")?,
145            host_bytes: next_field(cx, &mut fields, "host-bytes")?,
146            result_bytes: next_field(cx, &mut fields, "result-bytes")?,
147            certificate_stencil_work: next_field(cx, &mut fields, "certificate-stencil-work")?,
148        };
149        value.validate()?;
150        Ok(value)
151    }
152
153    fn example() -> Self {
154        Self::from_estimate(WorkEstimate::new(4, 1).expect("example work"))
155    }
156
157    fn validate(&self) -> Result<()> {
158        self.to_estimate().map(|_| ())
159    }
160}
161
162impl_record_citizen!(WorkEstimateDescriptor, "interference/WorkEstimate", 6);
163
164/// Citizen descriptor for one solver's immutable completion evidence.
165#[derive(Clone, Debug, PartialEq)]
166pub struct StudyEvidenceDescriptor {
167    /// `interference/strict` or `interference/annotate`.
168    pub sampling_policy: Symbol,
169    /// Unchanged sampling truth.
170    pub sampling: SamplingCertificateDescriptor,
171    /// Preflight work estimate.
172    pub work: WorkEstimateDescriptor,
173    /// Provider identity.
174    pub provider: Symbol,
175    /// Component dtype.
176    pub dtype: Symbol,
177    /// Absolute Cartesian-component tolerance.
178    pub component_absolute_tolerance: f64,
179    /// Absolute squared-magnitude tolerance.
180    pub squared_magnitude_absolute_tolerance: f64,
181    /// Completed output cells.
182    pub completed_cells: u64,
183    /// Completed source evaluations.
184    pub completed_emitter_evaluations: u64,
185    /// Host-to-provider uploads.
186    pub uploads: u64,
187    /// Accepted provider submissions.
188    pub submissions: u64,
189    /// Intermediate host readbacks.
190    pub intermediate_materializations: u64,
191    /// Final component readbacks.
192    pub final_materializations: u64,
193    /// Ordered execution segments.
194    pub segments: u64,
195    /// Adapter/provider implementation identity.
196    pub adapter: String,
197    /// Optional measured hardware/profile identity.
198    pub profile: Option<String>,
199}
200
201impl StudyEvidenceDescriptor {
202    /// Projects evidence from the deterministic host reference solver.
203    pub fn from_reference(evidence: &SolveEvidence) -> Self {
204        let preflight = evidence.preflight();
205        Self {
206            sampling_policy: sampling_policy_symbol(preflight.sampling_policy),
207            sampling: SamplingCertificateDescriptor::from_certificate(
208                preflight.sampling_certificate,
209            ),
210            work: WorkEstimateDescriptor::from_estimate(preflight.work_estimate),
211            provider: reference_provider_symbol(),
212            dtype: domains::f64(),
213            component_absolute_tolerance: 0.0,
214            squared_magnitude_absolute_tolerance: 0.0,
215            completed_cells: evidence.completed_cells(),
216            completed_emitter_evaluations: evidence.completed_emitter_evaluations(),
217            uploads: 0,
218            submissions: 0,
219            intermediate_materializations: 0,
220            final_materializations: 0,
221            segments: 1,
222            adapter: "reference-f64".to_owned(),
223            profile: None,
224        }
225    }
226
227    /// Returns the checked sampling policy.
228    pub fn sampling_policy(&self) -> Result<SamplingPolicy> {
229        if self.sampling_policy == strict_policy_symbol() {
230            Ok(SamplingPolicy::Strict)
231        } else if self.sampling_policy == annotate_policy_symbol() {
232            Ok(SamplingPolicy::Annotate)
233        } else {
234            Err(invalid(
235                "StudyEvidence",
236                format!("unknown sampling policy {}", self.sampling_policy),
237            ))
238        }
239    }
240}
241
242impl RecordCitizenSpec for StudyEvidenceDescriptor {
243    const FIELDS: &'static [&'static str] = &[
244        "sampling-policy",
245        "sampling",
246        "work",
247        "provider",
248        "dtype",
249        "component-absolute-tolerance",
250        "squared-magnitude-absolute-tolerance",
251        "completed-cells",
252        "completed-emitter-evaluations",
253        "uploads",
254        "submissions",
255        "intermediate-materializations",
256        "final-materializations",
257        "segments",
258        "adapter",
259        "profile",
260    ];
261
262    fn encode_fields(&self, cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
263        Ok(vec![
264            encode_field(&self.sampling_policy),
265            encode_record(cx, &self.sampling)?,
266            encode_record(cx, &self.work)?,
267            encode_field(&self.provider),
268            encode_field(&self.dtype),
269            encode_field(&self.component_absolute_tolerance),
270            encode_field(&self.squared_magnitude_absolute_tolerance),
271            encode_field(&self.completed_cells),
272            encode_field(&self.completed_emitter_evaluations),
273            encode_field(&self.uploads),
274            encode_field(&self.submissions),
275            encode_field(&self.intermediate_materializations),
276            encode_field(&self.final_materializations),
277            encode_field(&self.segments),
278            encode_field(&self.adapter),
279            encode_field(&self.profile),
280        ])
281    }
282
283    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
284        let mut fields = fields.into_iter();
285        let sampling_policy = next_field(cx, &mut fields, "sampling-policy")?;
286        let sampling = decode_record(
287            cx,
288            fields
289                .next()
290                .ok_or_else(|| invalid("StudyEvidence", "missing sampling evidence"))?,
291            "sampling",
292        )?;
293        let work = decode_record(
294            cx,
295            fields
296                .next()
297                .ok_or_else(|| invalid("StudyEvidence", "missing work estimate"))?,
298            "work",
299        )?;
300        let value = Self {
301            sampling_policy,
302            sampling,
303            work,
304            provider: next_field(cx, &mut fields, "provider")?,
305            dtype: next_field(cx, &mut fields, "dtype")?,
306            component_absolute_tolerance: next_field(
307                cx,
308                &mut fields,
309                "component-absolute-tolerance",
310            )?,
311            squared_magnitude_absolute_tolerance: next_field(
312                cx,
313                &mut fields,
314                "squared-magnitude-absolute-tolerance",
315            )?,
316            completed_cells: next_field(cx, &mut fields, "completed-cells")?,
317            completed_emitter_evaluations: next_field(
318                cx,
319                &mut fields,
320                "completed-emitter-evaluations",
321            )?,
322            uploads: next_field(cx, &mut fields, "uploads")?,
323            submissions: next_field(cx, &mut fields, "submissions")?,
324            intermediate_materializations: next_field(
325                cx,
326                &mut fields,
327                "intermediate-materializations",
328            )?,
329            final_materializations: next_field(cx, &mut fields, "final-materializations")?,
330            segments: next_field(cx, &mut fields, "segments")?,
331            adapter: next_field(cx, &mut fields, "adapter")?,
332            profile: next_field(cx, &mut fields, "profile")?,
333        };
334        value.validate()?;
335        Ok(value)
336    }
337
338    fn example() -> Self {
339        sample_study_evidence()
340    }
341
342    fn validate(&self) -> Result<()> {
343        let policy = self.sampling_policy()?;
344        let sampling = self.sampling.to_certificate()?;
345        self.work.to_estimate()?;
346        if policy == SamplingPolicy::Strict && sampling.verdict != SamplingVerdict::Resolved {
347            return Err(invalid(
348                "StudyEvidence",
349                "strict policy cannot carry non-resolved sampling",
350            ));
351        }
352        if self.provider.to_string().trim().is_empty() {
353            return Err(invalid("StudyEvidence", "provider cannot be empty"));
354        }
355        if self.dtype != domains::f32() && self.dtype != domains::f64() {
356            return Err(invalid(
357                "StudyEvidence",
358                "dtype must be numbers/f32 or numbers/f64",
359            ));
360        }
361        for (name, value) in [
362            (
363                "component absolute tolerance",
364                self.component_absolute_tolerance,
365            ),
366            (
367                "squared-magnitude absolute tolerance",
368                self.squared_magnitude_absolute_tolerance,
369            ),
370        ] {
371            if !value.is_finite() || value < 0.0 {
372                return Err(invalid(
373                    "StudyEvidence",
374                    format!("{name} must be finite and non-negative"),
375                ));
376            }
377        }
378        if self.completed_cells != self.work.cells
379            || self.completed_emitter_evaluations != self.work.emitter_evaluations
380        {
381            return Err(invalid(
382                "StudyEvidence",
383                "completion counts must equal the admitted work",
384            ));
385        }
386        if self.intermediate_materializations != 0 {
387            return Err(invalid(
388                "StudyEvidence",
389                "intermediate materialization is forbidden",
390            ));
391        }
392        if self.final_materializations > 2 {
393            return Err(invalid(
394                "StudyEvidence",
395                "a two-component field permits at most two final materializations",
396            ));
397        }
398        if self.segments == 0 {
399            return Err(invalid(
400                "StudyEvidence",
401                "completed evidence must name at least one segment",
402            ));
403        }
404        if self.adapter.trim().is_empty()
405            || self
406                .profile
407                .as_ref()
408                .is_some_and(|profile| profile.trim().is_empty())
409        {
410            return Err(invalid(
411                "StudyEvidence",
412                "adapter and present profile identities cannot be empty",
413            ));
414        }
415        if self.provider == reference_provider_symbol()
416            && (self.dtype != domains::f64()
417                || self.component_absolute_tolerance != 0.0
418                || self.squared_magnitude_absolute_tolerance != 0.0
419                || self.uploads != 0
420                || self.submissions != 0
421                || self.final_materializations != 0
422                || self.profile.is_some())
423        {
424            return Err(invalid(
425                "StudyEvidence",
426                "reference provider evidence must remain exact host f64",
427            ));
428        }
429        Ok(())
430    }
431}
432
433impl_record_citizen!(StudyEvidenceDescriptor, "interference/StudyEvidence", 16);
434
435/// Complete Tensor-backed interference study with inseparable truth.
436#[derive(Clone, Debug, PartialEq)]
437pub struct StudyDescriptor {
438    /// Exact coherent problem.
439    pub problem: ProblemDescriptor,
440    /// Exact physical sampling plane.
441    pub plane: PlaneDescriptor,
442    /// Two-component Tensor field.
443    pub field: PhasorFieldDescriptor,
444    /// Sampling, work, provider, tolerance, and execution evidence.
445    pub evidence: StudyEvidenceDescriptor,
446}
447
448impl StudyDescriptor {
449    /// Builds and validates a complete study from canonical runtime records.
450    ///
451    /// Alternate study solvers use this constructor so they cannot bypass the
452    /// same problem, plane, field, sampling, work, and dtype invariants as the
453    /// reference provider.
454    pub fn new(
455        problem: ProblemDescriptor,
456        plane: PlaneDescriptor,
457        field: PhasorFieldDescriptor,
458        evidence: StudyEvidenceDescriptor,
459    ) -> Result<Self> {
460        let value = Self {
461            problem,
462            plane,
463            field,
464            evidence,
465        };
466        value.validate()?;
467        Ok(value)
468    }
469
470    /// Projects one completed reference solve into runtime records.
471    pub fn from_reference(
472        problem: &sim_lib_interference_core::InterferenceProblem,
473        plane: sim_lib_interference_core::SamplingPlane,
474        field: HostPhasorField,
475        evidence: &SolveEvidence,
476    ) -> Result<Self> {
477        Self::new(
478            ProblemDescriptor::from_problem(problem),
479            PlaneDescriptor::from_plane(plane),
480            PhasorFieldDescriptor::from_host(field)?,
481            StudyEvidenceDescriptor::from_reference(evidence),
482        )
483    }
484}
485
486impl RecordCitizenSpec for StudyDescriptor {
487    const FIELDS: &'static [&'static str] = &["problem", "plane", "field", "evidence"];
488
489    fn encode_fields(&self, cx: &mut Cx) -> Result<Vec<sim_kernel::Expr>> {
490        Ok(vec![
491            encode_record(cx, &self.problem)?,
492            encode_record(cx, &self.plane)?,
493            encode_record(cx, &self.field)?,
494            encode_record(cx, &self.evidence)?,
495        ])
496    }
497
498    fn decode_fields(cx: &mut Cx, fields: Vec<Value>) -> Result<Self> {
499        let mut fields = fields.into_iter();
500        let value = Self {
501            problem: decode_next_record(cx, &mut fields, "problem")?,
502            plane: decode_next_record(cx, &mut fields, "plane")?,
503            field: decode_next_record(cx, &mut fields, "field")?,
504            evidence: decode_next_record(cx, &mut fields, "evidence")?,
505        };
506        value.validate()?;
507        Ok(value)
508    }
509
510    fn example() -> Self {
511        let value = Self {
512            problem: sample_problem(),
513            plane: sample_plane(),
514            field: <PhasorFieldDescriptor as RecordCitizenSpec>::example(),
515            evidence: sample_study_evidence(),
516        };
517        value.validate().expect("example study");
518        value
519    }
520
521    fn validate(&self) -> Result<()> {
522        let problem = self.problem.to_problem()?;
523        let plane = self.plane.to_plane()?;
524        self.field.validate()?;
525        self.evidence.validate()?;
526        if self.field.rows != self.plane.rows || self.field.cols != self.plane.columns {
527            return Err(invalid(
528                "Study",
529                "field dimensions must equal the physical plane dimensions",
530            ));
531        }
532        if self.field.real.dtype() != &self.evidence.dtype {
533            return Err(invalid(
534                "Study",
535                "field dtype must equal the evidence dtype",
536            ));
537        }
538        let expected_work = WorkEstimate::for_request(&problem, &plane)
539            .map_err(|error| invalid("Study", format!("{error:?}")))?;
540        if self.evidence.work != WorkEstimateDescriptor::from_estimate(expected_work) {
541            return Err(invalid(
542                "Study",
543                "work evidence does not match the problem and plane",
544            ));
545        }
546        let thresholds = self.evidence.sampling.to_certificate()?.thresholds;
547        let expected_sampling =
548            SamplingCertificate::measure_with_thresholds(&problem, &plane, thresholds)
549                .map_err(|error| invalid("Study", format!("{error:?}")))?;
550        if self.evidence.sampling
551            != SamplingCertificateDescriptor::from_certificate(expected_sampling)
552        {
553            return Err(invalid(
554                "Study",
555                "sampling evidence does not match the problem and plane",
556            ));
557        }
558        Ok(())
559    }
560}
561
562impl_record_citizen!(StudyDescriptor, "interference/Study", 4);
563
564fn decode_next_record<T>(
565    cx: &mut Cx,
566    fields: &mut impl Iterator<Item = Value>,
567    name: &'static str,
568) -> Result<T>
569where
570    T: sim_citizen::CitizenRuntime,
571{
572    decode_record(
573        cx,
574        fields
575            .next()
576            .ok_or_else(|| invalid("Study", format!("missing {name}")))?,
577        name,
578    )
579}
580
581pub(crate) fn sample_sampling() -> SamplingCertificateDescriptor {
582    let problem = sample_problem().to_problem().expect("example problem");
583    let plane = sample_plane().to_plane().expect("example plane");
584    SamplingCertificateDescriptor::from_certificate(
585        SamplingCertificate::measure(&problem, &plane).expect("example sampling"),
586    )
587}
588
589fn sample_study_evidence() -> StudyEvidenceDescriptor {
590    let problem = sample_problem().to_problem().expect("example problem");
591    let plane = sample_plane().to_plane().expect("example plane");
592    let work = WorkEstimate::for_request(&problem, &plane).expect("example work");
593    let sampling =
594        SamplingCertificate::measure(&problem, &plane).expect("example sampling certificate");
595    let value = StudyEvidenceDescriptor {
596        sampling_policy: annotate_policy_symbol(),
597        sampling: SamplingCertificateDescriptor::from_certificate(sampling),
598        work: WorkEstimateDescriptor::from_estimate(work),
599        provider: reference_provider_symbol(),
600        dtype: domains::f64(),
601        component_absolute_tolerance: 0.0,
602        squared_magnitude_absolute_tolerance: 0.0,
603        completed_cells: work.cells,
604        completed_emitter_evaluations: work.emitter_evaluations,
605        uploads: 0,
606        submissions: 0,
607        intermediate_materializations: 0,
608        final_materializations: 0,
609        segments: 1,
610        adapter: "reference-f64".to_owned(),
611        profile: None,
612    };
613    value.validate().expect("example study evidence");
614    value
615}
616
617pub(crate) fn verdict_symbol(verdict: SamplingVerdict) -> Symbol {
618    Symbol::qualified(
619        "interference",
620        match verdict {
621            SamplingVerdict::Resolved => "resolved",
622            SamplingVerdict::Marginal => "marginal",
623            SamplingVerdict::Aliased => "aliased",
624        },
625    )
626}
627
628fn sampling_policy_symbol(policy: SamplingPolicy) -> Symbol {
629    match policy {
630        SamplingPolicy::Strict => strict_policy_symbol(),
631        SamplingPolicy::Annotate => annotate_policy_symbol(),
632    }
633}
634
635fn strict_policy_symbol() -> Symbol {
636    Symbol::qualified("interference", "strict")
637}
638
639fn annotate_policy_symbol() -> Symbol {
640    Symbol::qualified("interference", "annotate")
641}
642
643fn reference_provider_symbol() -> Symbol {
644    Symbol::qualified("interference", "reference-f64")
645}