1use dune_netlist::ir::{
27 MeasCount, MeasCross, MeasEdge, MeasFunc, MeasLevel, MeasPoint, MeasStat, MeasTrig, MeasWindow,
28 Measure, OutVar,
29};
30
31use crate::select::{ColData, OutExpr};
32use crate::solution::Solution;
33
34const DC_FAR: f64 = 1e99;
36
37#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct Measurement {
42 pub value: f64,
43 pub detail: MeasDetail,
44}
45
46#[derive(Clone, Copy, Debug, PartialEq)]
48pub enum MeasDetail {
49 None,
51 Delay { trig: f64, targ: f64 },
53 At { at: f64 },
55 With { with: f64 },
58 Window { from: f64, to: f64 },
65}
66
67impl MeasDetail {
68 pub fn pairs(&self) -> Vec<(&'static str, f64)> {
72 match *self {
73 MeasDetail::None => Vec::new(),
74 MeasDetail::Delay { trig, targ } => vec![("trig", trig), ("targ", targ)],
75 MeasDetail::At { at } => vec![("at", at)],
76 MeasDetail::With { with } => vec![("with", with)],
77 MeasDetail::Window { from, to } => vec![("from", from), ("to", to)],
78 }
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum MeasClause {
86 Trig,
87 Targ,
88 When,
89 At,
90}
91
92impl MeasClause {
93 pub fn tag(self) -> &'static str {
95 match self {
96 MeasClause::Trig => "TRIG",
97 MeasClause::Targ => "TARG",
98 MeasClause::When => "WHEN",
99 MeasClause::At => "AT",
100 }
101 }
102}
103
104#[derive(Clone, Debug, PartialEq, thiserror::Error)]
106pub enum MeasureError {
107 #[error("out of interval")]
110 OutOfInterval { clause: MeasClause },
111 #[error("no such vector as `{0}`")]
113 UnknownVector(String),
114 #[error("measure limited to tran, dc, sp, or ac analysis (plot `{0}`)")]
116 UnsupportedPlot(String),
117}
118
119impl MeasureError {
120 pub fn describe(&self) -> String {
123 match self {
124 MeasureError::OutOfInterval { clause } => {
125 format!("out of interval ({})", clause.tag())
126 }
127 other => other.to_string(),
128 }
129 }
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134enum Kind {
135 Tran,
136 Ac,
138 Dc,
139}
140
141fn plot_kind(sol: &Solution) -> Result<Kind, MeasureError> {
142 let kind = match sol.plotname.as_str() {
143 "Transient Analysis" => Kind::Tran,
144 "AC Analysis" | "SP Analysis" => Kind::Ac,
145 "DC transfer characteristic" => Kind::Dc,
146 other => return Err(MeasureError::UnsupportedPlot(other.to_string())),
147 };
148 if sol.scale.is_none() {
149 return Err(MeasureError::UnsupportedPlot(sol.plotname.clone()));
150 }
151 Ok(kind)
152}
153
154pub fn applies_to(analysis: dune_netlist::ir::MeasAnalysis, sol: &Solution) -> bool {
156 use dune_netlist::ir::MeasAnalysis as A;
157 matches!(
158 (analysis, sol.plotname.as_str()),
159 (A::Tran, "Transient Analysis")
160 | (A::Ac, "AC Analysis")
161 | (A::Sp, "SP Analysis")
162 | (A::Dc, "DC transfer characteristic")
163 )
164}
165
166struct Plot<'a> {
168 sol: &'a Solution,
169 kind: Kind,
170 x: Vec<f64>,
171}
172
173impl<'a> Plot<'a> {
174 fn new(sol: &'a Solution) -> Result<Self, MeasureError> {
175 let kind = plot_kind(sol)?;
176 let sel = sol
177 .select(&[])
178 .map_err(|e| MeasureError::UnknownVector(e.to_string()))?;
179 let x = match sel.scale.map(|c| c.data) {
180 Some(ColData::Real(v)) => v,
181 Some(ColData::Complex(v)) => v.iter().map(|z| z.re).collect(),
182 None => return Err(MeasureError::UnsupportedPlot(sol.plotname.clone())),
183 };
184 Ok(Plot { sol, kind, x })
185 }
186
187 fn trace(&self, var: &OutVar) -> Result<Vec<f64>, MeasureError> {
190 let expr = OutExpr::from_ir(var);
191 let label = expr.label();
192 let sel = self
193 .sol
194 .select(std::slice::from_ref(&expr))
195 .map_err(|_| MeasureError::UnknownVector(label.clone()))?;
196 let Some(col) = sel.columns.into_iter().next() else {
197 return Err(MeasureError::UnknownVector(label));
198 };
199 Ok(match col.data {
200 ColData::Real(v) => v,
201 ColData::Complex(v) => v.iter().map(|z| z.re).collect(),
202 })
203 }
204}
205
206impl Solution {
207 pub fn measure(&self, func: &MeasFunc) -> Result<Measurement, MeasureError> {
212 let p = Plot::new(self)?;
213 match func {
214 MeasFunc::Delay { trig, targ } => delay(&p, trig, targ),
215 MeasFunc::When(c) => {
216 let t = when(&p, c)?.ok_or(MeasureError::OutOfInterval {
217 clause: MeasClause::When,
218 })?;
219 Ok(Measurement {
220 value: t,
221 detail: MeasDetail::None,
222 })
223 }
224 MeasFunc::Find { var, at } => {
225 let y = p.trace(var)?;
226 let x = point(&p, at)?;
227 let v = value_at(&p, &y, x).ok_or(MeasureError::OutOfInterval {
228 clause: MeasClause::At,
229 })?;
230 Ok(Measurement {
231 value: v,
232 detail: MeasDetail::None,
233 })
234 }
235 MeasFunc::Deriv { var, at } => {
236 let y = p.trace(var)?;
237 let x = point(&p, at)?;
238 let d = derivative(&p.x, &y);
239 let v = value_at(&p, &d, x).filter(|v| !v.is_nan()).ok_or(
240 MeasureError::OutOfInterval {
241 clause: MeasClause::At,
242 },
243 )?;
244 Ok(Measurement {
245 value: v,
246 detail: MeasDetail::None,
247 })
248 }
249 MeasFunc::Stat { stat, var, window } => {
250 let y = p.trace(var)?;
251 stat_measure(&p, *stat, &y, window)
252 }
253 }
254 }
255}
256
257#[derive(Clone, Debug)]
259pub struct MeasureOutcome {
260 pub card: usize,
262 pub plot: usize,
264 pub result: Result<Measurement, MeasureError>,
265}
266
267pub fn measure_all(measures: &[Measure], solutions: &[Solution]) -> Vec<MeasureOutcome> {
271 let mut out = Vec::new();
272 for (pi, sol) in solutions.iter().enumerate() {
273 for (ci, m) in measures.iter().enumerate() {
274 if applies_to(m.analysis, sol) {
275 out.push(MeasureOutcome {
276 card: ci,
277 plot: pi,
278 result: sol.measure(&m.func),
279 });
280 }
281 }
282 }
283 out
284}
285
286fn delay(p: &Plot, trig: &MeasTrig, targ: &MeasTrig) -> Result<Measurement, MeasureError> {
288 let mut trig = trig.clone();
290 let mut targ = targ.clone();
291 if let (MeasTrig::Cross(a), MeasTrig::Cross(b)) = (&mut trig, &mut targ) {
292 match (a.window.from, b.window.from) {
293 (Some(f), None) => b.window.from = Some(f),
294 (None, Some(f)) => a.window.from = Some(f),
295 _ => {}
296 }
297 }
298 let side = |t: &MeasTrig, clause| -> Result<f64, MeasureError> {
299 match t {
300 MeasTrig::At(x) => Ok(*x),
301 MeasTrig::Cross(c) => when(p, c)?.ok_or(MeasureError::OutOfInterval { clause }),
302 }
303 };
304 let t0 = side(&trig, MeasClause::Trig)?;
305 let t1 = side(&targ, MeasClause::Targ)?;
306 Ok(Measurement {
307 value: t1 - t0,
308 detail: MeasDetail::Delay { trig: t0, targ: t1 },
309 })
310}
311
312fn point(p: &Plot, at: &MeasPoint) -> Result<f64, MeasureError> {
314 match at {
315 MeasPoint::At(x) => Ok(*x),
316 MeasPoint::When(c) => when(p, c)?.ok_or(MeasureError::OutOfInterval {
317 clause: MeasClause::At,
318 }),
319 }
320}
321
322#[derive(Clone, Copy)]
326struct Win {
327 td: f64,
328 from: f64,
329 to: f64,
330}
331
332fn window(kind: Kind, w: &MeasWindow) -> Win {
333 match kind {
334 Kind::Dc => {
335 let mut from = w.from.unwrap_or(-DC_FAR);
336 let mut to = w.to.unwrap_or(DC_FAR);
337 if to < from {
338 std::mem::swap(&mut from, &mut to);
339 }
340 Win { td: 0.0, from, to }
341 }
342 _ => Win {
343 td: w.td.unwrap_or(0.0),
344 from: w.from.unwrap_or(0.0),
345 to: w.to.unwrap_or(0.0),
346 },
347 }
348}
349
350enum Gate {
352 Skip,
353 Take,
354 Stop,
355}
356
357fn gate(kind: Kind, w: &Win, x: f64) -> Gate {
359 match kind {
360 Kind::Dc => {
361 if x < w.from || x > w.to {
362 Gate::Skip
363 } else {
364 Gate::Take
365 }
366 }
367 _ => {
368 if x < w.from {
369 Gate::Skip
370 } else if w.to != 0.0 && x > w.to {
371 Gate::Stop
372 } else {
373 Gate::Take
374 }
375 }
376 }
377}
378
379#[derive(Clone, Copy, PartialEq, Eq)]
380enum Side {
381 Above,
382 Below,
383}
384
385fn when(p: &Plot, c: &MeasCross) -> Result<Option<f64>, MeasureError> {
389 let y = p.trace(&c.var)?;
390 let y2 = match &c.level {
391 MeasLevel::Value(_) => None,
392 MeasLevel::Var(v) => Some(p.trace(v)?),
393 };
394 let level = match c.level {
395 MeasLevel::Value(v) => v,
396 MeasLevel::Var(_) => f64::NAN,
397 };
398 let w = window(p.kind, &c.window);
399 let (want_rise, want_fall, want_cross) = match c.edge {
401 MeasEdge::Any => (None, None, None),
402 MeasEdge::Rise(n) => (Some(n), None, None),
403 MeasEdge::Fall(n) => (None, Some(n), None),
404 MeasEdge::Cross(n) => (None, None, Some(n)),
405 };
406 let last_rise = want_rise == Some(MeasCount::Last);
407 let last_fall = want_fall == Some(MeasCount::Last);
408 let any_last = matches!(
409 c.edge,
410 MeasEdge::Rise(MeasCount::Last)
411 | MeasEdge::Fall(MeasCount::Last)
412 | MeasEdge::Cross(MeasCount::Last)
413 );
414 let nth = |want: Option<MeasCount>, n: u32| want == Some(MeasCount::Nth(n));
415
416 let origin = p.x.first().copied().unwrap_or(0.0);
417 let (mut rise, mut fall, mut cross) = (0u32, 0u32, 0u32);
418 let mut section = Side::Above;
419 let mut started = false;
420 let (mut px, mut pv, mut pv2) = (0.0, 0.0, 0.0);
421 let mut last: Option<f64> = None;
422
423 for i in 0..y.len().min(p.x.len()) {
424 let x = p.x[i];
425 let v = y[i];
426 let v2 = y2.as_ref().map_or(level, |y2| y2[i]);
427 match p.kind {
428 Kind::Tran if x < w.td => continue,
429 Kind::Ac if x < 0.0 => continue,
430 _ => {}
431 }
432 match gate(p.kind, &w, x) {
433 Gate::Skip => continue,
434 Gate::Stop => break,
435 Gate::Take => {}
436 }
437 if started && p.kind == Kind::Dc && x == origin {
440 started = false;
441 cross = 0;
442 }
443 if !started {
444 section = if v < v2 { Side::Below } else { Side::Above };
445 started = true;
446 (px, pv, pv2) = (x, v, v2);
447 continue;
448 }
449 let mut pending = false;
450 if section == Side::Below && v >= v2 {
451 section = Side::Above;
452 cross += 1;
453 rise += 1;
454 pending = !last_fall;
455 } else if section == Side::Above && v <= v2 {
456 section = Side::Below;
457 cross += 1;
458 fall += 1;
459 pending = !last_rise;
460 }
461 let interp = || match &y2 {
462 None => px + (level - pv) * (x - px) / (v - pv),
463 Some(_) => px + (pv2 - pv) * (x - px) / (v - pv - v2 + pv2),
464 };
465 if nth(want_cross, cross) || nth(want_rise, rise) || nth(want_fall, fall) {
468 return Ok(finite(interp()));
469 }
470 if pending {
471 if c.edge == MeasEdge::Any {
472 return Ok(finite(interp()));
473 }
474 if any_last {
475 last = Some(interp());
476 }
477 }
478 (px, pv, pv2) = (x, v, v2);
479 }
480 Ok(last.and_then(finite))
481}
482
483fn finite(x: f64) -> Option<f64> {
487 (!x.is_nan()).then_some(x)
488}
489
490fn value_at(p: &Plot, y: &[f64], at: f64) -> Option<f64> {
493 let n = y.len().min(p.x.len());
494 for i in 1..n {
495 let (px, x) = (p.x[i - 1], p.x[i]);
496 let forward = px <= at && x >= at;
497 let backward = p.kind == Kind::Dc && px >= at && x <= at;
498 if forward || backward {
499 let (pv, v) = (y[i - 1], y[i]);
500 if x == px {
501 return Some(v);
502 }
503 return Some(pv + (at - px) * (v - pv) / (x - px));
504 }
505 }
506 None
507}
508
509fn derivative(x: &[f64], y: &[f64]) -> Vec<f64> {
515 let n = x.len().min(y.len());
516 let slope = |a: usize, b: usize| {
517 let h = x[b] - x[a];
518 if h == 0.0 {
519 f64::NAN
520 } else {
521 (y[b] - y[a]) / h
522 }
523 };
524 (0..n)
525 .map(|i| {
526 if n < 2 {
527 return f64::NAN;
528 }
529 if i == 0 {
530 return slope(0, 1);
531 }
532 if i == n - 1 {
533 return slope(n - 2, n - 1);
534 }
535 let h0 = x[i] - x[i - 1];
536 let h1 = x[i + 1] - x[i];
537 if h0 == 0.0 {
538 return slope(i, i + 1);
539 }
540 if h1 == 0.0 || h0 + h1 == 0.0 {
541 return slope(i - 1, i);
542 }
543 (h0 * h0 * y[i + 1] - h1 * h1 * y[i - 1] + (h1 * h1 - h0 * h0) * y[i])
544 / (h0 * h1 * (h0 + h1))
545 })
546 .collect()
547}
548
549fn stat_measure(
550 p: &Plot,
551 stat: MeasStat,
552 y: &[f64],
553 w: &MeasWindow,
554) -> Result<Measurement, MeasureError> {
555 let win = window(p.kind, w);
556 let oob = MeasureError::OutOfInterval {
557 clause: MeasClause::Trig,
558 };
559 match stat {
560 MeasStat::Min | MeasStat::Max | MeasStat::MinAt | MeasStat::MaxAt => {
561 let max = matches!(stat, MeasStat::Max | MeasStat::MaxAt);
562 let (m, at) = extremum(p, y, &win, max).ok_or(oob)?;
563 Ok(match stat {
564 MeasStat::Min | MeasStat::Max => Measurement {
565 value: m,
566 detail: MeasDetail::At { at },
567 },
568 _ => Measurement {
569 value: at,
570 detail: MeasDetail::With { with: m },
571 },
572 })
573 }
574 MeasStat::Pp => {
575 let (lo, _) = extremum(p, y, &win, false).ok_or(oob.clone())?;
576 let (hi, _) = extremum(p, y, &win, true).ok_or(oob)?;
577 Ok(Measurement {
578 value: hi - lo,
579 detail: MeasDetail::Window {
580 from: win.from,
581 to: win.to,
582 },
583 })
584 }
585 MeasStat::Avg => {
586 let (avg, to) = average(p, y, &win);
587 let avg = avg.ok_or(oob)?;
588 Ok(Measurement {
589 value: avg,
590 detail: MeasDetail::Window { from: win.from, to },
591 })
592 }
593 MeasStat::Rms | MeasStat::Integ => {
594 let (v, from, to) = rms_integral(p, y, &win, stat == MeasStat::Rms);
595 if v.is_nan() {
596 return Err(oob);
597 }
598 Ok(Measurement {
599 value: v,
600 detail: MeasDetail::Window { from, to },
601 })
602 }
603 }
604}
605
606fn extremum(p: &Plot, y: &[f64], w: &Win, max: bool) -> Option<(f64, f64)> {
609 let mut best: Option<(f64, f64)> = None;
610 for (&x, &v) in p.x.iter().zip(y) {
611 match gate(p.kind, w, x) {
612 Gate::Skip => continue,
613 Gate::Stop => break,
614 Gate::Take => {}
615 }
616 best = match best {
617 None => Some((v, x)),
618 Some((m, _)) if (max && v >= m) || (!max && v <= m) => Some((v, x)),
619 keep => keep,
620 };
621 }
622 best
623}
624
625fn average(p: &Plot, y: &[f64], w: &Win) -> (Option<f64>, f64) {
629 let mut sum = 0.0;
630 let mut tsum = 0.0;
631 let mut prev: Option<(f64, f64)> = None;
632 let mut last_x = 0.0;
633 for (&x, &v) in p.x.iter().zip(y) {
634 last_x = x;
635 match gate(p.kind, w, x) {
636 Gate::Skip => continue,
637 Gate::Stop => break,
638 Gate::Take => {}
639 }
640 if let Some((px, pv)) = prev {
641 sum += 0.5 * (v + pv) * (x - px);
642 tsum += x - px;
643 }
644 prev = Some((x, v));
645 }
646 match prev {
647 Some(_) => {
649 let v = sum / tsum;
650 (Some(v).filter(|v| !v.is_nan()), last_x)
651 }
652 None => (None, last_x),
653 }
654}
655
656pub(crate) fn almost_equal_ulps(a: f64, b: f64, max_ulps: i64) -> bool {
658 if a == b {
659 return true;
660 }
661 let ord = |x: f64| {
662 let i = x.to_bits() as i64;
663 if i < 0 { i64::MIN.wrapping_sub(i) } else { i }
664 };
665 ord(a).wrapping_sub(ord(b)).unsigned_abs() <= max_ulps as u64
666}
667
668fn rms_integral(p: &Plot, y: &[f64], w: &Win, rms: bool) -> (f64, f64, f64) {
673 let n = y.len().min(p.x.len());
674 let sq = |v: f64| if rms { v * v } else { v };
675 let lerp = |i: usize, at: f64| {
676 let slope = (y[i] - y[i - 1]) / (p.x[i] - p.x[i - 1]);
677 let yint = y[i - 1] - slope * p.x[i - 1];
678 slope * at + yint
679 };
680 let mut xs = Vec::new();
681 let mut ys = Vec::new();
682 let mut from_at = f64::NAN;
683 let mut to_val: Option<f64> = None;
684 for (i, (&x0, &v0)) in p.x.iter().zip(y).enumerate() {
685 let (mut x, mut v) = (x0, v0);
686 if x < w.from {
687 continue;
688 }
689 if w.to != 0.0 && x > w.to {
690 if i > 0 && !almost_equal_ulps(x, w.to, 100) {
691 v = lerp(i, w.to);
692 x = w.to;
693 }
694 xs.push(x);
695 ys.push(sq(v));
696 to_val = Some(x);
697 break;
698 }
699 if xs.is_empty() {
700 if w.from != 0.0 && i > 0 && !almost_equal_ulps(x, w.from, 100) {
701 v = lerp(i, w.from);
702 x = w.from;
703 }
704 from_at = x;
705 }
706 xs.push(x);
707 ys.push(sq(v));
708 }
709 let m = xs.len();
710 let mut width: Vec<f64> = (0..m.saturating_sub(1))
712 .map(|i| xs[i + 1] - xs[i])
713 .collect();
714 width.extend([0.0, 0.0]);
715 let (mut s1, mut s2, mut s3) = (0.0, 0.0, 0.0);
716 let mut i = 0;
717 while i + 1 < m {
718 if i + 3 < m
719 && almost_equal_ulps(width[i], width[i + 1], 100)
720 && almost_equal_ulps(width[i], width[i + 2], 100)
721 {
722 s1 += 3.0 * width[i] * (ys[i] + 3.0 * (ys[i + 1] + ys[i + 2]) + ys[i + 3]) / 8.0;
723 i += 3;
724 } else if i + 2 < m && almost_equal_ulps(width[i], width[i + 1], 100) {
725 s2 += width[i] * (ys[i] + 4.0 * ys[i + 1] + ys[i + 2]) / 3.0;
726 i += 2;
727 } else {
728 s3 += width[i] * (ys[i] + ys[i + 1]) / 2.0;
729 i += 1;
730 }
731 }
732 let to = match to_val {
735 Some(t) if t >= 0.0 => t,
736 _ => p.x.get(n.wrapping_sub(1)).copied().unwrap_or(f64::NAN),
737 };
738 if m == 0 {
739 return (f64::NAN, from_at, to);
740 }
741 let sum = s1 + s2 + s3;
742 let v = if rms {
743 (sum / (to - from_at)).sqrt()
744 } else {
745 sum
746 };
747 (v, from_at, to)
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753 use dune_netlist::parse_measure_body;
754 use dune_raw::{Data, VarType};
755
756 fn tran(t: Vec<f64>, y: Vec<f64>) -> Solution {
757 Solution {
758 plotname: "Transient Analysis".into(),
759 variables: vec![
760 ("time".into(), VarType::Time),
761 ("v(1)".into(), VarType::Voltage),
762 ],
763 data: Data::Real(vec![t, y]),
764 scale: Some(0),
765 }
766 }
767
768 fn meas(s: &Solution, body: &str) -> Result<Measurement, MeasureError> {
769 let (f, _) = parse_measure_body(body).unwrap();
770 s.measure(&f)
771 }
772
773 fn val(s: &Solution, body: &str) -> f64 {
774 meas(s, body)
775 .unwrap_or_else(|e| panic!("{body}: {e}"))
776 .value
777 }
778
779 fn tri() -> Solution {
781 let t: Vec<f64> = (0..=40).map(|i| f64::from(i) * 0.1).collect();
782 let y = t
783 .iter()
784 .map(|&t| {
785 let f = t - t.floor();
786 if (t.floor() as i64) % 2 == 0 {
787 f
788 } else {
789 1.0 - f
790 }
791 })
792 .collect();
793 tran(t, y)
794 }
795
796 fn close(a: f64, b: f64) {
797 assert!((a - b).abs() <= 1e-12 * b.abs().max(1.0), "{a} vs {b}");
798 }
799
800 #[test]
801 fn crossings_by_edge_and_count() {
802 let s = tri();
803 close(val(&s, "when v(1)=0.25"), 0.25);
804 close(val(&s, "when v(1)=0.25 rise=2"), 2.25);
805 close(val(&s, "when v(1)=0.25 fall=1"), 1.75);
806 close(val(&s, "when v(1)=0.25 cross=3"), 2.25);
807 close(val(&s, "when v(1)=0.25 cross=last"), 3.75);
808 close(val(&s, "when v(1)=0.25 rise=last"), 2.25);
809 close(val(&s, "when v(1)=0.25 last"), 3.75);
810 close(val(&s, "when v(1)=0.25 td=1"), 1.75);
811 close(val(&s, "when v(1)=0.75 from=1 to=1.5"), 1.25);
812 assert!(meas(&s, "when v(1)=0.25 from=1 to=1.5").is_err());
813 }
814
815 #[test]
816 fn out_of_interval_and_unknown_vectors() {
817 let s = tri();
818 assert_eq!(
819 meas(&s, "when v(1)=5"),
820 Err(MeasureError::OutOfInterval {
821 clause: MeasClause::When
822 })
823 );
824 assert!(matches!(
825 meas(&s, "max v(9)"),
826 Err(MeasureError::UnknownVector(_))
827 ));
828 assert_eq!(
829 meas(&s, "trig v(1) val=0.5 rise=1 targ v(1) val=0.5 rise=9"),
830 Err(MeasureError::OutOfInterval {
831 clause: MeasClause::Targ
832 })
833 );
834 let op = Solution {
835 plotname: "Operating Point".into(),
836 variables: vec![("v(1)".into(), VarType::Voltage)],
837 data: Data::Real(vec![vec![1.0]]),
838 scale: None,
839 };
840 assert!(matches!(
841 meas(&op, "max v(1)"),
842 Err(MeasureError::UnsupportedPlot(_))
843 ));
844 }
845
846 #[test]
847 fn first_segment_crossing_counts() {
848 let s = tri();
851 close(val(&s, "when v(1)=0.05"), 0.05);
852 close(val(&s, "when v(1)=0.05 rise=1"), 0.05);
853 }
854
855 #[test]
856 fn delay_find_and_stats() {
857 let s = tri();
858 let m = meas(&s, "trig v(1) val=0.5 rise=1 targ v(1) val=0.5 fall=1").unwrap();
859 close(m.value, 1.0);
860 assert!(matches!(m.detail, MeasDetail::Delay { .. }));
861 close(val(&s, "trig at=0.2 targ v(1) val=0.5 rise=2"), 2.3);
862 close(val(&s, "find v(1) at=1.25"), 0.75);
863 close(val(&s, "find v(1) when v(1)=0.5 cross=2"), 0.5);
864 close(val(&s, "deriv v(1) at=0.55"), 1.0);
865 close(val(&s, "deriv v(1) at=1.55"), -1.0);
866 close(val(&s, "max v(1)"), 1.0);
867 close(val(&s, "min v(1) from=0.5 to=1.5"), 0.5);
868 close(val(&s, "pp v(1)"), 1.0);
869 close(val(&s, "avg v(1)"), 0.5);
870 let m = meas(&s, "max_at v(1) to=2").unwrap();
871 close(m.value, 1.0);
872 assert_eq!(m.detail, MeasDetail::With { with: 1.0 });
873 }
874
875 #[test]
876 fn simpson_is_exact_on_cubics() {
877 let t: Vec<f64> = (0..=12).map(|i| f64::from(i) / 10.0).collect();
883 let s = Solution {
884 plotname: "Transient Analysis".into(),
885 variables: vec![
886 ("time".into(), VarType::Time),
887 ("v(1)".into(), VarType::Voltage),
888 ("v(2)".into(), VarType::Voltage),
889 ],
890 data: Data::Real(vec![
891 t.clone(),
892 t.clone(),
893 t.iter().map(|t| t * t * t).collect(),
894 ]),
895 scale: Some(0),
896 };
897 close(val(&s, "integ v(2)"), 1.2f64.powi(4) / 4.0);
898 close(val(&s, "rms v(1)"), (1.44f64 / 3.0).sqrt());
899 let m = meas(&s, "integ v(1) from=0.05 to=0.95").unwrap();
901 close(m.value, 0.5 * (0.95 * 0.95 - 0.05 * 0.05));
902 let MeasDetail::Window { from, to } = m.detail else {
903 panic!()
904 };
905 close(from, 0.05);
906 close(to, 0.95);
907 }
908
909 #[test]
910 fn ulps_like_ngspice() {
911 assert!(almost_equal_ulps(1.0, 1.0 + 50.0 * f64::EPSILON, 100));
912 assert!(!almost_equal_ulps(1.0, 1.0 + 1e-10, 100));
913 assert!(almost_equal_ulps(0.0, -0.0, 100));
914 assert!(!almost_equal_ulps(1.0, -1.0, 100));
915 }
916
917 #[test]
918 fn derivative_is_exact_on_parabolas() {
919 let x = [0.0, 0.1, 0.3, 0.35, 0.8];
920 let y: Vec<f64> = x.iter().map(|x| 3.0 * x * x - x).collect();
921 let d = derivative(&x, &y);
922 for i in 1..4 {
923 close(d[i], 6.0 * x[i] - 1.0);
924 }
925 }
926}