1use 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
29pub fn problem_function_symbol() -> Symbol {
31 Symbol::qualified("interference", "problem")
32}
33
34pub fn sampling_plane_function_symbol() -> Symbol {
36 Symbol::qualified("interference", "sampling-plane")
37}
38
39pub fn solve_function_symbol() -> Symbol {
41 Symbol::qualified("interference", "solve")
42}
43
44pub fn project_function_symbol() -> Symbol {
46 Symbol::qualified("interference", "project")
47}
48
49pub fn analyze_function_symbol() -> Symbol {
51 Symbol::qualified("interference", "analyze")
52}
53
54pub fn scenarios_function_symbol() -> Symbol {
56 Symbol::qualified("interference", "scenarios")
57}
58
59pub 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}