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 crossing(p: &SwitchParams, c1: f64, c0: f64) -> Option<f64> {
200 let (lo, hi) = (p.thresh - p.hyst.abs(), p.thresh + p.hyst.abs());
201 let between = |l: f64| (c1 <= l && l <= c0) || (c0 <= l && l <= c1);
202 let rising = c0 > c1;
203 let order = if (p.hyst > 0.0) == rising {
205 [hi, lo]
206 } else {
207 [lo, hi]
208 };
209 let level = order.into_iter().find(|&l| between(l))?;
210 let theta = (level - c1) / (c0 - c1);
211 (theta.is_finite() && (0.0..=1.0).contains(&theta)).then_some(theta)
212}
213
214fn flip_level(p: &SwitchParams, c1: f64, on: bool) -> f64 {
218 let (lo, hi) = (p.thresh - p.hyst.abs(), p.thresh + p.hyst.abs());
219 if p.hyst > 0.0 {
220 if on { hi } else { lo }
221 } else if on {
222 if c1 < lo { lo } else { hi }
223 } else if c1 > hi {
224 hi
225 } else {
226 lo
227 }
228}
229
230fn is_on(state: f64) -> bool {
232 state == REALLY_ON || state == HYST_ON
233}
234
235impl SwitchInst {
236 pub fn new(p: SwitchParams, pos: Var, neg: Var, ctrl: Control, on: bool, st: usize) -> Self {
237 SwitchInst {
238 p,
239 pos,
240 neg,
241 ctrl,
242 on,
243 st,
244 slots: [TRASH; 4],
245 }
246 }
247
248 fn is_current(&self) -> bool {
249 matches!(self.ctrl, Control::Current { .. })
250 }
251
252 fn ctrl_value(&self, sol: &[f64]) -> f64 {
253 match self.ctrl {
254 Control::Voltage { pos, neg } => sol[pos] - sol[neg],
255 Control::Current { branch } => sol[branch],
256 }
257 }
258
259 fn g_small_signal(&self, state0: f64) -> f64 {
263 if state0 as i32 != 0 {
264 self.p.gon
265 } else {
266 self.p.goff
267 }
268 }
269
270 fn stamp(&self, g: f64, mut add: impl FnMut(Slot, f64)) {
271 add(self.slots[0], g);
272 add(self.slots[1], -g);
273 add(self.slots[2], -g);
274 add(self.slots[3], g);
275 }
276}
277
278impl crate::devices::Device for SwitchInst {
279 fn par_load(&self) -> bool {
280 true
281 }
282
283 fn disto_setup(&mut self, _ctx: &crate::disto::DistoSetupCtx) -> Result<(), &'static str> {
285 Ok(())
286 }
287
288 fn alloc(&mut self, mb: &mut MatrixBuilder) {
289 self.slots = [
290 mb.slot(self.pos, self.pos),
291 mb.slot(self.pos, self.neg),
292 mb.slot(self.neg, self.pos),
293 mb.slot(self.neg, self.neg),
294 ];
295 }
296
297 fn load(&mut self, ctx: &mut LoadCtx) {
299 let st = self.st + SWITCHSTATE;
300 let old = ctx.states.get(0, st);
301 let prev = ctx.states.get(1, st);
302 let c = self.ctrl_value(ctx.sol);
303 let current = self.is_current();
304
305 let state = match ctx.mode {
306 InitMode::Jct | InitMode::Fix => init_state(&self.p, self.on, c),
307 InitMode::Smsig => prev,
308 InitMode::Float => {
309 let keep = if current { prev } else { old };
310 let s = float_state(&self.p, c, keep, prev);
311 if s != old {
312 ctx.noncon = true;
314 }
315 s
316 }
317 InitMode::Tran | InitMode::Pred => tran_state(&self.p, c, prev, current),
318 };
319
320 ctx.states.set(0, st, state);
321 ctx.states.set(0, self.st + CTRLVALUE, c);
326
327 let g = if is_on(state) {
328 self.p.gon
329 } else {
330 self.p.goff
331 };
332 self.stamp(g, |s, v| ctx.add(s, v));
333 }
334
335 fn trunc(&self, t: &TerrCtx, time_step: &mut f64) {
343 if self.is_current() {
344 return;
345 }
346 let ctrl = self.st + CTRLVALUE;
347 let v0 = t.states.get(0, ctrl);
348 let last_change = v0 - t.states.get(1, ctrl);
349 if t.states.get(0, self.st + SWITCHSTATE) == 0.0 {
350 let r = self.p.thresh + self.p.hyst;
351 if v0 < r && last_change > 0.0 {
352 let max_change = (r - v0) * 0.75 + 0.05;
353 let max_step = max_change / last_change * t.delta_old[0];
354 if *time_step > max_step {
355 *time_step = max_step;
356 }
357 }
358 } else {
359 let r = self.p.thresh - self.p.hyst;
360 if v0 > r && last_change < 0.0 {
361 let max_change = (r - v0) * 0.75 - 0.05;
362 let max_step = max_change / last_change * t.delta_old[0];
363 if *time_step > max_step {
364 *time_step = max_step;
365 }
366 }
367 }
368 }
369
370 fn event(
387 &self,
388 states: &crate::state::States,
389 from: usize,
390 to: usize,
391 back: f64,
392 ) -> Option<f64> {
393 let st = self.st + SWITCHSTATE;
394 let on = is_on(states.get(to, st));
395 if on == is_on(states.get(from, st)) {
396 return None;
397 }
398 let ctrl = self.st + CTRLVALUE;
399 let c1 = states.get(from, ctrl);
400 if let Some(theta) = crossing(&self.p, c1, states.get(to, ctrl)) {
401 return Some(theta);
402 }
403 let rate = (c1 - states.get(from + 1, ctrl)) / back;
404 let theta = (flip_level(&self.p, c1, on) - c1) / rate;
405 Some(if theta.is_finite() && theta >= 0.0 {
406 theta.min(0.5)
407 } else {
408 0.5
409 })
410 }
411
412 fn load_ac(&mut self, ctx: &mut AcCtx) {
416 let g = self.g_small_signal(ctx.states.get(0, self.st + SWITCHSTATE));
417 self.stamp(g, |s, v| ctx.add_g(s, v));
418 }
419
420 fn load_pz(&mut self, ctx: &mut PzCtx) -> Result<(), &'static str> {
422 let g = self.g_small_signal(ctx.states.get(0, self.st + SWITCHSTATE));
423 self.stamp(g, |s, v| ctx.add_g(s, v));
424 Ok(())
425 }
426
427 fn noise_suffixes(&self) -> Option<&'static [&'static str]> {
430 Some(&[""])
431 }
432
433 fn noise_dens(&self, ctx: &NoiseCtx) -> Vec<f64> {
434 let g = self.g_small_signal(ctx.states.get(0, self.st + SWITCHSTATE));
435 vec![ctx.thermal(self.pos, self.neg, g)]
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn p(hyst: f64) -> SwitchParams {
444 SwitchParams::new(2.5, hyst, Some(10.0), Some(1e6), 1e-12)
445 }
446
447 #[test]
448 fn defaults_follow_swsetup() {
449 let d = SwitchParams::new(0.0, 0.0, None, None, 1e-12);
450 assert_eq!(d.gon, 1.0);
451 assert_eq!(d.goff, 1e-12);
452 let q = p(1.0);
453 assert_eq!(q.gon, 0.1);
454 assert_eq!(q.goff, 1e-6);
455 }
456
457 #[test]
458 fn init_honours_the_hint() {
459 let q = p(1.0);
460 assert_eq!(init_state(&q, true, 4.0), REALLY_ON);
463 assert_eq!(init_state(&q, true, 2.5), HYST_ON);
464 assert_eq!(init_state(&q, true, 0.0), HYST_ON);
465 assert_eq!(init_state(&q, false, 0.0), REALLY_OFF);
467 assert_eq!(init_state(&q, false, 2.5), HYST_OFF);
468 assert_eq!(init_state(&q, false, 4.0), HYST_OFF);
469 assert!(is_on(HYST_ON) && is_on(REALLY_ON));
470 assert!(!is_on(HYST_OFF) && !is_on(REALLY_OFF));
471 }
472
473 #[test]
474 fn hysteresis_rising_and_falling() {
475 let q = p(1.0);
476 let mut s = REALLY_OFF;
479 let mut trace = Vec::new();
480 for v in [0.0, 1.0, 2.0, 3.0, 3.4, 3.6, 4.5, 5.0] {
481 s = float_state(&q, v, s, s);
482 trace.push(is_on(s));
483 }
484 assert_eq!(trace, [false, false, false, false, false, true, true, true]);
485 let mut trace = Vec::new();
487 for v in [5.0, 3.0, 2.0, 1.6, 1.4, 0.0] {
488 s = float_state(&q, v, s, s);
489 trace.push(is_on(s));
490 }
491 assert_eq!(trace, [true, true, true, true, false, false]);
492 assert_eq!(tran_state(&q, 2.5, REALLY_ON, false), REALLY_ON);
495 assert_eq!(tran_state(&q, 2.5, REALLY_OFF, false), REALLY_OFF);
496 assert_eq!(tran_state(&q, 3.6, REALLY_OFF, true), REALLY_ON);
497 assert_eq!(tran_state(&q, 1.4, HYST_ON, true), REALLY_OFF);
498 }
499
500 #[test]
501 fn in_window_hint_survives_float_for_sw_not_for_csw() {
502 let q = p(1.0);
503 let after_fix = init_state(&q, true, 2.5);
506 assert_eq!(float_state(&q, 2.5, after_fix, 0.0), HYST_ON);
507 assert_eq!(float_state(&q, 2.5, 0.0, 0.0), REALLY_OFF);
508 }
509
510 #[test]
511 fn non_positive_hysteresis_machine() {
512 let q = p(-1.0);
515 assert_eq!(float_state(&q, 4.0, 0.0, REALLY_OFF), REALLY_ON);
516 assert_eq!(float_state(&q, 1.0, 0.0, REALLY_ON), REALLY_OFF);
517 assert_eq!(float_state(&q, 2.5, 0.0, REALLY_ON), HYST_OFF);
518 assert_eq!(float_state(&q, 2.5, 0.0, REALLY_OFF), HYST_ON);
519 assert_eq!(float_state(&q, 2.5, 0.0, HYST_ON), HYST_ON);
520 assert_eq!(tran_state(&q, 2.5, REALLY_ON, false), REALLY_OFF);
521 assert_eq!(tran_state(&q, 2.5, REALLY_ON, true), HYST_OFF);
522 let z = p(0.0);
525 assert_eq!(float_state(&z, 2.6, 0.0, REALLY_OFF), REALLY_ON);
526 assert_eq!(float_state(&z, 2.4, 0.0, REALLY_ON), REALLY_OFF);
527 assert_eq!(float_state(&z, 2.5, 0.0, REALLY_OFF), HYST_ON);
528 }
529
530 #[test]
531 fn crossing_picks_the_flipping_edge() {
532 let q = p(1.0);
535 assert_eq!(crossing(&q, 3.0, 4.0), Some(0.5));
536 assert_eq!(crossing(&q, 1.0, 5.0), Some(0.625));
537 assert_eq!(crossing(&q, 5.0, 1.0), Some(0.875));
538 assert_eq!(crossing(&q, 2.0, 3.0), None);
539 let n = p(-1.0);
541 assert_eq!(crossing(&n, 1.0, 5.0), Some(0.125));
542 assert_eq!(crossing(&n, 5.0, 1.0), Some(0.375));
543 assert_eq!(crossing(&n, 2.0, 4.0), Some(0.75));
544 assert_eq!(crossing(&q, 3.5, 3.5), None);
546 }
547
548 #[test]
549 fn flip_level_follows_the_state_machine() {
550 let q = p(1.0);
551 assert_eq!(flip_level(&q, 3.0, true), 3.5);
552 assert_eq!(flip_level(&q, 3.0, false), 1.5);
553 let n = p(-1.0);
554 assert_eq!(flip_level(&n, 1.0, true), 1.5);
555 assert_eq!(flip_level(&n, 2.0, true), 3.5);
556 assert_eq!(flip_level(&n, 4.0, false), 3.5);
557 assert_eq!(flip_level(&n, 2.0, false), 1.5);
558 }
559
560 #[test]
561 fn small_signal_state_is_the_int_cast() {
562 let sw = SwitchInst::new(p(1.0), 1, 2, Control::Voltage { pos: 3, neg: 0 }, false, 0);
563 assert_eq!(sw.g_small_signal(REALLY_OFF), 1e-6);
564 assert_eq!(sw.g_small_signal(REALLY_ON), 0.1);
565 assert_eq!(sw.g_small_signal(HYST_OFF), 0.1);
566 assert_eq!(sw.g_small_signal(HYST_ON), 0.1);
567 }
568}