1use crate::graph::LayoutGraph;
8use std::collections::BTreeSet;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum GroupKind {
13 GainStage,
16 ToneStack,
18 PushPullOutput,
20 PhaseInverter,
22 OpAmpStage,
24 InputSection,
26 OutputSection,
28 Generic,
30}
31
32#[derive(Debug, Clone)]
34pub struct FunctionalGroup {
35 pub name: String,
37 pub label: String,
39 pub kind: GroupKind,
41 pub members: Vec<usize>,
43 pub primary_device: Option<usize>,
45}
46
47pub fn detect_groups(graph: &LayoutGraph) -> Vec<FunctionalGroup> {
49 let mut assigned: BTreeSet<usize> = BTreeSet::new();
50 let mut groups = Vec::new();
51
52 assigned.insert(graph.in_node);
54 assigned.insert(graph.out_node);
55 assigned.insert(graph.vcc_node);
56 assigned.insert(graph.gnd_node);
57
58 let gain_stages = detect_gain_stages(graph, &assigned);
60 for gs in &gain_stages {
61 for &m in &gs.members {
62 assigned.insert(m);
63 }
64 }
65 groups.extend(gain_stages);
66
67 let opamp_stages = detect_opamp_stages(graph, &assigned);
69 for os in &opamp_stages {
70 for &m in &os.members {
71 assigned.insert(m);
72 }
73 }
74 groups.extend(opamp_stages);
75
76 let pp = detect_push_pull(graph, &assigned);
78 for g in &pp {
79 for &m in &g.members {
80 assigned.insert(m);
81 }
82 }
83 groups.extend(pp);
84
85 let tone_stacks = detect_tone_stacks(graph, &assigned);
87 for ts in &tone_stacks {
88 for &m in &ts.members {
89 assigned.insert(m);
90 }
91 }
92 groups.extend(tone_stacks);
93
94 let remaining = assign_remaining(graph, &assigned);
96 groups.extend(remaining);
97
98 groups
99}
100
101fn detect_gain_stages(graph: &LayoutGraph, assigned: &BTreeSet<usize>) -> Vec<FunctionalGroup> {
106 let mut groups = Vec::new();
107 let mut stage_num = 0;
108 let mut locally_assigned: BTreeSet<usize> = BTreeSet::new();
109
110 for node in &graph.nodes {
111 if assigned.contains(&node.id) || locally_assigned.contains(&node.id) {
112 continue;
113 }
114
115 let is_gain_device = node
116 .comp
117 .as_ref()
118 .map_or(false, |c| c.kind.is_gain_device());
119
120 if !is_gain_device {
121 continue;
122 }
123
124 stage_num += 1;
125 let mut members = vec![node.id];
126
127 let neighbors = graph.neighbors(node.id);
133 for &nid in &neighbors {
134 if assigned.contains(&nid) || locally_assigned.contains(&nid) || nid == node.id {
135 continue;
136 }
137 if !graph.is_passive(nid) {
138 continue;
139 }
140
141 if graph.connects_to_vcc(nid) || graph.connects_to_gnd(nid) {
143 members.push(nid);
144 continue;
145 }
146
147 let passive_neighbors = graph.neighbors(nid);
150 if passive_neighbors.len() <= 5 {
151 let connects_to_other_active = passive_neighbors.iter().any(|&pid| {
153 pid != node.id
154 && !assigned.contains(&pid)
155 && !locally_assigned.contains(&pid)
156 && graph.is_active_device(pid)
157 });
158 if !connects_to_other_active {
159 members.push(nid);
160 }
161 }
162 }
163
164 for &m in &members {
165 locally_assigned.insert(m);
166 }
167
168 let device_name = &node.comp_id;
169 let kind_name = node
170 .comp
171 .as_ref()
172 .map_or("Gain", |c| match c.kind.layout_class() {
173 "triode" | "vari_mu" => "Triode",
174 "pentode" => "Pentode",
175 "npn" | "pnp" => "BJT",
176 "njfet" | "pjfet" => "JFET",
177 "nmos" | "pmos" => "MOSFET",
178 _ => "Gain",
179 });
180
181 groups.push(FunctionalGroup {
182 name: format!("gain_stage_{stage_num}"),
183 label: format!("{device_name} {kind_name} Stage"),
184 kind: GroupKind::GainStage,
185 members,
186 primary_device: Some(node.id),
187 });
188 }
189
190 groups
191}
192
193fn detect_opamp_stages(graph: &LayoutGraph, assigned: &BTreeSet<usize>) -> Vec<FunctionalGroup> {
198 let mut groups = Vec::new();
199 let mut stage_num = 0;
200 let mut locally_assigned: BTreeSet<usize> = BTreeSet::new();
201
202 for node in &graph.nodes {
203 if assigned.contains(&node.id) || locally_assigned.contains(&node.id) {
204 continue;
205 }
206
207 let is_opamp = node
208 .comp
209 .as_ref()
210 .map_or(false, |c| c.kind.op_amp_type().is_some());
211
212 if !is_opamp {
213 continue;
214 }
215
216 stage_num += 1;
217 let mut members = vec![node.id];
218
219 let neighbors = graph.neighbors(node.id);
221 for &nid in &neighbors {
222 if assigned.contains(&nid) || locally_assigned.contains(&nid) || nid == node.id {
223 continue;
224 }
225 let is_feedback_component = graph
227 .node_kind(nid)
228 .map_or(false, |k| k.is_simple_passive() || k.is_diode_family());
229 if is_feedback_component {
230 members.push(nid);
231 }
232 }
233
234 for &m in &members {
235 locally_assigned.insert(m);
236 }
237
238 let device_name = &node.comp_id;
239 groups.push(FunctionalGroup {
240 name: format!("opamp_stage_{stage_num}"),
241 label: format!("{device_name} Op-Amp Stage"),
242 kind: GroupKind::OpAmpStage,
243 members,
244 primary_device: Some(node.id),
245 });
246 }
247
248 groups
249}
250
251fn detect_push_pull(graph: &LayoutGraph, assigned: &BTreeSet<usize>) -> Vec<FunctionalGroup> {
256 let mut groups = Vec::new();
257
258 let transformers: Vec<usize> = graph
260 .nodes
261 .iter()
262 .filter(|n| {
263 !assigned.contains(&n.id) && n.comp.as_ref().map_or(false, |c| c.kind.is_transformer())
264 })
265 .map(|n| n.id)
266 .collect();
267
268 for tx_id in transformers {
269 let mut members = vec![tx_id];
270
271 let neighbors = graph.neighbors(tx_id);
273 let pentodes: Vec<usize> = neighbors
274 .iter()
275 .filter(|&&nid| {
276 !assigned.contains(&nid)
277 && graph
278 .node_kind(nid)
279 .map_or(false, |k| k.layout_class() == "pentode")
280 })
281 .copied()
282 .collect();
283
284 if pentodes.len() >= 2 {
285 for &pid in &pentodes {
287 members.push(pid);
288 for &nid in &graph.neighbors(pid) {
290 if !assigned.contains(&nid) && graph.is_passive(nid) {
291 if graph.connects_to_vcc(nid) || graph.connects_to_gnd(nid) {
292 members.push(nid);
293 }
294 }
295 }
296 }
297
298 groups.push(FunctionalGroup {
299 name: "push_pull_output".into(),
300 label: "Push-Pull Output".into(),
301 kind: GroupKind::PushPullOutput,
302 members,
303 primary_device: Some(tx_id),
304 });
305 }
306 }
307
308 groups
309}
310
311fn detect_tone_stacks(graph: &LayoutGraph, assigned: &BTreeSet<usize>) -> Vec<FunctionalGroup> {
316 let mut groups = Vec::new();
317
318 let unassigned_pots: Vec<usize> = graph
320 .nodes
321 .iter()
322 .filter(|n| !assigned.contains(&n.id) && n.comp.as_ref().map_or(false, |c| c.kind.is_pot()))
323 .map(|n| n.id)
324 .collect();
325
326 if unassigned_pots.is_empty() {
327 return groups;
328 }
329
330 let mut pot_clusters: Vec<BTreeSet<usize>> = Vec::new();
332
333 for &pot_id in &unassigned_pots {
334 let pot_neighbors = graph.neighbors(pot_id);
336 let mut matching_cluster = None;
337
338 for (ci, cluster) in pot_clusters.iter().enumerate() {
339 for &existing_pot in cluster.iter() {
341 let existing_neighbors = graph.neighbors(existing_pot);
342 if pot_neighbors
343 .intersection(&existing_neighbors)
344 .next()
345 .is_some()
346 {
347 matching_cluster = Some(ci);
348 break;
349 }
350 }
351 if matching_cluster.is_some() {
352 break;
353 }
354 }
355
356 match matching_cluster {
357 Some(ci) => {
358 pot_clusters[ci].insert(pot_id);
359 }
360 None => {
361 let mut cluster = BTreeSet::new();
362 cluster.insert(pot_id);
363 pot_clusters.push(cluster);
364 }
365 }
366 }
367
368 let mut ts_num = 0;
369 for cluster in pot_clusters {
370 ts_num += 1;
371 let mut members: Vec<usize> = cluster.iter().copied().collect();
372
373 for &pot_id in &cluster {
375 for &nid in &graph.neighbors(pot_id) {
376 if assigned.contains(&nid) || members.contains(&nid) {
377 continue;
378 }
379 if graph.is_passive(nid) && !graph.is_active_device(nid) {
380 members.push(nid);
381 }
382 }
383 }
384
385 let pot_names: Vec<&str> = members
386 .iter()
387 .filter_map(|&m| {
388 if graph.node_kind(m).map_or(false, |k| k.is_pot()) {
389 Some(graph.nodes[m].comp_id.as_str())
390 } else {
391 None
392 }
393 })
394 .collect();
395
396 let label = if pot_names.len() == 1 {
397 format!("{} Control", pot_names[0])
398 } else {
399 "Tone Stack".into()
400 };
401
402 groups.push(FunctionalGroup {
403 name: format!("tone_stack_{ts_num}"),
404 label,
405 kind: GroupKind::ToneStack,
406 members,
407 primary_device: None,
408 });
409 }
410
411 groups
412}
413
414fn assign_remaining(graph: &LayoutGraph, assigned: &BTreeSet<usize>) -> Vec<FunctionalGroup> {
419 let mut groups = Vec::new();
420 let mut input_members = Vec::new();
421 let mut output_members = Vec::new();
422 let mut generic_members = Vec::new();
423
424 for node in &graph.nodes {
425 if assigned.contains(&node.id) || node.is_anchor {
426 continue;
427 }
428
429 let connects_to_in = graph.edges.iter().any(|e| {
431 (e.from == graph.in_node && e.to == node.id)
432 || (e.from == node.id && e.to == graph.in_node)
433 });
434 let connects_to_out = graph.edges.iter().any(|e| {
435 (e.from == graph.out_node && e.to == node.id)
436 || (e.from == node.id && e.to == graph.out_node)
437 });
438
439 if connects_to_in {
440 input_members.push(node.id);
441 } else if connects_to_out {
442 output_members.push(node.id);
443 } else {
444 generic_members.push(node.id);
445 }
446 }
447
448 if !input_members.is_empty() {
449 groups.push(FunctionalGroup {
450 name: "input".into(),
451 label: "Input".into(),
452 kind: GroupKind::InputSection,
453 members: input_members,
454 primary_device: None,
455 });
456 }
457
458 if !output_members.is_empty() {
459 groups.push(FunctionalGroup {
460 name: "output".into(),
461 label: "Output".into(),
462 kind: GroupKind::OutputSection,
463 members: output_members,
464 primary_device: None,
465 });
466 }
467
468 if !generic_members.is_empty() {
469 groups.push(FunctionalGroup {
470 name: "misc".into(),
471 label: "Misc".into(),
472 kind: GroupKind::Generic,
473 members: generic_members,
474 primary_device: None,
475 });
476 }
477
478 groups
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484 use pedalkernel::compiler::components::*;
485 use pedalkernel::dsl::*;
486
487 #[test]
488 fn detect_triode_gain_stage() {
489 let pedal = PedalDef {
490 name: "Test".into(),
491 supplies: vec![],
492 components: vec![
493 ComponentDef {
494 id: "C1".into(),
495 kind: Box::new(Capacitor {
496 config: CapConfig::new(100e-9),
497 }),
498 },
499 ComponentDef {
500 id: "R1".into(),
501 kind: Box::new(Resistor { value: 1e6 }),
502 },
503 ComponentDef {
504 id: "V1".into(),
505 kind: Box::new(Triode {
506 model: "12AX7".into(),
507 }),
508 },
509 ComponentDef {
510 id: "R2".into(),
511 kind: Box::new(Resistor { value: 100e3 }),
512 },
513 ComponentDef {
514 id: "R3".into(),
515 kind: Box::new(Resistor { value: 1500.0 }),
516 },
517 ComponentDef {
518 id: "C2".into(),
519 kind: Box::new(Capacitor {
520 config: CapConfig::new(25e-6),
521 }),
522 },
523 ],
524 nets: vec![
525 NetDef {
526 from: Pin::Reserved("in".into()),
527 to: vec![Pin::ComponentPin {
528 component: "C1".into(),
529 pin: "a".into(),
530 }],
531 },
532 NetDef {
533 from: Pin::ComponentPin {
534 component: "C1".into(),
535 pin: "b".into(),
536 },
537 to: vec![
538 Pin::ComponentPin {
539 component: "R1".into(),
540 pin: "a".into(),
541 },
542 Pin::ComponentPin {
543 component: "V1".into(),
544 pin: "grid".into(),
545 },
546 ],
547 },
548 NetDef {
549 from: Pin::Reserved("vcc".into()),
550 to: vec![Pin::ComponentPin {
551 component: "R2".into(),
552 pin: "a".into(),
553 }],
554 },
555 NetDef {
556 from: Pin::ComponentPin {
557 component: "R2".into(),
558 pin: "b".into(),
559 },
560 to: vec![Pin::ComponentPin {
561 component: "V1".into(),
562 pin: "plate".into(),
563 }],
564 },
565 NetDef {
566 from: Pin::ComponentPin {
567 component: "V1".into(),
568 pin: "cathode".into(),
569 },
570 to: vec![
571 Pin::ComponentPin {
572 component: "R3".into(),
573 pin: "a".into(),
574 },
575 Pin::ComponentPin {
576 component: "C2".into(),
577 pin: "a".into(),
578 },
579 ],
580 },
581 NetDef {
582 from: Pin::ComponentPin {
583 component: "R3".into(),
584 pin: "b".into(),
585 },
586 to: vec![Pin::Reserved("gnd".into())],
587 },
588 NetDef {
589 from: Pin::ComponentPin {
590 component: "C2".into(),
591 pin: "b".into(),
592 },
593 to: vec![Pin::Reserved("gnd".into())],
594 },
595 NetDef {
596 from: Pin::ComponentPin {
597 component: "R1".into(),
598 pin: "b".into(),
599 },
600 to: vec![Pin::Reserved("gnd".into())],
601 },
602 NetDef {
603 from: Pin::ComponentPin {
604 component: "V1".into(),
605 pin: "plate".into(),
606 },
607 to: vec![Pin::Reserved("out".into())],
608 },
609 ],
610 controls: vec![],
611 trims: vec![],
612 monitors: vec![],
613 sidechains: vec![],
614 mirrors: Default::default(),
615 subtitle: None,
616 calibrate: false,
617 subcircuits: vec![],
618 ports: vec![],
619 init_hints: vec![],
620 uses: vec![],
621 };
622
623 let graph = LayoutGraph::from_pedal(&pedal);
624 let groups = detect_groups(&graph);
625
626 let gain_stages: Vec<_> = groups
628 .iter()
629 .filter(|g| g.kind == GroupKind::GainStage)
630 .collect();
631 assert!(!gain_stages.is_empty(), "Should detect a gain stage");
632
633 let gs = &gain_stages[0];
635 let has_triode = gs.members.iter().any(|&m| graph.nodes[m].comp_id == "V1");
636 assert!(has_triode, "Gain stage should contain the triode");
637 }
638}