1use 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
25pub 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}