pedalkernel/compiler/component.rs
1//! # Component Trait and Supporting Types
2//!
3//! This module defines [`Component`], the central trait that drives the entire
4//! PedalKernel compilation pipeline. Every circuit element — from a simple
5//! resistor to a 12AX7 vacuum tube — implements `Component` to declare its
6//! electrical behavior, topology, and runtime characteristics.
7//!
8//! # Design Philosophy
9//!
10//! **Components declare; the compiler reacts.** The pipeline never pattern-matches
11//! on component types. Instead, it queries trait methods to determine how each
12//! component participates in the circuit:
13//!
14//! - **What edges does it create?** ([`Component::edges`]) — determines graph topology
15//! - **Is it passive or nonlinear?** ([`Component::is_passive`], [`Component::is_nonlinear`]) — determines stage solver
16//! - **What is its output impedance?** ([`Component::output_impedance`]) — determines stage boundaries
17//! - **What non-ideal effects does it have?** ([`Component::nonideal_fx`]) — determines post-processing
18//! - **How does signal flow through it?** ([`Component::signal_terminals`]) — determines feedback loops
19//!
20//! This inversion of control means adding a new component type requires implementing
21//! one trait — no changes to the graph builder, SPQR decomposer, stage builder,
22//! or any other pipeline stage.
23//!
24//! # How Components Drive the Pipeline
25//!
26//! ## Stage Splitting via Output Impedance
27//!
28//! [`OutputImpedance`] controls where the compiler can safely split a circuit into
29//! independent processing stages. This is based on Harold Black's theorem:
30//! a negative-feedback amplifier's output behaves as a voltage source (zero
31//! output impedance), meaning downstream loads cannot affect upstream behavior.
32//!
33//! When a component returns [`OutputImpedance::VoltageSource`], the compiler knows
34//! it can insert a stage boundary after that component's output. Op-amps in
35//! negative feedback return `VoltageSource`; passive components return
36//! [`OutputImpedance::Finite`].
37//!
38//! ## Feedback Detection via Signal Terminals
39//!
40//! [`SignalTerminals`] drives the feedback analysis pass. The compiler uses Tarjan's
41//! strongly-connected-component algorithm to find feedback loops. Components with
42//! [`SignalTerminals::Amplifier`] define the forward path direction; passive
43//! components with [`SignalTerminals::Passive`] can carry signal in either direction
44//! and may form the feedback path.
45//!
46//! ## Post-Processing via Non-Ideal Effects
47//!
48//! [`NonIdealFx`] allows components to declare physical imperfections from their
49//! SPICE models and datasheets. An LM308 op-amp declares its 1 MHz GBW product
50//! and 0.3 V/us slew rate; the stage builder translates these into a first-order
51//! IIR lowpass and a slew-rate limiter applied after the ideal WDF/MNA solver.
52//!
53//! # Key Types
54//!
55//! - [`Component`] — the trait itself (see its documentation for method groups)
56//! - [`EdgeKind`] — electrical classification of a circuit edge (Linear, Reactive, Nonlinear, Vcvs, etc.)
57//! - [`ComponentEdge`] — a single edge declared by a component
58//! - [`GraphRole`] — how a component participates in circuit graph construction
59//! - [`OutputImpedance`] — voltage-source vs. finite impedance classification
60//! - [`SignalTerminals`] — signal flow directionality for feedback analysis
61//! - [`NonIdealFx`] — non-ideal behaviors (GBW, slew rate, rail saturation)
62//! - [`StampResult`] — result of stamping a component into an MNA matrix
63//! - [`StampContext`] — pin-to-MNA-index resolution for multi-terminal stamping
64//! - [`ControlParam`] — controllable parameter declaration (pots, LFO rate, etc.)
65//! - [`ModulationSink`] — how a component receives LFO/envelope modulation
66//! - [`SolverMethod`] — preferred nonlinear solver (Newton-Raphson, Wright Omega, Ebers-Moll, Gummel-Poon)
67
68use hashbrown::HashMap;
69
70use crate::tree::MnaSystem;
71
72use super::classify::NonlinearKind;
73use super::dyn_node::DynNode;
74use super::graph::NodeId;
75use super::validate::Severity;
76
77/// Resistance used to model open circuits (infinite R, inactive fork paths).
78pub(crate) const OPEN_CIRCUIT_R: f64 = 1_000_000.0;
79
80// ═══════════════════════════════════════════════════════════════════════════
81// Types
82// ═══════════════════════════════════════════════════════════════════════════
83
84/// Result of stamping a component into an MNA system.
85pub enum StampResult {
86 /// Stamped conductance directly into G matrix (resistors, tempcos, switched resistors).
87 Stamped,
88 /// Produces a reactive WDF port (capacitor, inductor, switched cap/inductor).
89 Reactive { dyn_node: DynNode, rp: f64 },
90 /// Produces a pot entry for dynamic recomputation.
91 Pot {
92 dyn_node: DynNode,
93 initial_conductance: f64,
94 },
95 /// Not stampable by the trait (transformer needs caller context, or non-passive).
96 Skip,
97}
98
99/// Context for multi-terminal MNA stamping.
100///
101/// Provides pin-to-MNA-index resolution and pre-allocated vsource indices
102/// so components can stamp themselves into the MNA without knowing graph details.
103pub struct StampContext<'a> {
104 /// Resolve a pin name (e.g., "pos", "neg", "out") to its MNA node index.
105 /// Returns `None` for pins connected to ground or supply rails.
106 pub pin_to_mna: &'a dyn Fn(&str) -> Option<usize>,
107 /// Starting vsource index allocated for this component.
108 pub vsrc_base: usize,
109 /// Starting internal MNA node index allocated for this component.
110 /// Components that declared `mna_internal_node_count() > 0` get indices
111 /// `[internal_node_base .. internal_node_base + mna_internal_node_count())`.
112 pub internal_node_base: usize,
113 /// Sample rate in Hz.
114 pub sample_rate: f64,
115 /// Reactive one-ports for state-space integration.
116 pub reactive_one_ports: Option<&'a mut Vec<pedalkernel_rt::boundary_math::MnaOnePort>>,
117}
118
119/// Pin configuration for validation and graph construction.
120pub struct PinConfig {
121 /// Valid pin names for this component type.
122 pub valid_pins: &'static [&'static str],
123 /// Pin aliases: (short, long) pairs that should be unioned in the graph.
124 pub aliases: &'static [(&'static str, &'static str)],
125}
126
127/// Inferred direction for a component pin (used by layout).
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum PinDirection {
130 /// Signal enters through this pin (e.g., triode grid).
131 Input,
132 /// Signal exits through this pin (e.g., triode plate).
133 Output,
134 /// Pin connects upward toward VCC (e.g., plate load destination).
135 Up,
136 /// Pin connects downward toward GND (e.g., cathode bias).
137 Down,
138 /// Direction determined by context (e.g., resistor terminals).
139 Bidirectional,
140}
141
142/// Component-declared non-ideal behavior applied as post-processing.
143///
144/// Output impedance classification for stage-splitting decisions.
145///
146/// Determines where the compiler can safely split the circuit into
147/// independent stages. Based on Harold Black's observation: a negative
148/// feedback amplifier's output is a voltage source (zero impedance),
149/// so downstream loads don't affect upstream behavior.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum OutputImpedance {
152 /// Zero output impedance — this node is a voltage source.
153 /// Safe to split the circuit after this component's output pin.
154 /// Op-amp outputs, unity-gain buffers, ideal voltage sources.
155 VoltageSource,
156 /// Finite or high output impedance — splitting here changes the circuit.
157 /// Resistors, caps, inductors, BJT collectors, JFET drains, diodes.
158 Finite,
159}
160
161pub use crate::nonideal_fx::NonIdealFx;
162
163/// Electrical behavior classification for a component port (pin pair).
164///
165/// Used by the compiler's optimization legality checks to determine whether
166/// a subgraph can be safely lowered to IIR/BlackFeedback, or must remain
167/// as full MNA/WDF. Nonlinear and ControlledConductance ports create
168/// coupling barriers that prevent unsafe optimization.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum PortSemantic {
171 /// Fixed impedance — R, voltage source. Safe to lower/optimize.
172 LinearPassive,
173 /// Stores energy between samples — C, L. Creates time-domain coupling.
174 Reactive,
175 /// I(V) is nonlinear — diode junction, BJT Vbe/Vce, tube plate.
176 /// Must be solved by NR or explicit solver. Optimization barrier.
177 Nonlinear,
178 /// Impedance varies with external control — JFET Rds, photocoupler LDR, pot.
179 /// Optimization barrier when coupled to reactive/feedback nodes.
180 ControlledConductance,
181 /// Voltage constraint (virtual ground, VCVS output). Defines topology.
182 VoltageConstraint,
183}
184
185/// Signal flow classification for a component's pins.
186///
187/// Used by feedback analysis to determine which components are coupled
188/// through feedback loops. The Component trait drives all decisions —
189/// no hardcoded component-type matching in the pipeline.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum SignalTerminals {
192 /// No directionality — R, C, L, pot. Signal flows either way.
193 Passive,
194 /// Unidirectional two-port — diode (a→b). Signal has a preferred direction.
195 TwoPort {
196 input: &'static str,
197 output: &'static str,
198 },
199 /// Amplifier with input, output, and optional control pin.
200 /// Op-amp: input=neg, output=out, control=Some(pos).
201 /// BJT: input=base, output=collector, control=None.
202 /// Tube: input=grid, output=plate, control=None.
203 Amplifier {
204 input: &'static str,
205 output: &'static str,
206 control: Option<&'static str>,
207 },
208}
209
210/// Classification of a circuit edge by electrical behavior.
211///
212/// The planner uses edge kinds to group components into stages:
213/// - Linear + Reactive = passive WDF tree
214/// - Nonlinear seeds NR solver stages
215/// - Vccs needs MNA off-diagonal stamps
216/// - Behavioral breaks the graph (separate stages on each side)
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub enum EdgeKind {
219 /// Purely resistive: R, pot sub-R, tempco, switched-R, photocoupler LDR, JFET Vr.
220 Linear,
221 /// Has state, must be a WDF port: C, L, switched-C, switched-L.
222 Reactive,
223 /// Needs Newton-Raphson solver: diode, BJT, JFET amplifier, triode, MOSFET, OTA.
224 Nonlinear,
225 /// Voltage-controlled current source: OTA in linear mode (future).
226 Vccs,
227 /// Voltage-controlled voltage source: op-amp nullor (neg→out edge,
228 /// pos resolved via stamp_mna_multi). Forces R-node in SPQR.
229 Vcvs,
230 /// Handled outside WDF/MNA: BBD, delay line.
231 Behavioral,
232}
233
234/// A single edge declared by a component.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub struct ComponentEdge {
237 pub pin_a: &'static str,
238 pub pin_b: &'static str,
239 pub kind: EdgeKind,
240 /// For multi-port NL grouping (e.g., triode grid+plate ports).
241 pub port_group: Option<usize>,
242}
243
244/// Semantic axis in a K-method lookup table.
245///
246/// Components declare these axes so table generation does not need to infer
247/// solver dimensionality from concrete component names. The compiler may add
248/// topology-derived axes, such as source impedance, when the adapted WDF block
249/// makes the nonlinear root depend on more than the device I-V coordinates.
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum KMethodAxis {
252 /// Incident wave at the nonlinear root.
253 IncidentWave,
254 /// Device control voltage/current, e.g. BJT Vbe or triode Vgk.
255 DeviceControl,
256 /// Bias/control voltage injected by the surrounding circuit, e.g. a CV
257 /// source summed into a nonlinear ladder bias node.
258 BiasVoltage,
259}
260
261/// Component-declared K-method table shape.
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub struct KMethodSpec {
264 pub axes: &'static [KMethodAxis],
265 pub reason: &'static str,
266}
267
268impl KMethodSpec {
269 pub const fn dimensions(self) -> usize {
270 self.axes.len()
271 }
272}
273
274pub const K_AXIS_INCIDENT_1D: &[KMethodAxis] = &[KMethodAxis::IncidentWave];
275pub const K_AXIS_INCIDENT_CONTROL_2D: &[KMethodAxis] =
276 &[KMethodAxis::IncidentWave, KMethodAxis::DeviceControl];
277pub const K_AXIS_INCIDENT_CONTROL_CONTROL_3D: &[KMethodAxis] = &[
278 KMethodAxis::IncidentWave,
279 KMethodAxis::DeviceControl,
280 KMethodAxis::DeviceControl,
281];
282
283// ── Control & modulation declarations ─────────────────────────────────────
284
285/// A controllable parameter declared by a component.
286#[derive(Debug, Clone, PartialEq)]
287pub struct ControlParam {
288 /// Parameter name, must match `ControlDef.property` from the DSL.
289 pub name: &'static str,
290 /// What kind of control target this maps to.
291 pub kind: ControlParamKind,
292}
293
294/// Classification of a component's controllable parameter.
295#[derive(Debug, Clone, PartialEq)]
296pub enum ControlParamKind {
297 /// Potentiometer position (0–1) — resolved to PotInStage/PotInMultiNlStage/etc.
298 PotPosition,
299 /// LFO rate (0–1 normalized).
300 LfoRate,
301 /// LFO depth/amplitude (0–1 normalized).
302 LfoDepth,
303 /// BBD clock rate (0–1 → delay time).
304 BbdClockRate,
305 /// BBD feedback amount (0–1).
306 BbdFeedback,
307 /// Delay line time (0–1 normalized).
308 DelayTime,
309 /// Delay line feedback (0–1).
310 DelayFeedback,
311 /// Spring reverb dwell/drive input trim (0–1 normalized).
312 SpringDwell,
313 /// Spring reverb decay → RT60 authority (0–1 normalized).
314 SpringDecay,
315 /// Spring reverb damping LPF cutoff (0–1 normalized).
316 SpringDamping,
317 /// Spring reverb wet/dry mix (0–1 normalized).
318 SpringMix,
319 /// Switch position selector.
320 SwitchPosition { num_positions: usize },
321 /// Fire a single-sample impulse (drum trigger).
322 Trigger,
323}
324
325/// Modulation sink: how a component receives LFO/envelope control signals.
326#[derive(Debug, Clone, PartialEq)]
327pub struct ModulationSink {
328 /// Which kind of modulation target this maps to.
329 pub target_kind: ModulationSinkKind,
330 /// Center/offset voltage for the modulation.
331 pub bias: f64,
332 /// Modulation amplitude (half-swing from bias).
333 pub range: f64,
334}
335
336/// Classification of a modulation sink by target type.
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum ModulationSinkKind {
339 JfetVgs,
340 PhotocouplerLed,
341 TriodeVgk,
342 VariMuVgk,
343 PentodeVg1k,
344 MosfetVgs,
345 OtaIabc,
346 BbdClock,
347 /// VCA control-voltage port: normalized 0..1 mapped onto 0..-80 dB
348 /// (SSM2164-class dB-linear law; see the compiler's `vca_lowering` pass
349 /// and the runtime `Vca::set_cv_normalized`).
350 VcaCv,
351 DelaySpeed,
352 DelayTime,
353 /// Spring reverb dwell/drive CV port (normalized 0..1).
354 SpringDwell,
355}
356
357/// Context provided to `resolve_edges()` so a component can decide its role
358/// based on how it's wired in the circuit.
359pub struct ResolveContext {
360 /// True if the component's control/modulation pin is driven by an LFO,
361 /// envelope follower, or DC bias (not in the audio signal path).
362 pub control_pin_is_modulated: bool,
363 /// True if the component's wiper pin (pots) appears in the netlist.
364 pub wiper_connected: bool,
365}
366
367/// How a component participates in circuit graph construction.
368pub enum GraphRole {
369 /// Creates one edge between two named pins.
370 Edge {
371 pin_a: &'static str,
372 pin_b: &'static str,
373 },
374 /// Creates one edge AND counts as an active element.
375 ActiveEdge {
376 pin_a: &'static str,
377 pin_b: &'static str,
378 },
379 /// Creates one edge with multi-terminal coupling (tubes).
380 /// `edge_pin_a` → `edge_pin_b` is the WDF edge.
381 /// `coupled_pins` lists ALL pins that should be coupled for BFS traversal.
382 CoupledEdge {
383 edge_pin_a: &'static str,
384 edge_pin_b: &'static str,
385 coupled_pins: &'static [&'static str],
386 },
387 /// Potentiometer: 2-terminal or 3-terminal depending on wiper usage in nets.
388 Pot,
389 /// Transformer: complex multi-winding handling (aliases, coupling, center taps).
390 Transformer,
391 /// No circuit edge, not active (LFO, EnvelopeFollower, BBD, control elements).
392 Virtual,
393 /// No circuit edge, but counts as active (OpAmp, VCO, VCF, etc.).
394 ActiveIc,
395 /// Op-amp / finite-gain VCVS: three pins (pos, neg, out) participate in
396 /// the circuit graph as junction nodes. Produces no WDF-tree edge but is
397 /// absorbed as a VCVS stamp into the MNA matrix of its containing R-type
398 /// adaptor (Werner 2016 nullor, generalised to finite Aol and Ro).
399 ///
400 /// The three pins are registered in `CircuitGraph::nullor_pins` so the
401 /// R-type stage builder can emit one `MnaSystem::stamp_vcvs` per op-amp
402 /// with the appropriate datasheet parameters.
403 VcvsEdge {
404 pin_pos: &'static str,
405 pin_neg: &'static str,
406 pin_out: &'static str,
407 },
408}
409
410// ═══════════════════════════════════════════════════════════════════════════
411// Solver method hint
412// ═══════════════════════════════════════════════════════════════════════════
413
414/// Preferred nonlinear solver method for a component.
415///
416/// Components that support explicit (closed-form) solvers return
417/// `SolverMethod::WrightOmega` from `solver_hint()`. The compiler build pass
418/// uses this hint to select the appropriate root type.
419///
420/// `NewtonRaphson` is available as an explicit override for debugging or for
421/// circuits where the explicit solver needs to be compared against the iterative
422/// solver.
423#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
424pub enum SolverMethod {
425 /// Newton-Raphson iterative solver (default for all nonlinear devices).
426 #[default]
427 NewtonRaphson,
428 /// Wright Omega explicit solver (closed-form, no outer NR loop).
429 /// Faster than NR for diodes; equivalent audio quality.
430 WrightOmega,
431 /// Simplified Ebers-Moll BJT model (fast alternative to Gummel-Poon).
432 ///
433 /// Omits Early effect, high-injection knee currents, leakage, and
434 /// parasitic resistances. Uses only IS, BF, BR, NF, NR from the model.
435 /// Approximately 3× faster per NR iteration than GummelPoon.
436 ///
437 /// Suitable for clean amplifier stages where GP advanced effects are
438 /// negligible at the operating point.
439 EbersMoll,
440 /// Full Gummel-Poon BJT model (default for BJTs).
441 ///
442 /// Includes Early effect, high-injection, leakage, and parasitic
443 /// resistances. More accurate but more expensive per NR iteration.
444 GummelPoon,
445}
446
447/// Result of applying DC bias to an active component.
448#[derive(Debug, Clone)]
449pub enum BiasResult {
450 /// Component doesn't use bias (passive elements, diodes).
451 NotApplicable,
452 /// Bias applied successfully. Contains the computed model parameters.
453 Applied {
454 /// Positive rail voltage (V above bias point before clipping).
455 v_rail_pos: f64,
456 /// Negative rail voltage (V below bias point before clipping).
457 v_rail_neg: f64,
458 },
459}
460
461// ═══════════════════════════════════════════════════════════════════════════
462// Per-terminal neighbor requirements (boundary-arbitration groundwork)
463// ═══════════════════════════════════════════════════════════════════════════
464
465/// Device-agnostic classification of what a neighbouring passive (or active)
466/// element *does* for an active device's terminal.
467///
468/// This is the durable, role-based replacement for the role-blind topology
469/// heuristic that currently sets stage boundaries. A later "arbitration" phase
470/// will consume these roles to decide where stages split; **this declaration +
471/// inference + completeness pass changes no audio behaviour** — its only
472/// runtime-visible effect is a compile-time completeness diagnostic.
473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
474pub enum NeighborRole {
475 /// Develops the device's output. Terminates at a supply rail (plate /
476 /// collector / drain load; a cathode-follower's cathode load) — and may
477 /// equally be a transformer winding or another active device acting as the
478 /// load. Conceptually **absorbed** into the device's stage.
479 Load,
480 /// DC operating-point / AC-grounded support: grid leak, base divider,
481 /// cathode / emitter bias R, cathode / screen bypass C, screen dropper.
482 /// **Absorbed**.
483 Ref,
484 /// Series through-path to the NEXT active stage: coupling cap, grid stopper.
485 /// This is the future stage **boundary** (and ordering edge).
486 Signal,
487}
488
489impl NeighborRole {
490 /// Human-readable label for diagnostics.
491 pub fn label(self) -> &'static str {
492 match self {
493 NeighborRole::Load => "Load",
494 NeighborRole::Ref => "Ref",
495 NeighborRole::Signal => "Signal",
496 }
497 }
498}
499
500/// How many neighbours of a given [`NeighborRole`] a terminal expects.
501///
502/// A `Vec<NeighborReq>` (rather than an integer count) is used deliberately so
503/// future requirements can be expressed compositionally without an arithmetic
504/// model.
505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
506pub enum Cardinality {
507 /// Must be present — its absence is a completeness error.
508 Required,
509 /// May be present; never an error if absent.
510 Optional,
511 /// Zero or more — informational only, never an error.
512 Repeatable,
513}
514
515/// A single neighbour requirement declared by an active device terminal.
516#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517pub struct NeighborReq {
518 pub role: NeighborRole,
519 pub card: Cardinality,
520}
521
522impl NeighborReq {
523 pub const fn required(role: NeighborRole) -> Self {
524 Self {
525 role,
526 card: Cardinality::Required,
527 }
528 }
529 pub const fn optional(role: NeighborRole) -> Self {
530 Self {
531 role,
532 card: Cardinality::Optional,
533 }
534 }
535 pub const fn repeatable(role: NeighborRole) -> Self {
536 Self {
537 role,
538 card: Cardinality::Repeatable,
539 }
540 }
541}
542
543// ═══════════════════════════════════════════════════════════════════════════
544// Component trait
545// ═══════════════════════════════════════════════════════════════════════════
546
547/// The single source of truth for circuit component behavior.
548///
549/// Every circuit element in PedalKernel — resistors, capacitors, op-amps,
550/// BJTs, vacuum tubes, diodes, potentiometers, transformers, BBD delay lines —
551/// implements this trait. The compilation pipeline queries these methods to
552/// determine topology, solver strategy, stage boundaries, and runtime behavior
553/// without ever pattern-matching on concrete component types.
554///
555/// # Trait Object Support
556///
557/// `Component` is used as `Box<dyn Component>` throughout the pipeline. It
558/// requires [`Debug`] for trait-object formatting and provides `clone_box`,
559/// `as_any`, and `dyn_eq` for trait-object [`Clone`], downcasting, and
560/// [`PartialEq`] support.
561///
562/// # Method Groups
563///
564/// The trait methods are organized into functional groups:
565///
566/// ## Identity
567/// - [`type_tag`](Component::type_tag) — human-readable name (e.g., `"resistor"`, `"NPN transistor"`)
568///
569/// ## Ports
570/// - [`ports`](Component::ports) — terminal pairs carrying current. A resistor has 1 port `(a, b)`;
571/// a BJT has 2 ports `(base-emitter, collector-emitter)`.
572///
573/// ## Signal Flow
574/// - [`signal_terminals`](Component::signal_terminals) — directionality for feedback analysis.
575/// Amplifiers declare input/output pins; passive components are bidirectional.
576/// The compiler uses Tarjan's SCC algorithm on the directed signal graph to
577/// detect feedback loops.
578/// - [`output_impedance`](Component::output_impedance) — determines stage split points.
579/// [`OutputImpedance::VoltageSource`] (op-amps in feedback) allows safe splitting;
580/// [`OutputImpedance::Finite`] (passives, BJT collectors) prevents it.
581/// Based on Harold Black's negative feedback theorem (1934).
582///
583/// ## Non-Idealities
584/// - [`nonideal_fx`](Component::nonideal_fx) — declares physical imperfections as
585/// [`NonIdealFx`] variants. Values come from SPICE models and datasheets. The
586/// stage builder applies these as composable post-processing filters after the
587/// ideal WDF/MNA solver.
588///
589/// ## Classification
590/// - [`is_passive`](Component::is_passive) — whether BFS can collect this component into a stage
591/// - [`is_nonlinear`](Component::is_nonlinear) — whether this needs a Newton-Raphson solver
592/// - [`is_active_ic`](Component::is_active_ic) — whether this counts toward `num_active`
593/// - [`is_variable`](Component::is_variable) — whether the edge impedance changes at runtime
594/// (triggers scattering matrix recomputation)
595/// - Family booleans: [`is_bjt`](Component::is_bjt), [`is_jfet`](Component::is_jfet),
596/// [`is_tube`](Component::is_tube), [`is_pot`](Component::is_pot), etc.
597///
598/// ## Graph Building
599/// - [`graph_role`](Component::graph_role) — how this component participates in the circuit graph.
600/// [`GraphRole::Edge`] for passives, [`GraphRole::VcvsEdge`] for op-amps,
601/// [`GraphRole::Virtual`] for LFOs, [`GraphRole::Pot`] for potentiometers.
602/// - [`edges`](Component::edges) — declares circuit edges with [`EdgeKind`] classification.
603/// [`EdgeKind::Linear`] and [`EdgeKind::Reactive`] form passive WDF trees;
604/// [`EdgeKind::Nonlinear`] seeds NR solver stages; [`EdgeKind::Vcvs`] forces R-node MNA.
605/// - [`resolve_edges`](Component::resolve_edges) — context-dependent edge resolution. A JFET
606/// with its gate driven by an LFO resolves from `Nonlinear` to `Linear` (variable resistor).
607///
608/// ## MNA Stamping
609/// - [`stamp_mna`](Component::stamp_mna) — stamp into a 2-terminal MNA system (resistors, caps)
610/// - [`stamp_mna_multi`](Component::stamp_mna_multi) — stamp with multi-terminal pin resolution
611/// (op-amps resolve pos/neg/out pins)
612/// - [`mna_vsource_count`](Component::mna_vsource_count) — voltage sources needed (op-amps: 1)
613/// - [`mna_internal_node_count`](Component::mna_internal_node_count) — internal MNA nodes (op-amps: 1 for GBW pole)
614///
615/// ## WDF Leaf Creation
616/// - [`make_leaf`](Component::make_leaf) — create a runtime WDF leaf node (`DynNode`).
617/// Resistors, capacitors, and inductors return leaf nodes; nonlinear and virtual
618/// components return `None`.
619///
620/// ## Pin Interface
621/// - [`pin_config`](Component::pin_config) — valid pin names and aliases
622/// - [`pin_direction`](Component::pin_direction) — inferred direction for layout (Input, Output, Up, Down)
623///
624/// ## Controls
625/// - [`controls`](Component::controls) — declares runtime-adjustable parameters ([`ControlParam`]).
626/// Pots declare `PotPosition`; LFOs declare `LfoRate` and `LfoDepth`.
627/// - [`modulation_sink`](Component::modulation_sink) — how this component receives LFO/envelope
628/// control signals, including bias voltage and modulation range.
629///
630/// ## Validation
631/// - [`validate_values`](Component::validate_values) — design rule checks for suspicious or
632/// invalid component values (e.g., a 1-ohm resistor, a 1-farad capacitor).
633///
634/// # Implementing a New Component
635///
636/// To add a new component type to PedalKernel:
637///
638/// 1. Create a struct in [`super::components`] (e.g., `MyDevice { model: String }`)
639/// 2. Implement `Component` with at minimum:
640/// - `type_tag()` — return a human-readable name
641/// - `is_passive()` — `true` for R/C/L, `false` for active devices
642/// - `pin_config()` — declare valid pin names
643/// - `graph_role()` — how it enters the circuit graph
644/// - `edges()` — declare edge kinds (Linear, Nonlinear, etc.)
645/// - `stamp_mna()` — stamp into MNA matrices (for R-node solving)
646/// - `footprint_ref()` — KiCad symbol reference
647/// 3. For nonlinear devices, also implement `classify_nonlinear()` and set
648/// `is_nonlinear() -> true`
649/// 4. For active devices with feedback, implement `output_impedance()` and
650/// `signal_terminals()`
651/// 5. Add the DSL parser variant in [`crate::dsl`]
652///
653/// No changes are needed in the graph builder, SPQR decomposer, stage builder,
654/// or any other pipeline module.
655pub trait Component: std::fmt::Debug {
656 // ── Trait Object Support ──────────────────────────────────────────────
657
658 /// Clone into a boxed trait object.
659 fn clone_box(&self) -> Box<dyn Component>;
660
661 /// Downcast to `Any` for type-erased equality checks and downcasting.
662 fn as_any(&self) -> &dyn std::any::Any;
663
664 /// Downcast to `Any` mutably for in-place mutation (e.g., tolerance tweaks).
665 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
666
667 /// Dynamic equality: returns `true` if `other` is the same concrete type
668 /// and compares equal.
669 fn dyn_eq(&self, other: &dyn Component) -> bool;
670
671 // ── Identity ──────────────────────────────────────────────────────────
672
673 /// Human-readable type name (e.g. "resistor", "NPN transistor").
674 fn type_tag(&self) -> &'static str;
675
676 // ── Solver Hint ───────────────────────────────────────────────────────
677
678 /// Return the preferred nonlinear solver method for this component.
679 ///
680 /// Returns `None` to accept the global default (Newton-Raphson).
681 /// Returns `Some(SolverMethod::WrightOmega)` to request the explicit
682 /// closed-form Wright Omega solver (diodes only).
683 ///
684 /// The build pass uses this to choose between `DiodePairRoot`/`DiodeRoot`
685 /// (NR) and `ExplicitDiodePairRoot`/`ExplicitDiodeRoot` (WO).
686 fn solver_hint(&self) -> Option<SolverMethod> {
687 None
688 }
689
690 // ── Ports ─────────────────────────────────────────────────────────────
691
692 /// Terminal pairs carrying current through this component.
693 ///
694 /// Each port = one graph edge. A resistor has 1 port (a, b).
695 /// A BJT has 2 ports (base-emitter, collector-emitter).
696 /// A pot has 2 ports (a-wiper, wiper-b).
697 /// An op-amp has 1 port (neg-out, pos is voltage-sense).
698 fn ports(&self) -> Vec<(&'static str, &'static str)> {
699 vec![("a", "b")] // default: 1-port component
700 }
701
702 // ── Non-Idealities ────────────────────────────────────────────────────
703
704 /// Non-ideal behaviors for this component (GBW, slew, rails, thermal, etc.).
705 ///
706 /// Each component declares what non-idealities it has. Values come from
707 /// the SPICE model / datasheet lookup. The stage builder attaches them
708 /// as post-processing. No pattern matching on component type.
709 ///
710 /// Default: empty vec (ideal component).
711 fn nonideal_fx(&self, _sample_rate: f64) -> Vec<NonIdealFx> {
712 Vec::new()
713 }
714
715 // ── Signal Flow ──────────────────────────────────────────────────────
716
717 /// Output impedance at this component's output pin.
718 ///
719 /// `VoltageSource` means the output node voltage is determined entirely
720 /// by the component (via feedback), independent of downstream load.
721 /// The compiler can safely split the circuit at these nodes.
722 ///
723 /// `Finite` (default) means splitting here would change impedances
724 /// and break the transfer function.
725 fn output_impedance(&self) -> OutputImpedance {
726 OutputImpedance::Finite
727 }
728
729 /// Signal flow classification for this component's pins.
730 ///
731 /// Drives feedback analysis: amplifier input→output defines the
732 /// feedback cycle direction. Passive components have no directionality.
733 fn signal_terminals(&self) -> SignalTerminals {
734 SignalTerminals::Passive
735 }
736
737 // ── Port Semantics ────────────────────────────────────────────────────
738
739 /// Classify the electrical behavior of a port (pin pair) for optimization
740 /// legality checks. The compiler uses this to determine which subgraphs
741 /// can be safely lowered to IIR/BlackFeedback vs requiring full MNA/WDF.
742 ///
743 /// Default derives from existing classification methods. Override for
744 /// multi-port devices where different pin pairs have different semantics
745 /// (e.g., BJT: B-E is Nonlinear, C-E is Nonlinear).
746 fn port_semantic(&self, _pin_a: &str, _pin_b: &str) -> PortSemantic {
747 if self.is_nonlinear() {
748 return PortSemantic::Nonlinear;
749 }
750 if self.is_variable() {
751 return PortSemantic::ControlledConductance;
752 }
753 if self.capacitance().is_some() || self.inductance().is_some() {
754 return PortSemantic::Reactive;
755 }
756 PortSemantic::LinearPassive
757 }
758
759 // ── Classification ────────────────────────────────────────────────────
760
761 /// Whether this component is passive (collectible by BFS for stage building).
762 fn is_passive(&self) -> bool;
763
764 /// Whether this component is a nonlinear element (needs NR solver).
765 fn is_nonlinear(&self) -> bool {
766 false
767 }
768
769 /// Whether this component is an active IC (counted toward `num_active`).
770 fn is_active_ic(&self) -> bool {
771 false
772 }
773
774 /// Whether this component is a control-only element (no circuit pins).
775 fn is_control_only(&self) -> bool {
776 false
777 }
778
779 /// Whether this component's edge impedance changes at runtime.
780 /// Stages containing variable edges need scattering matrix recomputation.
781 fn is_variable(&self) -> bool {
782 false
783 }
784
785 // ── Pin Interface ─────────────────────────────────────────────────────
786
787 /// Valid pins and aliases for this component type.
788 fn pin_config(&self) -> PinConfig;
789
790 /// Extra pins that are valid as modulation targets but may not be in `pin_config`.
791 fn modulation_pins(&self) -> &'static [&'static str] {
792 &[]
793 }
794
795 // ── Graph Building ────────────────────────────────────────────────────
796
797 /// How this component participates in circuit graph construction.
798 fn graph_role(&self) -> GraphRole;
799
800 /// Declare this component's circuit edges with their electrical classification.
801 ///
802 /// Returns empty for Virtual/ActiveIc components (no WDF topology participation).
803 /// The planner uses these edges to group components into stages by EdgeKind.
804 fn edges(&self) -> Vec<ComponentEdge> {
805 vec![]
806 }
807
808 /// Internal signal-path adjacencies: pin pairs that signal can traverse through.
809 ///
810 /// Used by the validator to check in→out reachability. Default derives pairs
811 /// from `edges()`; if edges is empty, fans out from first valid_pin to all others.
812 /// Override for multi-terminal or complex components (BJTs, pots, transformers).
813 fn signal_adjacencies(&self) -> Vec<(&'static str, &'static str)> {
814 let edge_list = self.edges();
815 if !edge_list.is_empty() {
816 edge_list.iter().map(|e| (e.pin_a, e.pin_b)).collect()
817 } else {
818 let pins = self.pin_config().valid_pins;
819 if pins.len() >= 2 {
820 pins[1..].iter().map(|&p| (pins[0], p)).collect()
821 } else {
822 vec![]
823 }
824 }
825 }
826
827 /// Infer the signal direction for a given pin on this component.
828 ///
829 /// Used by the layout engine to determine directed graph edges.
830 /// Default: `Bidirectional` for all pins.
831 fn pin_direction(&self, _pin: &str) -> PinDirection {
832 PinDirection::Bidirectional
833 }
834
835 /// Context-dependent resolution: if this component's behavior depends on
836 /// how it's wired, return a new edge list reflecting the resolved role.
837 ///
838 /// Called after graph construction with neighbor connectivity info.
839 /// Default: no resolution needed (return None to keep current edges).
840 ///
841 /// Examples:
842 /// - JFET gate → LFO/pot → resolve drain-source as Linear (variable resistor)
843 /// - OTA Iabc → envelope → resolve as Vccs (linear mode)
844 fn resolve_edges(&self, _neighbors: &ResolveContext) -> Option<Vec<ComponentEdge>> {
845 None
846 }
847
848 // ── MNA Stamping ──────────────────────────────────────────────────────
849
850 /// Stamp into MNA system. Returns what was produced.
851 ///
852 /// `comp_id` is the component's string identifier (needed for `DynNode::Pot`).
853 /// `n1`/`n2` are MNA node indices (`None` = ground).
854 /// Transformers return `Skip` — they need coupled-edge context from the caller.
855 fn stamp_mna(
856 &self,
857 comp_id: &str,
858 n1: Option<usize>,
859 n2: Option<usize>,
860 mna: &mut MnaSystem,
861 sample_rate: f64,
862 ) -> StampResult;
863
864 /// Number of MNA voltage sources this component needs when stamped
865 /// into an R-node. Default: 0. Op-amps return 1 (for the VCVS constraint).
866 fn mna_vsource_count(&self) -> usize {
867 0
868 }
869
870 /// Number of internal MNA nodes this component needs.
871 /// Default: 0. Op-amps return 1 (internal gain stage node for GBW pole).
872 fn mna_internal_node_count(&self) -> usize {
873 0
874 }
875
876 /// Stamp this component into an MNA system using multi-terminal pin resolution.
877 ///
878 /// Default implementation resolves pins "a" and "b" and delegates to
879 /// `stamp_mna()`. Multi-terminal components (op-amps) override this to
880 /// resolve all their pins and stamp directly.
881 fn stamp_mna_multi(
882 &self,
883 comp_id: &str,
884 ctx: &mut StampContext,
885 mna: &mut MnaSystem,
886 ) -> StampResult {
887 let n1 = (ctx.pin_to_mna)("a");
888 let n2 = (ctx.pin_to_mna)("b");
889 self.stamp_mna(comp_id, n1, n2, mna, ctx.sample_rate)
890 }
891
892 // ── WDF Leaf Creation ─────────────────────────────────────────────────
893
894 /// Create a WDF leaf node for this component.
895 /// Returns `None` for non-leaf components (diodes, virtual elements, etc.).
896 fn make_leaf(&self, _comp_id: &str, _sample_rate: f64) -> Option<DynNode> {
897 None
898 }
899
900 // ── Value Access ──────────────────────────────────────────────────────
901
902 fn resistance(&self) -> Option<f64> {
903 None
904 }
905 fn capacitance(&self) -> Option<f64> {
906 None
907 }
908 fn inductance(&self) -> Option<f64> {
909 None
910 }
911
912 // ── Validation ────────────────────────────────────────────────────────
913
914 /// Check for suspicious or invalid component values.
915 fn validate_values(&self, _comp_id: &str) -> Vec<(Severity, String)> {
916 vec![]
917 }
918
919 // ── Neighbor requirements (boundary-arbitration groundwork) ───────────
920
921 /// Per-terminal neighbour requirements for active devices.
922 ///
923 /// Returns `(terminal_name, requirements)` pairs declaring what each
924 /// terminal needs from its connectivity to function as a circuit element.
925 /// The completeness pass classifies each terminal's actual neighbours
926 /// ([`NeighborRole`]) and reports a compile error when a [`Cardinality::
927 /// Required`] role is missing.
928 ///
929 /// **Config tolerance:** a [`NeighborRole::Load`] declared on more than one
930 /// terminal of the same device (e.g. both `plate` and `cathode` of a triode)
931 /// is satisfied if *any* of those terminals carries a Load. This lets a
932 /// common-cathode stage (plate-load) AND a cathode-follower (cathode-load,
933 /// plate straight to B+) both validate without a false positive.
934 ///
935 /// Default: empty (passive / non-active components declare nothing and are
936 /// never checked).
937 fn terminal_requirements(&self) -> Vec<(&'static str, Vec<NeighborReq>)> {
938 Vec::new()
939 }
940
941 // ── Classification (detailed) ────────────────────────────────────────
942
943 /// Classify this component as a nonlinear element for the circuit solver.
944 ///
945 /// Returns `Some((kind, junction_nodes))` if this is a nonlinear element,
946 /// where `junction_nodes` are the circuit nodes where passive elements connect.
947 /// Returns `None` for passive, virtual, and active-IC components.
948 ///
949 /// - 1 junction node: diodes, JFETs, MOSFETs, zeners, OTAs
950 /// - 2 junction nodes: BJTs (collector, emitter), triodes/pentodes (plate, cathode)
951 fn classify_nonlinear(
952 &self,
953 _comp_id: &str,
954 _node_a: NodeId,
955 _node_b: NodeId,
956 _gnd_node: NodeId,
957 _node_names: &HashMap<String, NodeId>,
958 ) -> Option<(NonlinearKind, Vec<NodeId>)> {
959 None
960 }
961
962 // ── Control Declarations ────────────────────────────────────────────
963
964 /// Declare controllable parameters for this component.
965 ///
966 /// The compiler matches `ControlDef.property` against each param's `name`
967 /// to determine the control target without heuristic net scanning.
968 fn controls(&self) -> Vec<ControlParam> {
969 vec![]
970 }
971
972 /// If this component can be a modulation sink (LFO/envelope target),
973 /// return the sink description for the given pin.
974 ///
975 /// Called during LFO/envelope binding to determine bias and range.
976 fn modulation_sink(&self, _pin: &str) -> Option<ModulationSink> {
977 None
978 }
979
980 // ── Hardware ──────────────────────────────────────────────────────────
981
982 /// KiCad symbol library reference and designator prefix.
983 fn footprint_ref(&self) -> (&'static str, &'static str);
984
985 // ── Classification (family booleans) ─────────────────────────────────
986 // Default: false. Override in concrete structs that belong to each family.
987
988 fn is_modulation_source(&self) -> bool {
989 false
990 }
991 fn is_bjt(&self) -> bool {
992 false
993 }
994 fn is_jfet(&self) -> bool {
995 false
996 }
997 fn is_mosfet(&self) -> bool {
998 false
999 }
1000 fn is_tube(&self) -> bool {
1001 false
1002 }
1003 fn is_transformer(&self) -> bool {
1004 false
1005 }
1006 fn is_pot(&self) -> bool {
1007 false
1008 }
1009 fn is_diode_family(&self) -> bool {
1010 false
1011 }
1012 fn is_trigger(&self) -> bool {
1013 false
1014 }
1015
1016 /// Whether this element's input node acts as a summing junction that
1017 /// should block BFS traversal in the signal flow graph.
1018 ///
1019 /// Op-amp neg nodes are summing junctions: the input signal, feedback
1020 /// network, and interstage coupling all connect to the same node.
1021 /// Without blocking, BFS from a downstream element can traverse backward
1022 /// through this element's feedback, across shared passives, and reach
1023 /// upstream elements — creating false cycle edges.
1024 ///
1025 /// Returns true for op-amps (VCVS). Returns false for diodes, BJTs,
1026 /// tubes, etc. — their input nodes don't create false coupling paths.
1027 fn feedback_input_is_barrier(&self) -> bool {
1028 false
1029 }
1030
1031 // ── K-method candidacy ────────────────────────────────────────────────
1032
1033 /// K-method table shape declared by the component.
1034 ///
1035 /// Requirements for candidacy:
1036 /// 1. Memoryless: the NL is purely algebraic (no internal state/caps)
1037 /// 2. Low port count: ≤3 dimensions for practical table sizes
1038 /// 3. Monotonic: unique output for any input (no fold-back/hysteresis)
1039 ///
1040 /// Axis dimensions:
1041 /// - 1D: incident wave only, e.g. diode/zener junction
1042 /// - 2D: incident wave + device control, e.g. BJT Vbe or triode Vgk
1043 /// - 3D: incident wave + two controls, e.g. pentode grids
1044 ///
1045 /// Linear components return (false, 0, "linear") — they don't need
1046 /// NL tabulation. Variable components (pots, photocouplers) return
1047 /// false because their parameters change at runtime.
1048 fn k_method_spec(&self) -> Option<KMethodSpec> {
1049 if !self.is_nonlinear() {
1050 return None;
1051 }
1052 // Default for unknown NL: reject conservatively.
1053 None
1054 }
1055
1056 /// Compatibility wrapper for callers that still only need yes/no +
1057 /// dimensionality. New code should prefer [`Component::k_method_spec`]
1058 /// because it preserves the meaning of each table axis.
1059 fn k_method_candidacy(&self) -> (bool, usize, &'static str) {
1060 if let Some(spec) = self.k_method_spec() {
1061 (true, spec.dimensions(), spec.reason)
1062 } else if self.is_nonlinear() {
1063 (
1064 false,
1065 0,
1066 "unknown nonlinear type — override k_method_spec()",
1067 )
1068 } else {
1069 (false, 0, "linear — not a nonlinear element")
1070 }
1071 }
1072
1073 // ── Composite classification ─────────────────────────────────────────
1074
1075 /// Passive two-terminal element (R, C, L, etc.) — excludes transformers and pots.
1076 fn is_simple_passive(&self) -> bool {
1077 self.is_passive() && !self.is_transformer()
1078 }
1079
1080 /// Amplifying device: tube, BJT, JFET, MOSFET.
1081 fn is_gain_device(&self) -> bool {
1082 false
1083 }
1084
1085 // ── Accessor methods (defaults return None) ──────────────────────────
1086
1087 fn model_name(&self) -> Option<&str> {
1088 None
1089 }
1090 fn op_amp_type(&self) -> Option<crate::dsl::OpAmpType> {
1091 None
1092 }
1093 fn pot_taper(&self) -> Option<crate::dsl::PotTaper> {
1094 None
1095 }
1096 fn diode_type(&self) -> Option<crate::dsl::DiodeType> {
1097 None
1098 }
1099 fn transformer_config(&self) -> Option<&crate::dsl::TransformerConfig> {
1100 None
1101 }
1102
1103 // ── Bias application ────────────────────────────────────────────────
1104
1105 /// Apply DC bias from a static bias network detected at compile time.
1106 ///
1107 /// `bias_voltages` maps pin names (e.g. "pos", "neg", "base", "gate")
1108 /// to their DC voltage computed from the circuit's resistor divider.
1109 /// `supply_voltage` is the pedal's supply rail voltage (e.g. 9.0V).
1110 ///
1111 /// Each component type interprets bias differently:
1112 /// - **Op-amp**: sets positive/negative rail limits from bias point
1113 /// - **BJT**: sets quiescent Ic, Vce from collector/emitter voltages
1114 /// - **Triode/Pentode**: sets grid bias (Vgk) from grid DC voltage
1115 /// - **JFET**: sets Vgs from gate bias voltage
1116 ///
1117 /// Returns `BiasResult` describing what was applied.
1118 /// Default: no-op (passive components don't need bias).
1119 fn apply_bias(
1120 &self,
1121 _bias_voltages: &hashbrown::HashMap<String, f64>,
1122 _supply_voltage: f64,
1123 ) -> BiasResult {
1124 BiasResult::NotApplicable
1125 }
1126
1127 // ── Layout methods (defaults use type_tag) ──────────────────────────
1128
1129 /// Symbol identifier for layout rendering (e.g., "resistor", "triode", "opamp").
1130 fn symbol_name(&self) -> &'static str {
1131 self.type_tag()
1132 }
1133
1134 /// Layout class identifier for placement grouping (e.g., "resistor", "npn", "opamp").
1135 fn layout_class(&self) -> &'static str {
1136 self.type_tag()
1137 }
1138
1139 /// Human-readable display value for labels (e.g., "4.7kΩ", "100nF").
1140 fn display_value(&self) -> Option<String> {
1141 None
1142 }
1143}
1144
1145// ── Trait-object blanket impls ────────────────────────────────────────────
1146
1147impl Clone for Box<dyn Component> {
1148 fn clone(&self) -> Self {
1149 self.clone_box()
1150 }
1151}
1152
1153impl PartialEq for Box<dyn Component> {
1154 fn eq(&self, other: &Self) -> bool {
1155 self.dyn_eq(other.as_ref())
1156 }
1157}
1158
1159// ═══════════════════════════════════════════════════════════════════════════
1160// Tests
1161// ═══════════════════════════════════════════════════════════════════════════
1162
1163#[cfg(test)]
1164mod tests {
1165 use super::*;
1166 use crate::compiler::components::*;
1167 use crate::dsl::CapConfig;
1168
1169 #[test]
1170 fn box_dyn_component_clone() {
1171 let r: Box<dyn Component> = Box::new(Resistor { value: 1000.0 });
1172 let r2 = r.clone();
1173 assert_eq!(r2.type_tag(), "resistor");
1174 assert_eq!(r2.resistance(), Some(1000.0));
1175 }
1176
1177 #[test]
1178 fn box_dyn_component_eq() {
1179 let r1: Box<dyn Component> = Box::new(Resistor { value: 1000.0 });
1180 let r2: Box<dyn Component> = Box::new(Resistor { value: 1000.0 });
1181 let r3: Box<dyn Component> = Box::new(Resistor { value: 2200.0 });
1182 let c1: Box<dyn Component> = Box::new(Capacitor {
1183 config: CapConfig::new(100e-9),
1184 });
1185 assert!(r1.dyn_eq(r2.as_ref()));
1186 assert!(!r1.dyn_eq(r3.as_ref()));
1187 assert!(!r1.dyn_eq(c1.as_ref()));
1188 }
1189
1190 #[test]
1191 fn box_dyn_component_debug() {
1192 let r: Box<dyn Component> = Box::new(Resistor { value: 4700.0 });
1193 let dbg = format!("{:?}", r);
1194 assert!(dbg.contains("4700"));
1195 }
1196
1197 #[test]
1198 fn box_dyn_component_downcast() {
1199 let r: Box<dyn Component> = Box::new(Resistor { value: 1000.0 });
1200 let any = r.as_any();
1201 assert!(any.downcast_ref::<Resistor>().is_some());
1202 }
1203
1204 #[test]
1205 fn edges_linear_resistor() {
1206 let r = Resistor { value: 1000.0 };
1207 let edges = r.edges();
1208 assert_eq!(edges.len(), 1);
1209 assert_eq!(edges[0].kind, EdgeKind::Linear);
1210 assert_eq!(edges[0].pin_a, "a");
1211 assert_eq!(edges[0].pin_b, "b");
1212 }
1213
1214 #[test]
1215 fn edges_reactive_capacitor() {
1216 let c = Capacitor {
1217 config: CapConfig::new(100e-9),
1218 };
1219 let edges = c.edges();
1220 assert_eq!(edges.len(), 1);
1221 assert_eq!(edges[0].kind, EdgeKind::Reactive);
1222 }
1223
1224 #[test]
1225 fn edges_nonlinear_diode() {
1226 let d = Diode {
1227 diode_type: crate::dsl::DiodeType::Silicon,
1228 };
1229 let edges = d.edges();
1230 assert_eq!(edges.len(), 1);
1231 assert_eq!(edges[0].kind, EdgeKind::Nonlinear);
1232 }
1233
1234 #[test]
1235 fn edges_nonlinear_bjt() {
1236 let q = Npn {
1237 model: "2N3904".into(),
1238 };
1239 let edges = q.edges();
1240 assert_eq!(edges.len(), 1);
1241 assert_eq!(edges[0].kind, EdgeKind::Nonlinear);
1242 assert_eq!(edges[0].pin_a, "collector");
1243 assert_eq!(edges[0].pin_b, "emitter");
1244 }
1245
1246 #[test]
1247 fn edges_nonlinear_triode() {
1248 let v = Triode {
1249 model: "12AX7".into(),
1250 };
1251 let edges = v.edges();
1252 assert_eq!(edges.len(), 1);
1253 assert_eq!(edges[0].kind, EdgeKind::Nonlinear);
1254 assert_eq!(edges[0].pin_a, "plate");
1255 }
1256
1257 #[test]
1258 fn edges_behavioral_bbd() {
1259 let b = Bbd {
1260 bbd_type: crate::dsl::BbdType::Mn3207,
1261 };
1262 let edges = b.edges();
1263 assert_eq!(edges.len(), 1);
1264 assert_eq!(edges[0].kind, EdgeKind::Behavioral);
1265 }
1266
1267 #[test]
1268 fn edges_virtual_lfo_empty() {
1269 let l = Lfo {
1270 waveform: crate::dsl::LfoWaveformDsl::Triangle,
1271 timing_r: 100_000.0,
1272 timing_c: 1e-6,
1273 };
1274 assert!(l.edges().is_empty());
1275 }
1276
1277 #[test]
1278 fn edges_active_ic_opamp_vcvs() {
1279 let o = OpAmp {
1280 op_type: crate::dsl::OpAmpType::Tl072,
1281 };
1282 let edges = o.edges();
1283 assert_eq!(edges.len(), 1);
1284 assert_eq!(edges[0].kind, EdgeKind::Vcvs);
1285 assert_eq!(edges[0].pin_a, "neg");
1286 assert_eq!(edges[0].pin_b, "out");
1287 }
1288
1289 #[test]
1290 fn edges_ota_nonlinear() {
1291 let ota = OpAmp {
1292 op_type: crate::dsl::OpAmpType::Ca3080,
1293 };
1294 let edges = ota.edges();
1295 assert_eq!(edges.len(), 1);
1296 assert_eq!(edges[0].kind, EdgeKind::Nonlinear);
1297 assert_eq!(edges[0].pin_a, "pos");
1298 }
1299
1300 #[test]
1301 fn resolve_jfet_to_linear_when_modulated() {
1302 let j = NJfet {
1303 model: "2N5457".into(),
1304 };
1305 // Default: nonlinear
1306 assert_eq!(j.edges()[0].kind, EdgeKind::Nonlinear);
1307 // Modulated gate: resolves to linear (variable resistor)
1308 let ctx = ResolveContext {
1309 control_pin_is_modulated: true,
1310 wiper_connected: false,
1311 };
1312 let resolved = j.resolve_edges(&ctx).unwrap();
1313 assert_eq!(resolved.len(), 1);
1314 assert_eq!(resolved[0].kind, EdgeKind::Linear);
1315 assert_eq!(resolved[0].pin_a, "drain");
1316 }
1317
1318 #[test]
1319 fn resolve_jfet_no_change_without_modulation() {
1320 let j = PJfet {
1321 model: "2N5460".into(),
1322 };
1323 let ctx = ResolveContext {
1324 control_pin_is_modulated: false,
1325 wiper_connected: false,
1326 };
1327 assert!(j.resolve_edges(&ctx).is_none());
1328 }
1329
1330 #[test]
1331 fn resolve_ota_to_vccs_when_modulated() {
1332 let ota = OpAmp {
1333 op_type: crate::dsl::OpAmpType::Ca3080,
1334 };
1335 // CA3080 is linearized as a VCCS in the circuit graph.
1336 assert_eq!(ota.edges()[0].kind, EdgeKind::Vccs);
1337 // Modulated Iabc: resolves to VCCS
1338 let ctx = ResolveContext {
1339 control_pin_is_modulated: true,
1340 wiper_connected: false,
1341 };
1342 let resolved = ota.resolve_edges(&ctx).unwrap();
1343 assert_eq!(resolved.len(), 1);
1344 assert_eq!(resolved[0].kind, EdgeKind::Vccs);
1345 }
1346
1347 #[test]
1348 fn resolve_regular_opamp_unchanged() {
1349 let op = OpAmp {
1350 op_type: crate::dsl::OpAmpType::Tl072,
1351 };
1352 let ctx = ResolveContext {
1353 control_pin_is_modulated: true,
1354 wiper_connected: false,
1355 };
1356 // Regular opamps don't resolve (no modulation pins)
1357 assert!(op.resolve_edges(&ctx).is_none());
1358 }
1359}