Skip to main content

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}