1use 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#[derive(Clone, Debug, PartialEq)]
18pub struct SamplingCertificateDescriptor {
19 pub resolved_min_samples_per_wavelength: f64,
21 pub marginal_min_samples_per_wavelength: f64,
23 pub resolved_max_envelope_fraction_per_cell: f64,
25 pub marginal_max_envelope_fraction_per_cell: f64,
27 pub wavelength_m: f64,
29 pub samples_per_wavelength_u: f64,
31 pub samples_per_wavelength_v: f64,
33 pub samples_per_power_fringe_u: f64,
35 pub samples_per_power_fringe_v: f64,
37 pub nearest_point_source_distance_m: Option<f64>,
39 pub max_envelope_fraction_per_cell: f64,
41 pub verdict: Symbol,
43}
44
45impl SamplingCertificateDescriptor {
46 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct WorkEstimateDescriptor {
76 pub cells: u64,
78 pub emitters: u64,
80 pub emitter_evaluations: u64,
82 pub host_bytes: u64,
84 pub result_bytes: u64,
86 pub certificate_stencil_work: u64,
88}
89
90impl WorkEstimateDescriptor {
91 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 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#[derive(Clone, Debug, PartialEq)]
166pub struct StudyEvidenceDescriptor {
167 pub sampling_policy: Symbol,
169 pub sampling: SamplingCertificateDescriptor,
171 pub work: WorkEstimateDescriptor,
173 pub provider: Symbol,
175 pub dtype: Symbol,
177 pub component_absolute_tolerance: f64,
179 pub squared_magnitude_absolute_tolerance: f64,
181 pub completed_cells: u64,
183 pub completed_emitter_evaluations: u64,
185 pub uploads: u64,
187 pub submissions: u64,
189 pub intermediate_materializations: u64,
191 pub final_materializations: u64,
193 pub segments: u64,
195 pub adapter: String,
197 pub profile: Option<String>,
199}
200
201impl StudyEvidenceDescriptor {
202 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 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#[derive(Clone, Debug, PartialEq)]
437pub struct StudyDescriptor {
438 pub problem: ProblemDescriptor,
440 pub plane: PlaneDescriptor,
442 pub field: PhasorFieldDescriptor,
444 pub evidence: StudyEvidenceDescriptor,
446}
447
448impl StudyDescriptor {
449 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 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}