1use crate::integ::TerrCtx;
24use crate::matrix::{AcCtx, InitMode, LoadCtx, MatrixBuilder, NoiseCtx, PzCtx, Slot, TRASH, Var};
25
26pub(crate) const SW_NUM_STATES: usize = 2;
29const SWITCHSTATE: usize = 0;
30const CTRLVALUE: usize = 1;
31
32const REALLY_OFF: f64 = 0.0;
35const REALLY_ON: f64 = 1.0;
36const HYST_OFF: f64 = 2.0;
37const HYST_ON: f64 = 3.0;
38
39#[derive(Clone, Debug, Default)]
42pub struct SwModel {
43 pub vt: f64,
45 pub vh: f64,
48 pub ron: Option<f64>,
50 pub roff: Option<f64>,
53}
54
55#[derive(Clone, Debug, Default)]
58pub struct CswModel {
59 pub it: f64,
61 pub ih: f64,
64 pub ron: Option<f64>,
66 pub roff: Option<f64>,
68}
69
70#[derive(Clone, Copy, Debug)]
72pub(crate) enum Control {
73 Voltage { pos: Var, neg: Var },
75 Current { branch: Var },
77}
78
79#[derive(Clone, Copy, Debug)]
82pub(crate) struct SwitchParams {
83 pub thresh: f64,
84 pub hyst: f64,
85 pub gon: f64,
86 pub goff: f64,
87}
88
89impl SwitchParams {
90 pub fn new(thresh: f64, hyst: f64, ron: Option<f64>, roff: Option<f64>, gmin: f64) -> Self {
92 SwitchParams {
93 thresh,
94 hyst,
95 gon: ron.map_or(1.0, |r| 1.0 / r),
96 goff: roff.map_or(gmin, |r| 1.0 / r),
97 }
98 }
99}
100
101pub(crate) struct SwitchInst {
103 p: SwitchParams,
104 pos: Var,
105 neg: Var,
106 ctrl: Control,
107 on: bool,
109 st: usize,
110 slots: [Slot; 4],
111}
112
113fn init_state(p: &SwitchParams, on: bool, c: f64) -> f64 {
118 if on {
119 if (p.hyst >= 0.0 && c > p.thresh + p.hyst) || (p.hyst < 0.0 && c > p.thresh - p.hyst) {
120 REALLY_ON
121 } else {
122 HYST_ON
123 }
124 } else if (p.hyst >= 0.0 && c < p.thresh - p.hyst) || (p.hyst < 0.0 && c < p.thresh + p.hyst) {
125 REALLY_OFF
126 } else {
127 HYST_OFF
128 }
129}
130
131fn float_state(p: &SwitchParams, c: f64, keep: f64, prev: f64) -> f64 {
135 if p.hyst > 0.0 {
136 if c > p.thresh + p.hyst {
137 REALLY_ON
138 } else if c < p.thresh - p.hyst {
139 REALLY_OFF
140 } else {
141 keep
142 }
143 } else if c > p.thresh - p.hyst {
144 REALLY_ON
145 } else if c < p.thresh + p.hyst {
146 REALLY_OFF
147 } else if prev == HYST_OFF || prev == HYST_ON {
148 prev
149 } else if prev == REALLY_ON {
150 HYST_OFF
151 } else if prev == REALLY_OFF {
152 HYST_ON
153 } else {
154 -1.0
157 }
158}
159
160fn tran_state(p: &SwitchParams, c: f64, prev: f64, current_ctrl: bool) -> f64 {
165 if p.hyst > 0.0 {
166 if c > p.thresh + p.hyst {
167 REALLY_ON
168 } else if c < p.thresh - p.hyst {
169 REALLY_OFF
170 } else {
171 prev
172 }
173 } else if c > p.thresh - p.hyst {
174 REALLY_ON
175 } else if c < p.thresh + p.hyst {
176 REALLY_OFF
177 } else if prev == HYST_ON || prev == HYST_OFF {
178 prev
179 } else if current_ctrl {
180 if prev == REALLY_ON {
181 HYST_OFF
182 } else if prev == REALLY_OFF {
183 HYST_ON
184 } else {
185 -1.0
186 }
187 } else if prev == REALLY_ON {
188 REALLY_OFF
189 } else if prev == REALLY_OFF {
190 REALLY_ON
191 } else {
192 0.0
193 }
194}
195
196fn is_on(state: f64) -> bool {
198 state == REALLY_ON || state == HYST_ON
199}
200
201impl SwitchInst {
202 pub fn new(p: SwitchParams, pos: Var, neg: Var, ctrl: Control, on: bool, st: usize) -> Self {
203 SwitchInst {
204 p,
205 pos,
206 neg,
207 ctrl,
208 on,
209 st,
210 slots: [TRASH; 4],
211 }
212 }
213
214 fn is_current(&self) -> bool {
215 matches!(self.ctrl, Control::Current { .. })
216 }
217
218 fn ctrl_value(&self, sol: &[f64]) -> f64 {
219 match self.ctrl {
220 Control::Voltage { pos, neg } => sol[pos] - sol[neg],
221 Control::Current { branch } => sol[branch],
222 }
223 }
224
225 fn g_small_signal(&self, state0: f64) -> f64 {
229 if state0 as i32 != 0 {
230 self.p.gon
231 } else {
232 self.p.goff
233 }
234 }
235
236 fn stamp(&self, g: f64, mut add: impl FnMut(Slot, f64)) {
237 add(self.slots[0], g);
238 add(self.slots[1], -g);
239 add(self.slots[2], -g);
240 add(self.slots[3], g);
241 }
242}
243
244impl crate::devices::Device for SwitchInst {
245 fn par_load(&self) -> bool {
246 true
247 }
248
249 fn disto_setup(&mut self, _ctx: &crate::disto::DistoSetupCtx) -> Result<(), &'static str> {
251 Ok(())
252 }
253
254 fn alloc(&mut self, mb: &mut MatrixBuilder) {
255 self.slots = [
256 mb.slot(self.pos, self.pos),
257 mb.slot(self.pos, self.neg),
258 mb.slot(self.neg, self.pos),
259 mb.slot(self.neg, self.neg),
260 ];
261 }
262
263 fn load(&mut self, ctx: &mut LoadCtx) {
265 let st = self.st + SWITCHSTATE;
266 let old = ctx.states.get(0, st);
267 let prev = ctx.states.get(1, st);
268 let c = self.ctrl_value(ctx.sol);
269 let current = self.is_current();
270
271 let state = match ctx.mode {
272 InitMode::Jct | InitMode::Fix => init_state(&self.p, self.on, c),
273 InitMode::Smsig => prev,
274 InitMode::Float => {
275 let keep = if current { prev } else { old };
276 let s = float_state(&self.p, c, keep, prev);
277 if s != old {
278 ctx.noncon = true;
280 }
281 s
282 }
283 InitMode::Tran | InitMode::Pred => tran_state(&self.p, c, prev, current),
284 };
285
286 ctx.states.set(0, st, state);
287 if !current {
288 ctx.states.set(0, self.st + CTRLVALUE, c);
291 }
292
293 let g = if is_on(state) {
294 self.p.gon
295 } else {
296 self.p.goff
297 };
298 self.stamp(g, |s, v| ctx.add(s, v));
299 }
300
301 fn trunc(&self, t: &TerrCtx, time_step: &mut f64) {
309 if self.is_current() {
310 return;
311 }
312 let ctrl = self.st + CTRLVALUE;
313 let v0 = t.states.get(0, ctrl);
314 let last_change = v0 - t.states.get(1, ctrl);
315 if t.states.get(0, self.st + SWITCHSTATE) == 0.0 {
316 let r = self.p.thresh + self.p.hyst;
317 if v0 < r && last_change > 0.0 {
318 let max_change = (r - v0) * 0.75 + 0.05;
319 let max_step = max_change / last_change * t.delta_old[0];
320 if *time_step > max_step {
321 *time_step = max_step;
322 }
323 }
324 } else {
325 let r = self.p.thresh - self.p.hyst;
326 if v0 > r && last_change < 0.0 {
327 let max_change = (r - v0) * 0.75 - 0.05;
328 let max_step = max_change / last_change * t.delta_old[0];
329 if *time_step > max_step {
330 *time_step = max_step;
331 }
332 }
333 }
334 }
335
336 fn load_ac(&mut self, ctx: &mut AcCtx) {
340 let g = self.g_small_signal(ctx.states.get(0, self.st + SWITCHSTATE));
341 self.stamp(g, |s, v| ctx.add_g(s, v));
342 }
343
344 fn load_pz(&mut self, ctx: &mut PzCtx) -> Result<(), &'static str> {
346 let g = self.g_small_signal(ctx.states.get(0, self.st + SWITCHSTATE));
347 self.stamp(g, |s, v| ctx.add_g(s, v));
348 Ok(())
349 }
350
351 fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
354 Some(&[""])
355 }
356
357 fn noise_dens(&self, ctx: &NoiseCtx) -> Vec<f64> {
358 let g = self.g_small_signal(ctx.states.get(0, self.st + SWITCHSTATE));
359 vec![ctx.thermal(self.pos, self.neg, g)]
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366
367 fn p(hyst: f64) -> SwitchParams {
368 SwitchParams::new(2.5, hyst, Some(10.0), Some(1e6), 1e-12)
369 }
370
371 #[test]
372 fn defaults_follow_swsetup() {
373 let d = SwitchParams::new(0.0, 0.0, None, None, 1e-12);
374 assert_eq!(d.gon, 1.0);
375 assert_eq!(d.goff, 1e-12);
376 let q = p(1.0);
377 assert_eq!(q.gon, 0.1);
378 assert_eq!(q.goff, 1e-6);
379 }
380
381 #[test]
382 fn init_honours_the_hint() {
383 let q = p(1.0);
384 assert_eq!(init_state(&q, true, 4.0), REALLY_ON);
387 assert_eq!(init_state(&q, true, 2.5), HYST_ON);
388 assert_eq!(init_state(&q, true, 0.0), HYST_ON);
389 assert_eq!(init_state(&q, false, 0.0), REALLY_OFF);
391 assert_eq!(init_state(&q, false, 2.5), HYST_OFF);
392 assert_eq!(init_state(&q, false, 4.0), HYST_OFF);
393 assert!(is_on(HYST_ON) && is_on(REALLY_ON));
394 assert!(!is_on(HYST_OFF) && !is_on(REALLY_OFF));
395 }
396
397 #[test]
398 fn hysteresis_rising_and_falling() {
399 let q = p(1.0);
400 let mut s = REALLY_OFF;
403 let mut trace = Vec::new();
404 for v in [0.0, 1.0, 2.0, 3.0, 3.4, 3.6, 4.5, 5.0] {
405 s = float_state(&q, v, s, s);
406 trace.push(is_on(s));
407 }
408 assert_eq!(trace, [false, false, false, false, false, true, true, true]);
409 let mut trace = Vec::new();
411 for v in [5.0, 3.0, 2.0, 1.6, 1.4, 0.0] {
412 s = float_state(&q, v, s, s);
413 trace.push(is_on(s));
414 }
415 assert_eq!(trace, [true, true, true, true, false, false]);
416 assert_eq!(tran_state(&q, 2.5, REALLY_ON, false), REALLY_ON);
419 assert_eq!(tran_state(&q, 2.5, REALLY_OFF, false), REALLY_OFF);
420 assert_eq!(tran_state(&q, 3.6, REALLY_OFF, true), REALLY_ON);
421 assert_eq!(tran_state(&q, 1.4, HYST_ON, true), REALLY_OFF);
422 }
423
424 #[test]
425 fn in_window_hint_survives_float_for_sw_not_for_csw() {
426 let q = p(1.0);
427 let after_fix = init_state(&q, true, 2.5);
430 assert_eq!(float_state(&q, 2.5, after_fix, 0.0), HYST_ON);
431 assert_eq!(float_state(&q, 2.5, 0.0, 0.0), REALLY_OFF);
432 }
433
434 #[test]
435 fn non_positive_hysteresis_machine() {
436 let q = p(-1.0);
439 assert_eq!(float_state(&q, 4.0, 0.0, REALLY_OFF), REALLY_ON);
440 assert_eq!(float_state(&q, 1.0, 0.0, REALLY_ON), REALLY_OFF);
441 assert_eq!(float_state(&q, 2.5, 0.0, REALLY_ON), HYST_OFF);
442 assert_eq!(float_state(&q, 2.5, 0.0, REALLY_OFF), HYST_ON);
443 assert_eq!(float_state(&q, 2.5, 0.0, HYST_ON), HYST_ON);
444 assert_eq!(tran_state(&q, 2.5, REALLY_ON, false), REALLY_OFF);
445 assert_eq!(tran_state(&q, 2.5, REALLY_ON, true), HYST_OFF);
446 let z = p(0.0);
449 assert_eq!(float_state(&z, 2.6, 0.0, REALLY_OFF), REALLY_ON);
450 assert_eq!(float_state(&z, 2.4, 0.0, REALLY_ON), REALLY_OFF);
451 assert_eq!(float_state(&z, 2.5, 0.0, REALLY_OFF), HYST_ON);
452 }
453
454 #[test]
455 fn small_signal_state_is_the_int_cast() {
456 let sw = SwitchInst::new(p(1.0), 1, 2, Control::Voltage { pos: 3, neg: 0 }, false, 0);
457 assert_eq!(sw.g_small_signal(REALLY_OFF), 1e-6);
458 assert_eq!(sw.g_small_signal(REALLY_ON), 0.1);
459 assert_eq!(sw.g_small_signal(HYST_OFF), 0.1);
460 assert_eq!(sw.g_small_signal(HYST_ON), 0.1);
461 }
462}