Skip to main content

sound_to_patch/
confidence.rs

1//! How far a layer trusts one of its estimates.
2//!
3//! A [`Confidence`] holds a [`Level`], the [`Reason`]s behind it and an
4//! optional spread, or states that the layer made no estimate. The level is
5//! stored, not derived: each estimator sets it from its own signals. A
6//! confidence is never one float, because the agent that reads the report
7//! needs the level and the reasons to decide what to override.
8//!
9//! The spread is typed: `Confidence<Octaves>` describes a cutoff whose
10//! spread is in octaves, `Confidence<Cents>` a unison offset whose spread is
11//! in cents. An estimate that has no spread, such as a menu choice, uses
12//! [`Confidence<NoSpread>`](NoSpread).
13//!
14//! In a report a confidence is one JSON object:
15//!
16//! ```json
17//! {"level": "medium", "reasons": [{"code": "active_frames", "value": 412}], "spread": 0.3, "estimate": true}
18//! ```
19//!
20//! `spread` is left out when there is none. A "no estimate" state writes
21//! `"estimate": false`, no `level`, no `spread` and at least one reason.
22//! Deserialization applies the same rules as the constructors.
23
24use std::error::Error;
25use std::fmt;
26
27use serde::de::{self, Visitor};
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29
30use crate::units::{
31    Cents, Correlation, Decibels, Degrees, Hz, MidiPitch, Normalized, Octaves, Percent, Polarity,
32    RangeError, Ratio, Seconds, Semitones,
33};
34
35/// How far an estimator trusts its value. The order is `Low < Medium <
36/// High`, so a per-layer cap is `level.min(cap)`.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Level {
40    /// The value is a weak guess. The agent should check it first.
41    Low,
42    /// The value is likely right.
43    Medium,
44    /// The signals agree on the value.
45    High,
46}
47
48/// Defines [`ReasonCode`] and the list of all its variants together, so a
49/// new code cannot miss the list that the tests draw from.
50macro_rules! reason_codes {
51    ($($(#[$doc:meta])* $variant:ident,)+) => {
52        /// Why an estimator chose its level, or why it made no estimate.
53        ///
54        /// Each code is a stable `snake_case` identifier in the report. Each
55        /// estimator adds the codes of its own signals here.
56        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
57        #[serde(rename_all = "snake_case")]
58        pub enum ReasonCode {
59            $($(#[$doc])* $variant,)+
60        }
61
62        impl ReasonCode {
63            /// Every code, in declaration order.
64            #[cfg(test)]
65            pub(crate) const ALL: &[Self] = &[$(Self::$variant,)+];
66        }
67    };
68}
69
70reason_codes! {
71    /// The number of analysis frames with signal above the noise floor.
72    ActiveFrames,
73    /// Too few notes to fit the value.
74    TooFewNotes,
75    /// The pitch may be an octave off.
76    OctaveUncertain,
77    /// The stem is digital silence.
78    SilentStem,
79    /// The file has one channel, so no stereo value exists.
80    OneChannel,
81    /// No note list was given, so a value that needs the f0 has none.
82    NoNoteList,
83    /// The number of full cycles of a rate in the span. Below two, no rate
84    /// is reported.
85    CyclesInSpan,
86    /// The stem is flat-topped at its peak, so its level and crest are
87    /// capped by a limiter or by clipping.
88    HardLimited,
89    /// The stereo screen cannot see an anti-phase stereo chorus or a light
90    /// reverb.
91    StereoBlindSpot,
92    /// The steepest fall of the smoothed spectrum over one semitone above
93    /// 2 kHz, in dB. Below the steep-drop threshold the band edge comes from
94    /// the margin rule.
95    BandEdgeDrop,
96    /// The strongest line of a modulation scan over the median magnitude of
97    /// the scan. Below the prominence threshold no rate is reported.
98    LineProminence,
99    /// Fewer active frames than a statistic over frames needs; the value is
100    /// the minimum.
101    TooFewFrames,
102    /// No bin of the spectrum above 1 kHz rises the band-edge margin above
103    /// its floor, so the band edge is the Nyquist frequency at low
104    /// confidence.
105    NoBandEdge,
106    /// The best score of the tempo search over its median score. Below the
107    /// trust threshold the given tempo stays; 0 when the stem has no
108    /// onsets.
109    TempoPeak,
110    /// The share of the onset strength that repeats at the refined beat: the
111    /// best score of the tempo search over its lag count times the
112    /// zero-lag autocorrelation. Below the trust threshold the given tempo
113    /// stays.
114    BeatPeriodicity,
115    /// The share of the onset strength that repeats every 2 or every 4
116    /// refined beats, the better of the two, as [`Self::BeatPeriodicity`]
117    /// reads it on those lags alone. Onsets on every other beat or once a
118    /// bar read near 1. It trusts a sparse part when the beat periodicity
119    /// does not; 0 when the stem holds too few beats for it. At most 1.
120    SparsePeriodicity,
121    /// The share of the energy of the raw onset strength, before its
122    /// low-pass, within 25 ms of its peaks. Hits and plucks read near 1; a
123    /// swell, such as a pad under a slow tremolo, reads low. A refined
124    /// tempo is trusted only with sharp onsets.
125    OnsetSharpness,
126    /// The peaks of the onset strength per beat of the slowest tempo
127    /// searched. The sparse periodicity trusts a tempo only for a part
128    /// this sparse, because a dense grid, such as sixteenth notes, lines
129    /// up with any spacing that is a whole number of its steps.
130    OnsetsPerBeat,
131    /// `--bpm-exact` was given, so the tempo was not refined.
132    BpmExact,
133    /// The number of voiced frames of the f0 track.
134    VoicedFrames,
135    /// The line plays chords, so a method for one voice at a time made no
136    /// estimate.
137    PolyphonicLine,
138    /// How long a pitch motion moved faster than the glide floor, in
139    /// seconds. A short motion reads a glide's time less well.
140    MotionSpan,
141    /// The RMS residual of a fit: in cents for a glide, as a share of the
142    /// drop for a pitch drop.
143    FitResidual,
144    /// The RMS deviation of the stable pitches around a glide from their
145    /// medians, in cents. A unison beats and the track wanders, which
146    /// skews the glide's time.
147    PitchJitter,
148    /// The share of a contour's power that a fitted sine explains.
149    SineShare,
150    /// The kurtosis of a vibrato's contour after its line, the fourth
151    /// moment over the squared second: 1.5 for a sine, 1.0 for a square
152    /// wave such as a trill between two notes.
153    ContourKurtosis,
154    /// The time the band envelope of a strike takes from 10 % to 90 % of its
155    /// peak, in seconds. A slow attack hides the start of a pitch drop.
156    AttackTime,
157    /// A glide's fitted curve power sits on the upper bound of its range:
158    /// an exponential approach, read with less certainty.
159    CurveBound,
160    /// The share of the track frames inside the notes that are voiced
161    /// within 100 cents of a sounding note. Noise or DC under a note list
162    /// reads near 0.
163    NoteFrames,
164    /// The interquartile range of a value over the notes, divided by the
165    /// absolute value of its median. Above 0.5 the notes disagree too much
166    /// for a preset.
167    RelativeSpread,
168    /// The share of the pooled notes whose attack is an upper bound: its
169    /// rise is shorter than 1.5 RMS windows, or for an overlapping note
170    /// twice the rise of its band filter on an instant step.
171    UpperBoundShare,
172    /// The share of the held time whose count of voices agrees with the
173    /// verdict.
174    CountAgreement,
175    /// The stem's attack is long enough to hide a retrigger.
176    AttackHidesJoin,
177    /// No attack was read, so a retrigger it hides cannot be ruled out.
178    NoAttackRead,
179    /// The level holds through a join, which a retrigger with an instant attack
180    /// does too.
181    LevelKeptAtJoin,
182    /// The stem's edge reaches a level window beside a join, so a level is unread
183    /// or short.
184    StemEdgeAtJoin,
185    /// The tail weighs too much at the note-off for a release to be identified.
186    TailWeight,
187    /// The tail weighs too little at the note-off to show an RT60: a dry note
188    /// often leaves a little weight whose RT60 is noise.
189    TailLight,
190    /// The tail model fits too poorly for a release to be identified.
191    TailResidual,
192    /// A heavy tail fits too smoothly for a reverb: it reads as the note's own
193    /// exponential release, not as a space.
194    TailSmooth,
195    /// The tail fades faster than the note's band filter can show.
196    TailBand,
197    /// The level wobbles over the hold and the tail fits poorly, as under a
198    /// tremolo, so a written release reads low.
199    TailWobble,
200    /// The interquartile range of a value over the notes whose median is 0,
201    /// so the range has no ratio to the median. The value reads low.
202    ZeroMedian,
203}
204
205/// The value of the signal behind a [`ReasonCode`].
206///
207/// In JSON a count is an integer, a number has a decimal point, and a flag
208/// is a bool.
209#[derive(Clone, Copy, Debug, PartialEq)]
210pub enum Signal {
211    /// A count, such as a number of frames or notes.
212    Count(u64),
213    /// A measured value. [`Reason::new`] rejects NaN and both infinities.
214    Number(f64),
215    /// A yes-or-no signal, such as a saturation flag.
216    Flag(bool),
217}
218
219/// A reason code with the value of its signal.
220///
221/// ```
222/// use sound_to_patch::confidence::{Reason, ReasonCode, Signal};
223///
224/// let reason = Reason::new(ReasonCode::ActiveFrames, Signal::Count(412))?;
225/// assert!(Reason::new(ReasonCode::ActiveFrames, Signal::Number(f64::NAN)).is_err());
226/// # Ok::<(), sound_to_patch::confidence::ConfidenceError>(())
227/// ```
228#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
229#[serde(deny_unknown_fields)]
230pub struct Reason {
231    code: ReasonCode,
232    value: Signal,
233}
234
235impl Reason {
236    /// Pairs a code with its signal value.
237    ///
238    /// # Errors
239    ///
240    /// Returns a [`ConfidenceError`] for a [`Signal::Number`] that is NaN or
241    /// infinite.
242    pub fn new(code: ReasonCode, value: Signal) -> Result<Self, ConfidenceError> {
243        let value = match value {
244            Signal::Number(number) => Signal::Number(check_signal(number).map_err(Kind::Signal)?),
245            other => other,
246        };
247        Ok(Self { code, value })
248    }
249
250    /// Returns the reason code.
251    #[must_use]
252    pub fn code(self) -> ReasonCode {
253        self.code
254    }
255
256    /// Returns the signal value.
257    #[must_use]
258    pub fn value(self) -> Signal {
259        self.value
260    }
261}
262
263/// Accepts a finite signal, with `-0.0` stored as `+0.0` as the unit types
264/// store it, so the sign of a zero never reaches a report.
265fn check_signal(number: f64) -> Result<f64, RangeError> {
266    if number.is_finite() {
267        Ok(number + 0.0)
268    } else {
269        Err(RangeError::new(
270            "Reason signal",
271            number,
272            crate::units::Range::Finite,
273        ))
274    }
275}
276
277impl Serialize for Signal {
278    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
279        match *self {
280            Self::Count(count) => serializer.serialize_u64(count),
281            Self::Number(number) => serializer.serialize_f64(number),
282            Self::Flag(flag) => serializer.serialize_bool(flag),
283        }
284    }
285}
286
287impl<'de> Deserialize<'de> for Signal {
288    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
289        deserializer.deserialize_any(SignalVisitor)
290    }
291}
292
293struct SignalVisitor;
294
295impl Visitor<'_> for SignalVisitor {
296    type Value = Signal;
297
298    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        write!(f, "a count, a finite number or a bool")
300    }
301
302    fn visit_u64<E: de::Error>(self, count: u64) -> Result<Signal, E> {
303        Ok(Signal::Count(count))
304    }
305
306    // A negative integer is a number; within `i32` it converts exactly.
307    fn visit_i64<E: de::Error>(self, number: i64) -> Result<Signal, E> {
308        if let Ok(count) = u64::try_from(number) {
309            return Ok(Signal::Count(count));
310        }
311        i32::try_from(number)
312            .map(|number| Signal::Number(f64::from(number)))
313            .map_err(|_| E::invalid_value(de::Unexpected::Signed(number), &self))
314    }
315
316    fn visit_f64<E: de::Error>(self, number: f64) -> Result<Signal, E> {
317        check_signal(number).map(Signal::Number).map_err(E::custom)
318    }
319
320    fn visit_bool<E: de::Error>(self, flag: bool) -> Result<Signal, E> {
321        Ok(Signal::Flag(flag))
322    }
323}
324
325/// A unit a spread can be given in: every unit type except
326/// [`Bpm`](crate::units::Bpm), whose range excludes zero, or [`NoSpread`].
327///
328/// The trait is sealed.
329pub trait Spread: Copy + fmt::Debug + PartialEq + sealed::Spread {}
330
331/// The spread type of an estimate that has no spread, such as a menu
332/// choice. It has no values, so a `Confidence<NoSpread>` never holds a
333/// spread.
334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub enum NoSpread {}
336
337mod sealed {
338    use super::ConfidenceError;
339
340    pub trait Spread: Sized {
341        fn to_f64(self) -> f64;
342        fn from_f64(value: f64) -> Result<Self, ConfidenceError>;
343    }
344}
345
346macro_rules! spread {
347    ($($unit:ty),+) => {$(
348        impl sealed::Spread for $unit {
349            fn to_f64(self) -> f64 {
350                self.get()
351            }
352
353            fn from_f64(value: f64) -> Result<Self, ConfidenceError> {
354                Ok(Self::new(value).map_err(Kind::Spread)?)
355            }
356        }
357
358        impl Spread for $unit {}
359    )+};
360}
361
362spread!(
363    Hz,
364    Seconds,
365    Semitones,
366    Cents,
367    Octaves,
368    Decibels,
369    Normalized,
370    MidiPitch,
371    Correlation,
372    Degrees,
373    Ratio
374);
375
376impl<P: Polarity> sealed::Spread for Percent<P> {
377    fn to_f64(self) -> f64 {
378        self.get()
379    }
380
381    fn from_f64(value: f64) -> Result<Self, ConfidenceError> {
382        Ok(Self::new(value).map_err(Kind::Spread)?)
383    }
384}
385
386impl<P: Polarity> Spread for Percent<P> {}
387
388impl sealed::Spread for NoSpread {
389    fn to_f64(self) -> f64 {
390        match self {}
391    }
392
393    fn from_f64(_: f64) -> Result<Self, ConfidenceError> {
394        Err(Kind::SpreadNotAllowed.into())
395    }
396}
397
398impl Spread for NoSpread {}
399
400/// The confidence of one estimate: a level with an optional spread, or no
401/// estimate. Either state carries at least one [`Reason`].
402///
403/// The constructors keep the parts together: an estimate always has a
404/// level, and a "no estimate" state never has a level or a spread.
405///
406/// ```
407/// use sound_to_patch::confidence::{Confidence, Level, Reason, ReasonCode, Signal};
408/// use sound_to_patch::units::Octaves;
409///
410/// let frames = Reason::new(ReasonCode::ActiveFrames, Signal::Count(412))?;
411/// let cutoff = Confidence::estimate(Level::Medium, Some(Octaves::new(0.3)?), vec![frames])?;
412/// assert_eq!(cutoff.level(), Some(Level::Medium));
413///
414/// let notes = Reason::new(ReasonCode::TooFewNotes, Signal::Count(2))?;
415/// let envelope = Confidence::<Octaves>::no_estimate(vec![notes])?;
416/// assert_eq!(envelope.level(), None);
417/// # Ok::<(), Box<dyn std::error::Error>>(())
418/// ```
419///
420/// A "no estimate" state takes no level:
421///
422/// ```compile_fail,E0061
423/// use sound_to_patch::confidence::{Confidence, Level, NoSpread};
424///
425/// let _ = Confidence::<NoSpread>::no_estimate(Level::High, Vec::new());
426/// ```
427#[derive(Clone, Debug, PartialEq)]
428pub struct Confidence<S: Spread> {
429    state: State<S>,
430    reasons: Vec<Reason>,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq)]
434enum State<S> {
435    Estimate { level: Level, spread: Option<S> },
436    NoEstimate,
437}
438
439impl<S: Spread> Confidence<S> {
440    /// Builds the confidence of an estimate.
441    ///
442    /// # Errors
443    ///
444    /// Returns a [`ConfidenceError`] for an empty reason list or a
445    /// negative spread.
446    pub fn estimate(
447        level: Level,
448        spread: Option<S>,
449        reasons: Vec<Reason>,
450    ) -> Result<Self, ConfidenceError> {
451        if let Some(spread) = spread {
452            crate::units::Range::AtLeast(0.0)
453                .check("Confidence spread", spread.to_f64())
454                .map_err(Kind::Spread)?;
455        }
456        Self::with_reasons(State::Estimate { level, spread }, reasons)
457    }
458
459    /// Builds the state of a layer that had too little evidence to report a
460    /// value.
461    ///
462    /// # Errors
463    ///
464    /// Returns a [`ConfidenceError`] for an empty reason list.
465    pub fn no_estimate(reasons: Vec<Reason>) -> Result<Self, ConfidenceError> {
466        Self::with_reasons(State::NoEstimate, reasons)
467    }
468
469    fn with_reasons(state: State<S>, reasons: Vec<Reason>) -> Result<Self, ConfidenceError> {
470        if reasons.is_empty() {
471            return Err(Kind::NoReasons.into());
472        }
473        Ok(Self { state, reasons })
474    }
475
476    /// Returns `true` unless this is a "no estimate" state.
477    #[must_use]
478    pub fn is_estimate(&self) -> bool {
479        matches!(self.state, State::Estimate { .. })
480    }
481
482    /// Returns the level, or `None` for a "no estimate" state.
483    #[must_use]
484    pub fn level(&self) -> Option<Level> {
485        match self.state {
486            State::Estimate { level, .. } => Some(level),
487            State::NoEstimate => None,
488        }
489    }
490
491    /// Returns the spread, if the estimate has one.
492    #[must_use]
493    pub fn spread(&self) -> Option<S> {
494        match self.state {
495            State::Estimate { spread, .. } => spread,
496            State::NoEstimate => None,
497        }
498    }
499
500    /// Returns the reasons, at least one.
501    #[must_use]
502    pub fn reasons(&self) -> &[Reason] {
503        &self.reasons
504    }
505}
506
507#[derive(Serialize)]
508struct WireOut<'a> {
509    #[serde(skip_serializing_if = "Option::is_none")]
510    level: Option<Level>,
511    reasons: &'a [Reason],
512    #[serde(skip_serializing_if = "Option::is_none")]
513    spread: Option<f64>,
514    estimate: bool,
515}
516
517#[derive(Deserialize)]
518#[serde(deny_unknown_fields)]
519struct WireIn {
520    level: Option<Level>,
521    reasons: Vec<Reason>,
522    spread: Option<f64>,
523    estimate: bool,
524}
525
526impl<S: Spread> Serialize for Confidence<S> {
527    fn serialize<Ser: Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
528        WireOut {
529            level: self.level(),
530            reasons: &self.reasons,
531            spread: self.spread().map(sealed::Spread::to_f64),
532            estimate: self.is_estimate(),
533        }
534        .serialize(serializer)
535    }
536}
537
538impl<'de, S: Spread> Deserialize<'de> for Confidence<S> {
539    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
540        let wire = WireIn::deserialize(deserializer)?;
541        Self::from_wire(wire).map_err(de::Error::custom)
542    }
543}
544
545impl<S: Spread> Confidence<S> {
546    fn from_wire(wire: WireIn) -> Result<Self, ConfidenceError> {
547        match (wire.estimate, wire.level) {
548            (true, Some(level)) => {
549                let spread = wire.spread.map(S::from_f64).transpose()?;
550                Self::estimate(level, spread, wire.reasons)
551            }
552            (true, None) => Err(Kind::EstimateWithoutLevel.into()),
553            (false, Some(_)) => Err(Kind::LevelWithoutEstimate.into()),
554            (false, None) if wire.spread.is_some() => Err(Kind::SpreadWithoutEstimate.into()),
555            (false, None) => Self::no_estimate(wire.reasons),
556        }
557    }
558}
559
560/// A [`Confidence`] or a [`Reason`] whose parts do not fit together.
561///
562/// The message names the cause and the fix.
563#[derive(Clone, Debug, PartialEq)]
564pub struct ConfidenceError(Kind);
565
566#[derive(Clone, Debug, PartialEq)]
567enum Kind {
568    NoReasons,
569    Spread(RangeError),
570    Signal(RangeError),
571    SpreadNotAllowed,
572    EstimateWithoutLevel,
573    LevelWithoutEstimate,
574    SpreadWithoutEstimate,
575}
576
577impl From<Kind> for ConfidenceError {
578    fn from(kind: Kind) -> Self {
579        Self(kind)
580    }
581}
582
583impl fmt::Display for ConfidenceError {
584    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585        match &self.0 {
586            Kind::NoReasons => write!(
587                f,
588                "Confidence has no reasons: add at least one reason code with its signal value"
589            ),
590            Kind::Spread(error) | Kind::Signal(error) => write!(f, "{error}"),
591            Kind::SpreadNotAllowed => write!(
592                f,
593                "Confidence has a spread, but this estimate has none: remove \"spread\""
594            ),
595            Kind::EstimateWithoutLevel => write!(
596                f,
597                "Confidence with \"estimate\": true has no level: \
598                 add \"level\" or set \"estimate\": false"
599            ),
600            Kind::LevelWithoutEstimate => write!(
601                f,
602                "Confidence with \"estimate\": false has a level: \
603                 remove \"level\" or set \"estimate\": true"
604            ),
605            Kind::SpreadWithoutEstimate => write!(
606                f,
607                "Confidence with \"estimate\": false has a spread: \
608                 remove \"spread\" or set \"estimate\": true"
609            ),
610        }
611    }
612}
613
614impl Error for ConfidenceError {
615    fn source(&self) -> Option<&(dyn Error + 'static)> {
616        match &self.0 {
617            Kind::Spread(error) | Kind::Signal(error) => Some(error),
618            _ => None,
619        }
620    }
621}
622
623/// A value with its confidence. The value is present exactly when the
624/// confidence is an estimate.
625///
626/// ```
627/// use sound_to_patch::confidence::{Estimate, Level, Reason, ReasonCode, Signal};
628/// use sound_to_patch::units::Hz;
629///
630/// let frames = Reason::new(ReasonCode::ActiveFrames, Signal::Count(412))?;
631/// let centroid = Estimate::new(Hz::new(1520.0)?, Level::High, Some(Hz::new(310.0)?), vec![frames])?;
632/// assert_eq!(centroid.value().map(|hz| hz.get()), Some(1520.0));
633///
634/// let silent = Reason::new(ReasonCode::SilentStem, Signal::Flag(true))?;
635/// let none = Estimate::<Hz, Hz>::none(vec![silent])?;
636/// assert!(none.value().is_none());
637/// # Ok::<(), Box<dyn std::error::Error>>(())
638/// ```
639#[derive(Clone, Debug, PartialEq)]
640pub struct Estimate<V, S: Spread> {
641    value: Option<V>,
642    confidence: Confidence<S>,
643}
644
645impl<V, S: Spread> Estimate<V, S> {
646    /// A value with the confidence of an estimate.
647    ///
648    /// # Errors
649    ///
650    /// Returns a [`ConfidenceError`] as [`Confidence::estimate`] does.
651    pub fn new(
652        value: V,
653        level: Level,
654        spread: Option<S>,
655        reasons: Vec<Reason>,
656    ) -> Result<Self, ConfidenceError> {
657        Ok(Self {
658            value: Some(value),
659            confidence: Confidence::estimate(level, spread, reasons)?,
660        })
661    }
662
663    /// No value, and the reasons why.
664    ///
665    /// # Errors
666    ///
667    /// Returns a [`ConfidenceError`] as [`Confidence::no_estimate`] does.
668    pub fn none(reasons: Vec<Reason>) -> Result<Self, ConfidenceError> {
669        Ok(Self {
670            value: None,
671            confidence: Confidence::no_estimate(reasons)?,
672        })
673    }
674
675    /// Returns the value, or `None` for no estimate.
676    #[must_use]
677    pub fn value(&self) -> Option<&V> {
678        self.value.as_ref()
679    }
680
681    /// Returns the confidence.
682    #[must_use]
683    pub fn confidence(&self) -> &Confidence<S> {
684        &self.confidence
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691    use proptest::prelude::*;
692    use serde_json::json;
693
694    fn frames(count: u64) -> Reason {
695        Reason::new(ReasonCode::ActiveFrames, Signal::Count(count)).unwrap()
696    }
697
698    fn octaves(value: f64) -> Octaves {
699        Octaves::new(value).unwrap()
700    }
701
702    #[test]
703    fn estimate_serializes_in_the_report_layout() {
704        let confidence =
705            Confidence::estimate(Level::Medium, Some(octaves(0.3)), vec![frames(412)]).unwrap();
706        assert_eq!(
707            serde_json::to_value(&confidence).unwrap(),
708            json!({"level": "medium", "reasons": [{"code": "active_frames", "value": 412}], "spread": 0.3, "estimate": true})
709        );
710        assert_eq!(
711            serde_json::to_string(&confidence).unwrap(),
712            r#"{"level":"medium","reasons":[{"code":"active_frames","value":412}],"spread":0.3,"estimate":true}"#
713        );
714    }
715
716    #[test]
717    fn estimate_without_spread_leaves_the_field_out() {
718        let confidence = Confidence::<Cents>::estimate(Level::High, None, vec![frames(1)]).unwrap();
719        assert_eq!(
720            serde_json::to_value(&confidence).unwrap(),
721            json!({"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "estimate": true})
722        );
723    }
724
725    #[test]
726    fn no_estimate_has_no_level_no_spread_and_a_reason() {
727        let reason = Reason::new(ReasonCode::TooFewNotes, Signal::Count(2)).unwrap();
728        let confidence = Confidence::<Octaves>::no_estimate(vec![reason]).unwrap();
729        assert!(!confidence.is_estimate());
730        assert_eq!(confidence.level(), None);
731        assert_eq!(confidence.spread(), None);
732        assert_eq!(confidence.reasons(), &[reason]);
733        let value = serde_json::to_value(&confidence).unwrap();
734        assert_eq!(
735            value,
736            json!({"reasons": [{"code": "too_few_notes", "value": 2}], "estimate": false})
737        );
738        let back: Confidence<Octaves> = serde_json::from_value(value).unwrap();
739        assert_eq!(back, confidence);
740    }
741
742    #[test]
743    fn level_reasons_and_signals_stay_together() {
744        let reasons = vec![
745            frames(412),
746            Reason::new(ReasonCode::OctaveUncertain, Signal::Flag(true)).unwrap(),
747        ];
748        let confidence =
749            Confidence::estimate(Level::Low, Some(octaves(1.5)), reasons.clone()).unwrap();
750        assert!(confidence.is_estimate());
751        assert_eq!(confidence.level(), Some(Level::Low));
752        assert_eq!(confidence.spread(), Some(octaves(1.5)));
753        assert_eq!(confidence.reasons(), reasons.as_slice());
754        assert_eq!(confidence.reasons()[1].code(), ReasonCode::OctaveUncertain);
755        assert_eq!(confidence.reasons()[1].value(), Signal::Flag(true));
756    }
757
758    #[test]
759    fn every_state_needs_a_reason() {
760        let error = Confidence::<Octaves>::estimate(Level::High, None, Vec::new()).unwrap_err();
761        assert_eq!(
762            error.to_string(),
763            "Confidence has no reasons: add at least one reason code with its signal value"
764        );
765        assert!(error.source().is_none());
766        assert!(Confidence::<Octaves>::no_estimate(Vec::new()).is_err());
767        let json = json!({"reasons": [], "estimate": false});
768        assert!(serde_json::from_value::<Confidence<Octaves>>(json).is_err());
769    }
770
771    #[test]
772    fn spread_in_percent_serializes_as_a_number() {
773        let spread = Percent::<crate::units::Unipolar>::new(12.5).unwrap();
774        let confidence = Confidence::estimate(Level::Low, Some(spread), vec![frames(3)]).unwrap();
775        let value = serde_json::to_value(&confidence).unwrap();
776        assert_eq!(value["spread"], json!(12.5));
777        assert_eq!(
778            serde_json::from_value::<Confidence<_>>(value).unwrap(),
779            confidence
780        );
781    }
782
783    #[test]
784    fn negative_spread_is_rejected() {
785        let error =
786            Confidence::estimate(Level::High, Some(octaves(-0.3)), vec![frames(1)]).unwrap_err();
787        assert_eq!(
788            error.to_string(),
789            "Confidence spread out of range: got -0.3, expected a finite value of at least 0.0"
790        );
791        assert!(error.source().is_some());
792        assert!(Confidence::estimate(Level::High, Some(octaves(0.0)), vec![frames(1)]).is_ok());
793    }
794
795    #[test]
796    fn nan_and_infinite_spread_cannot_be_built() {
797        // The spread is a unit type, and every unit type rejects them.
798        assert!(Octaves::new(f64::NAN).is_err());
799        assert!(Octaves::new(f64::INFINITY).is_err());
800        let json = r#"{"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "spread": 1e999, "estimate": true}"#;
801        assert!(serde_json::from_str::<Confidence<Octaves>>(json).is_err());
802    }
803
804    #[test]
805    fn json_negative_or_out_of_range_spread_is_rejected() {
806        let json = json!({"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "spread": -0.3, "estimate": true});
807        let message = serde_json::from_value::<Confidence<Octaves>>(json)
808            .unwrap_err()
809            .to_string();
810        assert!(
811            message.contains("Confidence spread out of range"),
812            "{message}"
813        );
814        let json = json!({"level": "high", "reasons": [{"code": "active_frames", "value": 1}], "spread": 1.5, "estimate": true});
815        let message = serde_json::from_value::<Confidence<Normalized>>(json)
816            .unwrap_err()
817            .to_string();
818        assert!(message.contains("Normalized out of range"), "{message}");
819    }
820
821    #[test]
822    fn json_level_on_a_no_estimate_state_is_rejected() {
823        let json = json!({"level": "high", "reasons": [{"code": "too_few_notes", "value": 2}], "estimate": false});
824        assert_eq!(
825            serde_json::from_value::<Confidence<Octaves>>(json)
826                .unwrap_err()
827                .to_string(),
828            "Confidence with \"estimate\": false has a level: \
829             remove \"level\" or set \"estimate\": true"
830        );
831    }
832
833    #[test]
834    fn json_spread_on_a_no_estimate_state_is_rejected() {
835        let json = json!({"reasons": [{"code": "too_few_notes", "value": 2}], "spread": 0.3, "estimate": false});
836        assert_eq!(
837            serde_json::from_value::<Confidence<Octaves>>(json)
838                .unwrap_err()
839                .to_string(),
840            "Confidence with \"estimate\": false has a spread: \
841             remove \"spread\" or set \"estimate\": true"
842        );
843    }
844
845    #[test]
846    fn json_estimate_without_a_level_is_rejected() {
847        let json = json!({"reasons": [{"code": "active_frames", "value": 412}], "estimate": true});
848        assert_eq!(
849            serde_json::from_value::<Confidence<Octaves>>(json)
850                .unwrap_err()
851                .to_string(),
852            "Confidence with \"estimate\": true has no level: \
853             add \"level\" or set \"estimate\": false"
854        );
855    }
856
857    #[test]
858    fn json_spread_for_an_estimate_without_spread_is_rejected() {
859        let json = json!({"level": "low", "reasons": [{"code": "active_frames", "value": 1}], "spread": 0.3, "estimate": true});
860        assert_eq!(
861            serde_json::from_value::<Confidence<NoSpread>>(json)
862                .unwrap_err()
863                .to_string(),
864            "Confidence has a spread, but this estimate has none: remove \"spread\""
865        );
866    }
867
868    #[test]
869    fn json_unknown_field_and_missing_estimate_are_rejected() {
870        let json = json!({"level": "low", "reasons": [{"code": "active_frames", "value": 1}], "estimate": true, "score": 0.5});
871        assert!(serde_json::from_value::<Confidence<Octaves>>(json).is_err());
872        let json = json!({"level": "low", "reasons": [{"code": "active_frames", "value": 1}]});
873        assert!(serde_json::from_value::<Confidence<Octaves>>(json).is_err());
874        let json = json!({"code": "active_frames", "value": 1, "weight": 2});
875        assert!(serde_json::from_value::<Reason>(json).is_err());
876    }
877
878    #[test]
879    fn unknown_reason_code_and_level_are_rejected() {
880        assert!(serde_json::from_value::<Reason>(json!({"code": "vibes", "value": 1})).is_err());
881        assert!(serde_json::from_value::<Level>(json!("certain")).is_err());
882    }
883
884    #[test]
885    fn signals_read_from_json_by_kind() {
886        let read = |value| serde_json::from_value::<Signal>(value).unwrap();
887        assert_eq!(read(json!(412)), Signal::Count(412));
888        assert_eq!(read(json!(-3)), Signal::Number(-3.0));
889        assert_eq!(read(json!(2.5)), Signal::Number(2.5));
890        assert_eq!(read(json!(true)), Signal::Flag(true));
891        assert!(serde_json::from_value::<Signal>(json!(i64::MIN)).is_err());
892        assert_eq!(
893            serde_json::from_value::<Signal>(json!("412"))
894                .unwrap_err()
895                .to_string(),
896            "invalid type: string \"412\", expected a count, a finite number or a bool"
897        );
898        assert!(serde_json::from_str::<Signal>("1e999").is_err());
899    }
900
901    #[test]
902    fn a_signal_must_be_finite() {
903        let error = Reason::new(ReasonCode::ActiveFrames, Signal::Number(f64::NAN)).unwrap_err();
904        assert_eq!(
905            error.to_string(),
906            "Reason signal out of range: got NaN, expected a finite value"
907        );
908        assert!(error.source().is_some());
909        assert!(Reason::new(ReasonCode::ActiveFrames, Signal::Number(f64::INFINITY)).is_err());
910        assert!(Reason::new(ReasonCode::ActiveFrames, Signal::Number(-2.5)).is_ok());
911    }
912
913    #[test]
914    fn a_signal_stores_negative_zero_as_positive_zero() {
915        let reason = Reason::new(ReasonCode::ActiveFrames, Signal::Number(-0.0)).unwrap();
916        let Signal::Number(number) = reason.value() else {
917            panic!("{reason:?}");
918        };
919        assert!(number.is_sign_positive());
920        assert_eq!(
921            serde_json::to_string(&reason).unwrap(),
922            r#"{"code":"active_frames","value":0.0}"#
923        );
924        let read: Signal = serde_json::from_str("-0.0").unwrap();
925        assert!(matches!(read, Signal::Number(number) if number.is_sign_positive()));
926    }
927
928    #[test]
929    fn every_reason_code_has_a_distinct_snake_case_name() {
930        let names: Vec<String> = ReasonCode::ALL
931            .iter()
932            .map(|code| serde_json::to_string(code).unwrap())
933            .collect();
934        let mut unique = names.clone();
935        unique.sort();
936        unique.dedup();
937        assert_eq!(unique.len(), names.len());
938        assert!(names.contains(&"\"octave_uncertain\"".to_owned()));
939    }
940
941    #[test]
942    fn levels_order_low_to_high() {
943        assert!(Level::Low < Level::Medium && Level::Medium < Level::High);
944        assert_eq!(Level::High.min(Level::Low), Level::Low);
945    }
946
947    /// Finite values of both signs, subnormals and zero included.
948    fn finite() -> impl Strategy<Value = f64> {
949        prop::num::f64::NORMAL | prop::num::f64::SUBNORMAL | prop::num::f64::ZERO
950    }
951
952    fn reason() -> impl Strategy<Value = Reason> {
953        let code = prop::sample::select(ReasonCode::ALL);
954        let signal = prop_oneof![
955            any::<u64>().prop_map(Signal::Count),
956            finite().prop_map(Signal::Number),
957            any::<bool>().prop_map(Signal::Flag),
958        ];
959        (code, signal).prop_map(|(code, signal)| Reason::new(code, signal).unwrap())
960    }
961
962    fn confidence() -> impl Strategy<Value = Confidence<Cents>> {
963        let level = prop_oneof![Just(Level::Low), Just(Level::Medium), Just(Level::High)];
964        let spread = prop::option::of(finite().prop_map(|v| Cents::new(v.abs()).unwrap()));
965        let reasons = prop::collection::vec(reason(), 1..4);
966        (any::<bool>(), level, spread, reasons).prop_map(|(estimate, level, spread, reasons)| {
967            if estimate {
968                Confidence::estimate(level, spread, reasons).unwrap()
969            } else {
970                Confidence::no_estimate(reasons).unwrap()
971            }
972        })
973    }
974
975    proptest! {
976        #[test]
977        fn every_state_round_trips(confidence in confidence()) {
978            let json = serde_json::to_string(&confidence).unwrap();
979            let back: Confidence<Cents> = serde_json::from_str(&json).unwrap();
980            prop_assert_eq!(back, confidence);
981        }
982    }
983}