Skip to main content

pedalkernel/compiler/components/
tape_head.rs

1//! Voltage-driven Jiles-Atherton tape head component.
2//!
3//! A record/playback head modelled as a WDF one-port whose magnetic field is
4//! driven by PORT VOLTAGE (`H = kv*V`), so tape saturation tracks signal level
5//! at line drive — unlike the transformer core, which is Ampere/current-driven
6//! and only saturates at hundreds of mA of magnetizing current.
7//!
8//! Wiring: a simple 2-terminal `(a, b)` element that joins the surrounding
9//! passive/reactive WDF tree as a leaf. Its `make_leaf` produces
10//! `DynNode::TapeHeadVoltage`, whose per-sample Newton solves the rate-
11//! independent J-A field-domain trajectory and the port KCL (see
12//! `pedalkernel-rt/.../jiles_atherton.rs::TapeHeadVoltageRoot`).
13
14use crate::compiler::component::{
15    Component, ComponentEdge, EdgeKind, GraphRole, PinConfig, PinDirection, StampResult,
16};
17use crate::compiler::dyn_node::DynNode;
18use crate::dsl::TapeHeadConfig;
19use crate::tree::MnaSystem;
20use pedalkernel_rt::elements::JaCoreModel;
21
22use super::impl_component_dyn;
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct TapeHeadComp {
26    pub config: TapeHeadConfig,
27}
28
29impl TapeHeadComp {
30    fn ja_model(&self) -> JaCoreModel {
31        let c = &self.config;
32        JaCoreModel {
33            ms: c.ja_ms as crate::Wave,
34            a: c.ja_a as crate::Wave,
35            alpha: c.ja_alpha as crate::Wave,
36            k: c.ja_k as crate::Wave,
37            c: c.ja_c as crate::Wave,
38            // The voltage law uses kv (H = kv*V), not Ampere's law, so the
39            // geometry only needs to be sane positives for `is_complete()`.
40            n_turns: 100.0,
41            area: 1.0e-6,
42            path_len: 1.0e-3,
43            gap: 0.0,
44        }
45    }
46}
47
48impl Component for TapeHeadComp {
49    impl_component_dyn!();
50
51    fn type_tag(&self) -> &'static str {
52        "tape_head"
53    }
54
55    // Passive in the BFS sense: it is a self-contained WDF leaf (its own root
56    // solver), so it collects into the surrounding passive/reactive WDF tree
57    // exactly like the transformer core's J-A magnetizing leaf.
58    fn is_passive(&self) -> bool {
59        true
60    }
61
62    fn pin_config(&self) -> PinConfig {
63        PinConfig {
64            valid_pins: &["a", "b"],
65            aliases: &[],
66        }
67    }
68
69    fn graph_role(&self) -> GraphRole {
70        GraphRole::Edge {
71            pin_a: "a",
72            pin_b: "b",
73        }
74    }
75
76    fn edges(&self) -> Vec<ComponentEdge> {
77        // Reactive: carries magnetic state between samples, so it must be a WDF
78        // port (same classification the transformer winding uses).
79        vec![ComponentEdge {
80            pin_a: "a",
81            pin_b: "b",
82            kind: EdgeKind::Reactive,
83            port_group: None,
84        }]
85    }
86
87    fn stamp_mna(
88        &self,
89        _comp_id: &str,
90        _n1: Option<usize>,
91        _n2: Option<usize>,
92        _mna: &mut MnaSystem,
93        _sample_rate: f64,
94    ) -> StampResult {
95        StampResult::Skip
96    }
97
98    fn make_leaf(&self, comp_id: &str, sample_rate: f64) -> Option<DynNode> {
99        let c = &self.config;
100        Some(DynNode::TapeHeadVoltage(
101            Some(comp_id.to_string()),
102            self.ja_model(),
103            c.kv as crate::Wave,
104            c.isat as crate::Wave,
105            c.gp as crate::Wave,
106            c.h_bias as crate::Wave,
107            sample_rate as crate::Wave,
108            c.rp as crate::Wave,
109        ))
110    }
111
112    /// Report a representative inductance so the stage builder treats this as a
113    /// reactive WDF port (the actual port resistance comes from the leaf's own
114    /// `port_resistance()` — this value is only the reactive-port marker; see
115    /// `spqr_build.rs` where `inductance().is_some()` selects the WDF-port path).
116    fn inductance(&self) -> Option<f64> {
117        Some(self.config.rp / (2.0 * 48_000.0))
118    }
119
120    fn pin_direction(&self, pin: &str) -> PinDirection {
121        match pin {
122            "a" => PinDirection::Input,
123            "b" => PinDirection::Output,
124            _ => PinDirection::Bidirectional,
125        }
126    }
127
128    fn footprint_ref(&self) -> (&'static str, &'static str) {
129        ("Device:L", "TH")
130    }
131
132    fn symbol_name(&self) -> &'static str {
133        "inductor"
134    }
135    fn layout_class(&self) -> &'static str {
136        "tape_head"
137    }
138}