1use std::collections::BTreeMap;
18
19use super::families::{Extract, Families, Family, How, OpSpec, ParamKind, Values};
20use super::{Class, Compared, Diff, Finding, Subject, VennError};
21use crate::ir::{Circuit, Node, NodeIdx, OpKind};
22
23#[derive(Debug, Clone)]
25pub struct Usage<'a> {
26 pub recipe: &'a str,
28 pub node: &'a Node,
30 pub family: &'a Family,
32 pub kernels: String,
34 pub point: Values,
36 pub runtime: Values,
38}
39
40pub fn usages<'a>(
42 against: &[Subject<'a>],
43 fams: &'a Families,
44) -> Result<Vec<Usage<'a>>, VennError> {
45 let mut out = Vec::new();
46 for s in against {
47 let Some(plan) = s.plan else {
48 return Err(VennError::Load(format!(
49 "{}: a compared model needs a plan",
50 s.recipe
51 )));
52 };
53 for g in &plan.groups {
54 let kernels = if g.kernels.is_empty() {
55 format!("({} emitter)", g.emitter)
56 } else {
57 g.kernels
58 .iter()
59 .map(|k| k.to_string())
60 .collect::<Vec<_>>()
61 .join(" + ")
62 };
63 for &n in &g.nodes {
64 let node = &s.circuit.nodes[n];
65 let family = fams
66 .of_group(&g.kernels, &g.emitter, &node.op)
67 .ok_or_else(|| VennError::UnmappedKernel {
68 recipe: s.recipe.to_string(),
69 node: node.id.clone(),
70 op: node.op.name(),
71 kernels: kernels.clone(),
72 })?;
73 let (point, runtime) = point_of(family, s, node)?;
74 out.push(Usage {
75 recipe: s.recipe,
76 node,
77 family,
78 kernels: kernels.clone(),
79 point,
80 runtime,
81 });
82 }
83 }
84 }
85 Ok(out)
86}
87
88pub fn candidates<'f>(fams: &'f Families, c: &Circuit, n: &Node, rows: u64) -> Vec<&'f Family> {
90 fams.families
91 .iter()
92 .filter(|f| (f.rows.0..=f.rows.1).contains(&rows))
93 .filter(|f| implements(f, c, n))
94 .collect()
95}
96
97pub fn implements(f: &Family, c: &Circuit, n: &Node) -> bool {
99 f.ops.iter().any(|s| op_matches(s, c, n))
100}
101
102fn op_matches(s: &OpSpec, c: &Circuit, n: &Node) -> bool {
103 let role_ok = match n.op {
104 OpKind::Linear(r) => s.roles.is_empty() || s.roles.contains(&r),
105 _ => true,
106 };
107 let input = n.inputs.first().map(|&e| c.edges[e].format);
108 let feeds_ok = s.feeds.is_empty()
109 || n.outputs.first().is_some_and(|&e| {
110 c.edges[e]
111 .consumers
112 .iter()
113 .any(|&k| names(&s.feeds, &c.nodes[k].op))
114 });
115 let after_ok = s.after.is_empty()
116 || n.inputs.first().is_some_and(|&e| {
117 c.edges[e]
118 .producer
119 .is_some_and(|p| names(&s.after, &c.nodes[p].op))
120 });
121 let beside_ok = s.beside.is_empty()
122 || n.inputs.first().is_some_and(|&e| {
123 c.edges[e]
124 .consumers
125 .iter()
126 .any(|&k| c.nodes[k].id != n.id && names(&s.beside, &c.nodes[k].op))
127 });
128 s.names(&n.op)
129 && after_ok
130 && beside_ok
131 && role_ok
132 && (s.weight.is_empty() || n.weight.is_some_and(|w| s.weight.contains(&w)))
133 && (s.activation.is_empty() || input.is_some_and(|a| s.activation.contains(&a)))
134 && s.params.iter().all(|(k, v)| n.params.get(k) == Some(v))
135 && feeds_ok
136}
137
138fn names(set: &std::collections::BTreeSet<String>, op: &OpKind) -> bool {
140 set.contains(op.base_name()) || set.contains(&op.name())
141}
142
143fn extract(s: &Subject<'_>, n: &Node, ex: &Extract) -> Option<String> {
144 let c = s.circuit;
145 let first_out = || n.outputs.first().map(|&e| &c.edges[e]);
146 match ex {
147 Extract::Dim(d) => c.dims.get(d).map(u64::to_string),
148 Extract::Weight => n.weight.map(|w| w.name()),
149 Extract::Activation => n.inputs.first().map(|&e| c.edges[e].format.name()),
150 Extract::Output => first_out().map(|e| e.format.name()),
151 Extract::Op => Some(n.op.name()),
152 Extract::ConsumerOp => first_out()
153 .and_then(|e| e.consumers.first())
154 .map(|&k| c.nodes[k].op.name()),
155 Extract::Param(k) => n.params.get(k).cloned(),
156 Extract::Setting(k) => s.settings.get(k).cloned(),
157 Extract::InDim => n.inputs.first().map(|&e| c.edges[e].dim_value.to_string()),
158 Extract::OutDim => first_out().map(|e| e.dim_value.to_string()),
159 }
160}
161
162pub fn point_of(f: &Family, s: &Subject<'_>, n: &Node) -> Result<(Values, Values), VennError> {
164 let (mut point, mut runtime) = (Values::new(), Values::new());
165 for p in &f.params {
166 let Some(ex) = p.from.get(n.op.base_name()) else {
167 continue;
168 };
169 let v = extract(s, n, ex)
170 .or_else(|| p.absent.clone())
171 .ok_or_else(|| VennError::MissingValue {
172 family: f.id.clone(),
173 param: p.name.clone(),
174 node: n.id.clone(),
175 })?;
176 if p.kind == ParamKind::Runtime {
177 runtime.insert(p.name.clone(), v);
178 } else {
179 point.insert(p.name.clone(), v);
180 }
181 }
182 Ok((point, runtime))
183}
184
185fn diffs(f: &Family, target: &Values, other: &Values) -> Vec<Diff> {
186 let mut out = Vec::new();
187 for (k, v) in target {
188 let o = other.get(k).cloned().unwrap_or_default();
189 if &o != v {
190 let kind = f.param(k).map_or(ParamKind::Runtime, |p| p.kind);
191 out.push(Diff {
192 param: k.clone(),
193 kind,
194 target: v.clone(),
195 other: o,
196 });
197 }
198 }
199 out
200}
201
202fn opportunity(pointed: &[&Diff]) -> Class {
203 if pointed.iter().all(|d| d.kind == ParamKind::Policy) {
204 Class::PolicyVariant
205 } else {
206 Class::ParameterizationOpportunity
207 }
208}
209
210fn evidence(f: &Family, point: &Values, rows: u64) -> Vec<super::families::EvidenceSource> {
211 f.evidence
212 .iter()
213 .filter(|e| e.rows.contains(&rows) && point.iter().all(|(k, v)| e.point.get(k) == Some(v)))
214 .map(|e| e.source.clone())
215 .collect()
216}
217
218fn finding(
219 f: &Family,
220 s: &Subject<'_>,
221 n: &Node,
222 rows: u64,
223 used: &[Usage<'_>],
224) -> Result<Finding, VennError> {
225 let (point, runtime) = point_of(f, s, n)?;
226 let inst = f
227 .points
228 .iter()
229 .find(|p| point.iter().all(|(k, v)| p.values.get(k) == Some(v)));
230 let ev = evidence(f, &point, rows);
231 let shared = if ev.is_empty() {
232 Class::SharedUnmeasured
233 } else {
234 Class::Shared
235 };
236 let best = used
237 .iter()
238 .filter(|u| std::ptr::eq(u.family, f) && u.node.op.base_name() == n.op.base_name())
239 .map(|u| {
240 let mut d = diffs(f, &point, &u.point);
241 d.extend(diffs(f, &runtime, &u.runtime));
242 (u, d)
243 })
244 .min_by_key(|(_, d)| d.iter().filter(|x| x.kind != ParamKind::Runtime).count());
245 let (class, compared, d) = match best {
246 Some((u, d)) => {
247 let pointed: Vec<&Diff> = d.iter().filter(|x| x.kind != ParamKind::Runtime).collect();
248 let class = if pointed.is_empty() || inst.is_some_and(|p| p.how != How::Copy) {
249 shared
250 } else {
251 opportunity(&pointed)
252 };
253 let compared = Compared::Model {
254 recipe: u.recipe.to_string(),
255 node: u.node.id.clone(),
256 kernels: u.kernels.clone(),
257 };
258 (class, compared, d)
259 }
260 None => {
261 let (q, d) = f
262 .points
263 .iter()
264 .map(|p| (p, diffs(f, &point, &p.values)))
265 .min_by_key(|(_, d)| d.len())
266 .ok_or_else(|| VennError::Load(format!("family `{}` has no points", f.id)))?;
267 let pointed: Vec<&Diff> = d.iter().collect();
268 let class = if pointed.is_empty() {
269 shared
270 } else {
271 opportunity(&pointed)
272 };
273 let compared = Compared::Point {
274 how: q.how,
275 files: q.files.clone(),
276 };
277 (class, compared, d)
278 }
279 };
280 Ok(Finding {
281 family: f.id.clone(),
282 class,
283 point,
284 compared,
285 diffs: d,
286 instantiated: inst.map(|p| p.how),
287 evidence: if matches!(class, Class::Shared | Class::SharedUnmeasured) {
288 ev
289 } else {
290 Vec::new()
291 },
292 })
293}
294
295pub fn classify_node(
298 target: &Subject<'_>,
299 idx: NodeIdx,
300 rows: u64,
301 used: &[Usage<'_>],
302 fams: &Families,
303) -> Result<(Option<Finding>, Vec<Finding>), VennError> {
304 let n = &target.circuit.nodes[idx];
305 let planned = match target.plan {
306 Some(plan) => {
307 let g = plan
308 .groups
309 .iter()
310 .find(|g| g.nodes.contains(&idx))
311 .ok_or_else(|| {
312 VennError::Load(format!(
313 "{}: node `{}` is in no plan group",
314 target.recipe, n.id
315 ))
316 })?;
317 let f = fams
318 .of_group(&g.kernels, &g.emitter, &n.op)
319 .ok_or_else(|| VennError::UnmappedKernel {
320 recipe: target.recipe.to_string(),
321 node: n.id.clone(),
322 op: n.op.name(),
323 kernels: g
324 .kernels
325 .iter()
326 .map(|k| k.to_string())
327 .collect::<Vec<_>>()
328 .join(" + "),
329 })?;
330 Some(f)
331 }
332 None => None,
333 };
334 let order: BTreeMap<&str, usize> = fams
335 .families
336 .iter()
337 .enumerate()
338 .map(|(i, f)| (f.id.as_str(), i))
339 .collect();
340 let mut all = Vec::new();
341 let mut fams_for: Vec<&Family> = candidates(fams, target.circuit, n, rows);
342 if let Some(f) = planned
343 && !fams_for.iter().any(|g| std::ptr::eq(*g, f))
344 {
345 fams_for.push(f);
346 }
347 for f in fams_for {
348 all.push(finding(f, target, n, rows, used)?);
349 }
350 let key = |x: &Finding| {
351 (
352 x.class,
353 x.diffs
354 .iter()
355 .filter(|d| d.kind != ParamKind::Runtime)
356 .count(),
357 matches!(x.compared, Compared::Point { .. }),
358 order[x.family.as_str()],
359 )
360 };
361 all.sort_by_key(key);
362 let primary_pos = match planned {
363 Some(f) => all.iter().position(|x| x.family == f.id),
364 None => (!all.is_empty()).then_some(0),
365 };
366 let Some(pos) = primary_pos else {
367 return Ok((None, Vec::new()));
368 };
369 let primary = all.remove(pos);
370 let also = all
371 .into_iter()
372 .filter(|x| {
373 matches!(
374 x.class,
375 Class::PolicyVariant | Class::ParameterizationOpportunity
376 )
377 })
378 .collect();
379 Ok((Some(primary), also))
380}