Skip to main content

sim_lib_view_interference/
cookbook.rs

1//! Checked end-to-end builder for the interference surface recipe.
2
3use std::sync::Arc;
4
5use sim_kernel::{
6    Args, CapabilityName, Consistency, Cx, DefaultFactory, EagerPolicy, Error, EvalFabric,
7    EvalMode, EvalReply, EvalRequest, Expr, ObjectCompat, Result, Symbol, Value,
8};
9use sim_lib_intent::{Origin, intent};
10use sim_lib_interference_core::{
11    Emitter, FieldAmplitude, Hertz, InterferenceProblem, MetresPerSecond, NepersPerMetre, Point3M,
12    PositiveMetres, Radians, SamplingPlane, ScalarMedium, SourceSet, UnitVector3,
13};
14use sim_lib_interference_runtime::{
15    InterferenceLib, InterferenceRecordsLib, ScalarProjectionDescriptor, StudyDescriptor,
16};
17use sim_lib_interference_solve::ReferencePhasorSolver;
18use sim_lib_view::{Operation, SurfaceCodec, surface};
19use sim_value::{access, build};
20
21use crate::{
22    INTERFERENCE_PROJECT_CAPABILITY, INTERFERENCE_SOLVE_CAPABILITY, InterferenceSurfaceCodec,
23};
24
25/// Runs the complete checked recipe and returns its stable report lines.
26///
27/// The recipe encodes a solved study, realizes one projection edit and one
28/// model edit as ordinary [`Operation`] values, refreshes the Scene from the
29/// realized study, then proves domain detector reduction against explicit
30/// desktop, phone, and glance budgets.
31pub fn interference_study_demo() -> Result<Vec<String>> {
32    let mut cx = recipe_cx()?;
33    let codec = InterferenceSurfaceCodec::new();
34    let study = solved_study(96, 96)?;
35    let base = study.as_expr(&mut cx)?;
36    let initial = codec.encode(&mut cx, &base, &surface::preset("desktop").unwrap())?;
37    let mut lines = vec![format!(
38        "encode={} evidence=Study/{}",
39        scene_kind(&initial),
40        study.evidence.sampling.verdict
41    )];
42
43    let project = checked_operation(
44        &codec,
45        &mut cx,
46        &base,
47        edit(&base, &["observable"], Expr::Symbol(Symbol::new("phase"))),
48    )?;
49    let projected = realize(&mut cx, &project)?;
50    let projection = projected
51        .object()
52        .downcast_ref::<ScalarProjectionDescriptor>()
53        .expect("projection Operation result_shape admitted the runtime record");
54    lines.push(format!(
55        "project-edit={} realized=interference/Projection target={}x{}",
56        operation_head(&project.form),
57        projection.rows,
58        projection.columns
59    ));
60
61    let model = checked_operation(
62        &codec,
63        &mut cx,
64        &base,
65        edit(&base, &["frequency"], build::float(2.0)),
66    )?;
67    let refreshed = realize(&mut cx, &model)?;
68    let refreshed_study = refreshed
69        .object()
70        .downcast_ref::<StudyDescriptor>()
71        .expect("model Operation result_shape admitted the runtime Study");
72    let refreshed_expr = refreshed_study.as_expr(&mut cx)?;
73    let refreshed_scene = codec.encode(
74        &mut cx,
75        &refreshed_expr,
76        &surface::preset("desktop").unwrap(),
77    )?;
78    lines.push(format!(
79        "model-edit={} frequency-hz={} refreshed={}",
80        operation_head(&model.form),
81        refreshed_study.problem.frequency_hz,
82        scene_kind(&refreshed_scene)
83    ));
84
85    for (preset, budget) in [("desktop", 1_024_u64), ("phone", 256), ("watch", 64)] {
86        let mut caps = surface::preset(preset).expect("published surface preset");
87        set_display_limit(&mut caps, "heatmap-max-cells", budget);
88        set_display_limit(&mut caps, "heatmap-max-bytes", 64 * 1_024);
89        let scene = codec.encode(&mut cx, &refreshed_expr, &caps)?;
90        let heatmap = find_kind(&scene, "heatmap").expect("interference Scene heatmap");
91        let certificate =
92            access::field(heatmap, "projection-certificate").expect("projection certificate");
93        let target_rows = integer_field(heatmap, "rows");
94        let target_columns = integer_field(heatmap, "cols");
95        let cells = target_rows * target_columns;
96        assert!(cells <= budget, "projected cells exceed the surface budget");
97        let surface_name = if preset == "watch" { "glance" } else { preset };
98        lines.push(format!(
99            "surface={surface_name} source={}x{} target={target_rows}x{target_columns} cells={cells}/{budget} detector={}",
100            study.field.rows,
101            study.field.cols,
102            access::field_sym(certificate, "detector")
103                .expect("detector rule")
104        ));
105    }
106    Ok(lines)
107}
108
109fn recipe_cx() -> Result<Cx> {
110    let (mut cx, seat) = Cx::new_seated(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
111    cx.load_lib(&InterferenceRecordsLib)?;
112    cx.load_lib(&InterferenceLib)?;
113    seat.grant(&mut cx, sim_kernel::read_construct_capability())?;
114    seat.grant(
115        &mut cx,
116        CapabilityName::new(INTERFERENCE_PROJECT_CAPABILITY),
117    )?;
118    seat.grant(&mut cx, CapabilityName::new(INTERFERENCE_SOLVE_CAPABILITY))?;
119    Ok(cx)
120}
121
122fn solved_study(rows: usize, columns: usize) -> Result<StudyDescriptor> {
123    let point = |x, y, z| Point3M::from_metres(x, y, z);
124    let problem = InterferenceProblem::new(
125        Hertz::new(1.0).expect("positive recipe frequency"),
126        ScalarMedium::new(
127            MetresPerSecond::new(100.0).expect("positive recipe wave speed"),
128            NepersPerMetre::new(0.0).expect("finite recipe attenuation"),
129        ),
130        domain(SourceSet::new(vec![Emitter::ForwardPlane {
131            id: "recipe-plane".to_owned(),
132            through: domain(point(0.0, 0.0, 0.0))?,
133            direction: domain(UnitVector3::new(0.0, 0.0, 1.0))?,
134            amplitude: domain(FieldAmplitude::new(2.0))?,
135            phase: domain(Radians::new(0.0))?,
136        }]))?,
137        domain(PositiveMetres::new(0.001))?,
138    );
139    let plane = domain(SamplingPlane::new(
140        domain(point(0.0, 0.0, 1.0))?,
141        domain(UnitVector3::new(1.0, 0.0, 0.0))?,
142        domain(UnitVector3::new(0.0, 1.0, 0.0))?,
143        domain(PositiveMetres::new(1.0))?,
144        domain(PositiveMetres::new(1.0))?,
145        rows,
146        columns,
147    ))?;
148    let (field, evidence) = domain(ReferencePhasorSolver::default().solve(&problem, &plane))?;
149    StudyDescriptor::from_reference(&problem, plane, field, &evidence)
150}
151
152fn domain<T, E: std::fmt::Debug>(result: std::result::Result<T, E>) -> Result<T> {
153    result.map_err(|error| Error::Eval(format!("interference recipe fixture failed: {error:?}")))
154}
155
156fn checked_operation(
157    codec: &InterferenceSurfaceCodec,
158    cx: &mut Cx,
159    base: &Expr,
160    submitted: Expr,
161) -> Result<Operation> {
162    let draft = codec.decode(cx, base, &submitted)?;
163    assert!(draft.committable, "recipe edit must be committable");
164    codec.commit(cx, &draft)
165}
166
167fn realize(cx: &mut Cx, operation: &Operation) -> Result<sim_kernel::Value> {
168    let reply = RecipeFabric.realize(
169        cx,
170        EvalRequest {
171            expr: operation.form.clone(),
172            result_shape: operation.result_shape.clone(),
173            required_capabilities: operation.required_capabilities.clone(),
174            deadline: None,
175            consistency: Consistency::LocalOnly,
176            mode: EvalMode::Eval,
177            answer_limit: None,
178            stream_buffer: None,
179            stream: false,
180            trace: false,
181        },
182    )?;
183    Ok(reply.value)
184}
185
186struct RecipeFabric;
187
188impl EvalFabric for RecipeFabric {
189    fn realize(&self, cx: &mut Cx, request: EvalRequest) -> Result<EvalReply> {
190        for capability in &request.required_capabilities {
191            cx.require(capability)?;
192        }
193        let value = evaluate_operation(cx, request.expr)?;
194        if let Some(shape) = request.result_shape {
195            let shape = shape.object().as_shape().expect("registered Shape value");
196            let matched = shape.check_value(cx, value.clone())?;
197            assert!(
198                matched.accepted,
199                "realized result must satisfy Operation shape"
200            );
201        }
202        Ok(EvalReply {
203            value,
204            diagnostics: cx.take_diagnostics(),
205            trace: None,
206        })
207    }
208}
209
210fn evaluate_operation(cx: &mut Cx, form: Expr) -> Result<Value> {
211    let Expr::Call { operator, args } = form else {
212        return Err(Error::Eval(
213            "interference recipe Operation must be an evaluable call".to_owned(),
214        ));
215    };
216    let Expr::Symbol(function) = operator.as_ref() else {
217        return Err(Error::Eval(
218            "interference recipe Operation requires a symbol operator".to_owned(),
219        ));
220    };
221    let args = args
222        .iter()
223        .map(|arg| operation_argument(cx, arg))
224        .collect::<Result<Vec<_>>>()?;
225    cx.call_function(function, Args::new(args))
226}
227
228fn operation_argument(cx: &mut Cx, expr: &Expr) -> Result<Value> {
229    let Expr::Extension { tag, payload } = expr else {
230        return cx.eval_expr(expr.clone());
231    };
232    if *tag != Symbol::qualified("citizen", "read-construct") {
233        return cx.eval_expr(expr.clone());
234    }
235    let Expr::Vector(parts) = payload.as_ref() else {
236        return Err(Error::Eval(
237            "citizen read-construct payload must be a vector".to_owned(),
238        ));
239    };
240    let Some((Expr::Symbol(class), args)) = parts.split_first() else {
241        return Err(Error::Eval(
242            "citizen read-construct must begin with a class symbol".to_owned(),
243        ));
244    };
245    let args = args
246        .iter()
247        .map(|arg| sim_citizen::value_from_expr(cx, arg))
248        .collect::<Result<Vec<_>>>()?;
249    cx.read_construct(class, args)
250}
251
252fn edit(base: &Expr, path: &[&str], value: Expr) -> Expr {
253    intent(
254        "edit-field",
255        Origin::human(17),
256        vec![
257            ("target", base.clone()),
258            (
259                "path",
260                Expr::List(
261                    path.iter()
262                        .map(|segment| Expr::Symbol(Symbol::new(*segment)))
263                        .collect(),
264                ),
265            ),
266            ("value", value),
267        ],
268    )
269}
270
271fn operation_head(form: &Expr) -> &str {
272    let Expr::Call { operator, .. } = form else {
273        panic!("recipe Operation must be an evaluable call");
274    };
275    let Expr::Symbol(symbol) = operator.as_ref() else {
276        panic!("recipe Operation call must have a symbol operator");
277    };
278    symbol.name.as_ref()
279}
280
281fn scene_kind(scene: &Expr) -> String {
282    sim_lib_scene::node_kind(scene)
283        .expect("recipe output is a Scene")
284        .to_string()
285}
286
287fn find_kind<'a>(expr: &'a Expr, expected: &str) -> Option<&'a Expr> {
288    if matches!(
289        sim_lib_scene::node_kind(expr),
290        Some(kind) if kind.namespace.as_deref() == Some("scene") && kind.name.as_ref() == expected
291    ) {
292        return Some(expr);
293    }
294    match expr {
295        Expr::Map(entries) => entries.iter().find_map(|(key, value)| {
296            find_kind(key, expected).or_else(|| find_kind(value, expected))
297        }),
298        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
299            items.iter().find_map(|item| find_kind(item, expected))
300        }
301        _ => None,
302    }
303}
304
305fn set_display_limit(caps: &mut sim_lib_view::SurfaceCaps, field: &str, value: u64) {
306    let Expr::Map(entries) = &mut caps.display else {
307        panic!("display caps must be a map")
308    };
309    if let Some((_, found)) = entries.iter_mut().find(
310        |(key, _)| matches!(key, Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == field),
311    ) {
312        *found = build::uint(value);
313    } else {
314        entries.push((Expr::Symbol(Symbol::new(field)), build::uint(value)));
315    }
316}
317
318fn integer_field(expr: &Expr, field: &str) -> u64 {
319    let Expr::Number(number) = access::field(expr, field).expect("integer field") else {
320        panic!("{field} is not a number")
321    };
322    number.canonical.parse().expect("canonical integer")
323}