Skip to main content

sim_lib_interference_runtime/
ops.rs

1//! Shape-checked runtime operations for coherent interference studies.
2
3use std::sync::Arc;
4
5use sim_citizen::CitizenField;
6use sim_kernel::{
7    Cx, Demand, Error, Expr, ExprKind, LoadCx, PreparedArgs, Result, Symbol, Value,
8    force_list_to_vec,
9};
10use sim_lib_interference_core::{
11    Hertz, InterferenceProblem, MetresPerSecond, NepersPerMetre, PositiveMetres, SamplingPlane,
12    ScalarMedium, SourceSet,
13};
14use sim_lib_interference_solve::{
15    MultiToneStudy, Observable, ReductionRule, ReferencePhasorSolver, ToneCombination, ToneStudy,
16    analyze_fringes, reduce_for_view,
17};
18use sim_shape::{
19    Bindings, ExprKindShape, FunctionCase, FunctionObject, ListShape, RepeatShape, Shape,
20};
21
22use crate::{
23    PlaneDescriptor, ProblemDescriptor, ProjectionRequestDescriptor, ScalarProjectionDescriptor,
24    SolveRequest, StudyDescriptor, resolve_study_solver,
25    shapes::{plane_shape, problem_shape, projection_shape, study_shape},
26};
27use crate::{ops_outputs::*, ops_values::*};
28
29/// Returns the runtime constructor symbol for coherent problems.
30pub fn problem_function_symbol() -> Symbol {
31    Symbol::qualified("interference", "problem")
32}
33
34/// Returns the runtime constructor symbol for physical sampling planes.
35pub fn sampling_plane_function_symbol() -> Symbol {
36    Symbol::qualified("interference", "sampling-plane")
37}
38
39/// Returns the provider-routed study solve symbol.
40pub fn solve_function_symbol() -> Symbol {
41    Symbol::qualified("interference", "solve")
42}
43
44/// Returns the propagation-free scalar projection symbol.
45pub fn project_function_symbol() -> Symbol {
46    Symbol::qualified("interference", "project")
47}
48
49/// Returns the certified fringe-analysis symbol.
50pub fn analyze_function_symbol() -> Symbol {
51    Symbol::qualified("interference", "analyze")
52}
53
54/// Returns the bounded named-scenario constructor symbol.
55pub fn scenarios_function_symbol() -> Symbol {
56    Symbol::qualified("interference", "scenarios")
57}
58
59/// Returns the independently certified multi-tone composition symbol.
60pub fn multitone_function_symbol() -> Symbol {
61    Symbol::qualified("interference", "multitone")
62}
63
64pub(crate) fn function_symbols() -> [Symbol; 7] {
65    [
66        problem_function_symbol(),
67        sampling_plane_function_symbol(),
68        solve_function_symbol(),
69        project_function_symbol(),
70        analyze_function_symbol(),
71        scenarios_function_symbol(),
72        multitone_function_symbol(),
73    ]
74}
75
76pub(crate) fn runtime_functions(cx: &mut LoadCx) -> Vec<(Symbol, FunctionObject)> {
77    vec![
78        function(
79            cx,
80            problem_function_symbol(),
81            vec![map_shape()],
82            problem_shape(),
83            problem_impl,
84        ),
85        function(
86            cx,
87            sampling_plane_function_symbol(),
88            vec![map_shape()],
89            plane_shape(),
90            sampling_plane_impl,
91        ),
92        function(
93            cx,
94            solve_function_symbol(),
95            vec![problem_shape(), plane_shape(), map_shape()],
96            study_shape(),
97            solve_impl,
98        ),
99        function(
100            cx,
101            project_function_symbol(),
102            vec![study_shape(), map_shape()],
103            projection_shape(),
104            project_impl,
105        ),
106        function(
107            cx,
108            analyze_function_symbol(),
109            vec![study_shape(), map_shape()],
110            map_shape(),
111            analyze_impl,
112        ),
113        function(
114            cx,
115            scenarios_function_symbol(),
116            vec![map_shape()],
117            map_shape(),
118            scenarios_impl,
119        ),
120        function(
121            cx,
122            multitone_function_symbol(),
123            vec![
124                Arc::new(RepeatShape::with_bounds(problem_shape(), 1, None)),
125                plane_shape(),
126                map_shape(),
127            ],
128            map_shape(),
129            multitone_impl,
130        ),
131    ]
132}
133
134fn function(
135    cx: &mut LoadCx,
136    symbol: Symbol,
137    args: Vec<Arc<dyn Shape>>,
138    result: Arc<dyn Shape>,
139    implementation: fn(&mut Cx, &PreparedArgs, Bindings) -> Result<Value>,
140) -> (Symbol, FunctionObject) {
141    let callable = FunctionObject::new(
142        cx.fresh_function_id(),
143        symbol.clone(),
144        vec![FunctionCase {
145            id: cx.fresh_case_id(),
146            name: Symbol::qualified(symbol.to_string(), "checked"),
147            args: Arc::new(ListShape::tuple(args.clone())),
148            result: Some(result),
149            demand: vec![Demand::Value; args.len()],
150            priority: 10,
151            implementation,
152        }],
153    );
154    (symbol, callable)
155}
156
157fn map_shape() -> Arc<dyn Shape> {
158    Arc::new(ExprKindShape::new(ExprKind::Map))
159}
160
161fn problem_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
162    let [spec] = args.values() else {
163        return arity("interference/problem", 1, args.len());
164    };
165    let problem = build_problem(cx, spec.clone())?;
166    boxed(cx, problem)
167}
168
169fn sampling_plane_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
170    let [spec] = args.values() else {
171        return arity("interference/sampling-plane", 1, args.len());
172    };
173    let plane = build_plane(cx, spec.clone())?;
174    boxed(cx, plane)
175}
176
177fn solve_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
178    let [problem, plane, options] = args.values() else {
179        return arity("interference/solve", 3, args.len());
180    };
181    let problem = descriptor::<ProblemDescriptor>(problem, "interference/solve problem")?;
182    let plane = descriptor::<PlaneDescriptor>(plane, "interference/solve plane")?;
183    let options = map_from_value(cx, options.clone(), "interference/solve options")?;
184    reject_extra(
185        &options,
186        &["sampling", "sampling-thresholds", "work-budget"],
187        "interference/solve options",
188    )?;
189    let config = solve_config(&options)?;
190    let problem = problem.to_problem()?;
191    let plane = plane.to_plane()?;
192    let request = SolveRequest::new(
193        &problem,
194        &plane,
195        config.sampling_policy,
196        config.sampling_thresholds,
197        config.work_budget,
198    );
199    let solver = resolve_study_solver(cx)?;
200    let study = solver.solve(cx, &request)?;
201    boxed(cx, study)
202}
203
204fn project_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
205    let [study, request] = args.values() else {
206        return arity("interference/project", 2, args.len());
207    };
208    let study = descriptor::<StudyDescriptor>(study, "interference/project study")?;
209    let request = projection_request(cx, request.clone(), study)?;
210    let projection = project_study(cx, study, &request)?;
211    boxed(cx, projection)
212}
213
214fn analyze_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
215    let [study, request] = args.values() else {
216        return arity("interference/analyze", 2, args.len());
217    };
218    let study = descriptor::<StudyDescriptor>(study, "interference/analyze study")?;
219    let request_map = map_from_value(cx, request.clone(), "interference/analyze request")?;
220    reject_extra(
221        &request_map,
222        &[
223            "observable",
224            "wt",
225            "phase-floor",
226            "target-rows",
227            "target-cols",
228            "reduction",
229            "amplitude-floor",
230        ],
231        "interference/analyze request",
232    )?;
233    let amplitude_floor = optional_decode::<f64>(&request_map, "amplitude-floor")?.unwrap_or(0.0);
234    let request = projection_request_from_map(&request_map, study)?;
235    let projection = project_domain(cx, study, &request)?;
236    let report = analyze_fringes(&projection, amplitude_floor)
237        .map_err(|error| Error::Eval(format!("interference analysis failed: {error}")))?;
238    fringe_report_value(cx, &report)
239}
240
241fn scenarios_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
242    let [request] = args.values() else {
243        return arity("interference/scenarios", 1, args.len());
244    };
245    let map = map_from_value(cx, request.clone(), "interference/scenarios request")?;
246    let scenario = build_scenario(&map)?;
247    scenario_value(cx, scenario)
248}
249
250fn multitone_impl(cx: &mut Cx, args: &PreparedArgs, _bindings: Bindings) -> Result<Value> {
251    let [problems, plane, options] = args.values() else {
252        return arity("interference/multitone", 3, args.len());
253    };
254    let problem_values = problems
255        .object()
256        .as_list()
257        .ok_or_else(|| Error::Eval("interference/multitone problems must be a list".to_owned()))
258        .and_then(|list| force_list_to_vec(cx, list, "interference/multitone problems"))?;
259    let problems = problem_values
260        .iter()
261        .map(|value| {
262            descriptor::<ProblemDescriptor>(value, "interference/multitone problem")?.to_problem()
263        })
264        .collect::<Result<Vec<_>>>()?;
265    let plane = descriptor::<PlaneDescriptor>(plane, "interference/multitone plane")?.to_plane()?;
266    let options = map_from_value(cx, options.clone(), "interference/multitone options")?;
267    reject_extra(
268        &options,
269        &[
270            "weights",
271            "combination",
272            "seconds",
273            "sampling",
274            "sampling-thresholds",
275            "work-budget",
276        ],
277        "interference/multitone options",
278    )?;
279    let weights = required_list(&options, "weights", "interference/multitone options")?
280        .iter()
281        .map(|value| f64::decode_field_expr(value, "weight"))
282        .collect::<Result<Vec<_>>>()?;
283    if weights.len() != problems.len() {
284        return Err(Error::Eval(format!(
285            "interference/multitone requires one weight per problem: {} problems, {} weights",
286            problems.len(),
287            weights.len()
288        )));
289    }
290    let config = solve_config(&options)?;
291    let solver = ReferencePhasorSolver::new(
292        config.sampling_policy,
293        config.sampling_thresholds,
294        config.work_budget,
295    );
296    let tones = problems
297        .into_iter()
298        .zip(weights)
299        .map(|(problem, weight)| ToneStudy::solve(problem, plane, weight, solver))
300        .collect::<std::result::Result<Vec<_>, _>>()
301        .map_err(|error| Error::Eval(format!("interference multi-tone solve failed: {error}")))?;
302    let study = MultiToneStudy::new(tones).map_err(|error| {
303        Error::Eval(format!("interference multi-tone admission failed: {error}"))
304    })?;
305    let combination = match required_symbol(&options, "combination", "multitone combination")?
306        .name
307        .as_ref()
308    {
309        "incoherent-magnitude-squared" => ToneCombination::IncoherentMagnitudeSquared,
310        "instant" => ToneCombination::Instant {
311            seconds: required_decode(&options, "seconds")?,
312        },
313        other => {
314            return Err(Error::Eval(format!(
315                "unknown interference/multitone combination {other}"
316            )));
317        }
318    };
319    let projection = study
320        .combine(combination)
321        .map_err(|error| Error::Eval(format!("interference multi-tone combine failed: {error}")))?;
322    multitone_value(cx, &projection)
323}
324
325fn build_problem(cx: &mut Cx, spec: Value) -> Result<ProblemDescriptor> {
326    let map = map_from_value(cx, spec, "interference/problem")?;
327    reject_extra(
328        &map,
329        &[
330            "frequency-hz",
331            "speed-m-s",
332            "attenuation-np-m",
333            "singularity-radius-m",
334            "sources",
335        ],
336        "interference/problem",
337    )?;
338    let medium = ScalarMedium::new(
339        MetresPerSecond::new(required_decode(&map, "speed-m-s")?)
340            .map_err(domain_error("interference/problem speed-m-s"))?,
341        NepersPerMetre::new(required_decode(&map, "attenuation-np-m")?)
342            .map_err(domain_error("interference/problem attenuation-np-m"))?,
343    );
344    let sources = required_list(&map, "sources", "interference/problem")?
345        .iter()
346        .map(build_emitter)
347        .collect::<Result<Vec<_>>>()?;
348    let problem = InterferenceProblem::new(
349        Hertz::new(required_decode(&map, "frequency-hz")?)
350            .map_err(domain_error("interference/problem frequency-hz"))?,
351        medium,
352        SourceSet::new(sources).map_err(domain_error("interference/problem sources"))?,
353        PositiveMetres::new(required_decode(&map, "singularity-radius-m")?)
354            .map_err(domain_error("interference/problem singularity-radius-m"))?,
355    );
356    Ok(ProblemDescriptor::from_problem(&problem))
357}
358
359fn build_plane(cx: &mut Cx, spec: Value) -> Result<PlaneDescriptor> {
360    let map = map_from_value(cx, spec, "interference/sampling-plane")?;
361    reject_extra(
362        &map,
363        &[
364            "origin-m",
365            "u-axis",
366            "v-axis",
367            "extent-u-m",
368            "extent-v-m",
369            "rows",
370            "cols",
371        ],
372        "interference/sampling-plane",
373    )?;
374    let plane = SamplingPlane::new(
375        point3(
376            required_field(&map, "origin-m", "interference/sampling-plane")?,
377            "origin-m",
378        )?,
379        unit_vector(
380            required_field(&map, "u-axis", "interference/sampling-plane")?,
381            "u-axis",
382        )?,
383        unit_vector(
384            required_field(&map, "v-axis", "interference/sampling-plane")?,
385            "v-axis",
386        )?,
387        PositiveMetres::new(required_decode(&map, "extent-u-m")?)
388            .map_err(domain_error("sampling-plane extent-u-m"))?,
389        PositiveMetres::new(required_decode(&map, "extent-v-m")?)
390            .map_err(domain_error("sampling-plane extent-v-m"))?,
391        required_decode(&map, "rows")?,
392        required_decode(&map, "cols")?,
393    )
394    .map_err(domain_error("interference/sampling-plane"))?;
395    Ok(PlaneDescriptor::from_plane(plane))
396}
397
398fn projection_request(
399    cx: &mut Cx,
400    value: Value,
401    study: &StudyDescriptor,
402) -> Result<ProjectionRequestDescriptor> {
403    let map = map_from_value(cx, value, "interference/project request")?;
404    reject_extra(
405        &map,
406        &[
407            "observable",
408            "wt",
409            "phase-floor",
410            "target-rows",
411            "target-cols",
412            "reduction",
413        ],
414        "interference/project request",
415    )?;
416    projection_request_from_map(&map, study)
417}
418
419fn projection_request_from_map(
420    map: &[(Expr, Expr)],
421    study: &StudyDescriptor,
422) -> Result<ProjectionRequestDescriptor> {
423    let observable = match required_symbol(map, "observable", "projection observable")?
424        .name
425        .as_ref()
426    {
427        "real" => Observable::Real,
428        "imaginary" => Observable::Imaginary,
429        "amplitude" => Observable::Amplitude,
430        "phase" => Observable::Phase,
431        "magnitude-squared" => Observable::MagnitudeSquared,
432        "instant" => Observable::Instant {
433            wt: required_decode(map, "wt")?,
434        },
435        other => {
436            return Err(Error::Eval(format!(
437                "unknown projection observable {other}"
438            )));
439        }
440    };
441    let reduction = match required_symbol(map, "reduction", "projection reduction")?
442        .name
443        .as_ref()
444    {
445        "detail" => ReductionRule::Detail,
446        "detector-complex-mean" => ReductionRule::DetectorComplexMean,
447        "detector-scalar-area-mean" => ReductionRule::DetectorScalarAreaMean,
448        "detector-magnitude-squared-area-mean" => ReductionRule::DetectorMagnitudeSquaredAreaMean,
449        other => return Err(Error::Eval(format!("unknown projection reduction {other}"))),
450    };
451    ProjectionRequestDescriptor::new(
452        observable,
453        optional_decode::<f64>(map, "phase-floor")?.unwrap_or(0.0),
454        optional_decode::<usize>(map, "target-rows")?.unwrap_or(study.plane.rows),
455        optional_decode::<usize>(map, "target-cols")?.unwrap_or(study.plane.columns),
456        reduction,
457    )
458}
459
460fn project_study(
461    cx: &mut Cx,
462    study: &StudyDescriptor,
463    request: &ProjectionRequestDescriptor,
464) -> Result<ScalarProjectionDescriptor> {
465    let projection = project_domain(cx, study, request)?;
466    ScalarProjectionDescriptor::from_projection(&projection)
467}
468
469fn project_domain(
470    cx: &mut Cx,
471    study: &StudyDescriptor,
472    request: &ProjectionRequestDescriptor,
473) -> Result<sim_lib_interference_solve::ScalarProjection> {
474    let field = study.field.materialize_host(cx)?;
475    reduce_for_view(
476        &field,
477        study.evidence.sampling.to_certificate()?,
478        request.observable()?,
479        request.phase_floor,
480        request.target_rows,
481        request.target_columns,
482        request.reduction()?,
483    )
484    .map_err(|error| Error::Eval(format!("interference projection failed: {error}")))
485}