Skip to main content

sva_formula/
closed_form.rs

1// Concern: declares ClosedForm and its Body, as written before normalization | Non-concern: the canonical atom sum (spectral_sum/) | IO: none
2
3use crate::complex::C64;
4use crate::env::{NodeId, ParamId};
5use crate::origin::Origin;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum Var {
9    T,
10    F,
11}
12
13impl Var {
14    pub fn as_str(self) -> &'static str {
15        match self {
16            Var::T => "t",
17            Var::F => "f",
18        }
19    }
20}
21
22/// One `Var` for the whole of it: `t` and `f` never meet inside one.
23#[derive(Clone, Debug, PartialEq)]
24pub struct ClosedForm {
25    pub var: Var,
26    pub body: Body,
27    pub origin: Origin,
28}
29
30#[derive(Clone, Debug, PartialEq)]
31pub struct Part {
32    pub origin: Origin,
33    pub body: Box<Body>,
34}
35
36impl Part {
37    pub fn new(origin: Origin, body: Body) -> Part {
38        Part {
39            origin,
40            body: Box::new(body),
41        }
42    }
43
44    pub fn bare(body: Body) -> Part {
45        Part::new(Origin::UNKNOWN, body)
46    }
47}
48
49/// An infinite bound is a variant, so no sort key or hash ever sees a non-finite `f64`.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Edge {
52    NegInf,
53    At(u64),
54    PosInf,
55}
56
57/// Numeric: by bit pattern a negative bound sorts above every positive one.
58impl Ord for Edge {
59    fn cmp(&self, other: &Edge) -> std::cmp::Ordering {
60        self.value().total_cmp(&other.value())
61    }
62}
63
64impl PartialOrd for Edge {
65    fn partial_cmp(&self, other: &Edge) -> Option<std::cmp::Ordering> {
66        Some(self.cmp(other))
67    }
68}
69
70impl Edge {
71    pub fn at(x: f64) -> Edge {
72        match x {
73            x if x == f64::NEG_INFINITY => Edge::NegInf,
74            x if x == f64::INFINITY => Edge::PosInf,
75            x => Edge::At(crate::complex::canonical(x)),
76        }
77    }
78
79    pub fn value(self) -> f64 {
80        match self {
81            Edge::NegInf => f64::NEG_INFINITY,
82            Edge::At(bits) => f64::from_bits(bits),
83            Edge::PosInf => f64::INFINITY,
84        }
85    }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum Unary {
90    Sin,
91    Cos,
92    Exp,
93    Tanh,
94    Sat,
95    Abs,
96    Log,
97    Sqrt,
98    /// `step(0) = 1`, as a crop's `[l, r)` opens.
99    Step,
100}
101
102impl Unary {
103    pub fn name(self) -> &'static str {
104        match self {
105            Unary::Sin => "sin",
106            Unary::Cos => "cos",
107            Unary::Exp => "exp",
108            Unary::Tanh => "tanh",
109            Unary::Sat => "sat",
110            Unary::Abs => "abs",
111            Unary::Log => "log",
112            Unary::Sqrt => "sqrt",
113            Unary::Step => "step",
114        }
115    }
116
117    pub const ALL: &'static [Unary] = &[
118        Unary::Sin,
119        Unary::Cos,
120        Unary::Exp,
121        Unary::Tanh,
122        Unary::Sat,
123        Unary::Abs,
124        Unary::Log,
125        Unary::Sqrt,
126        Unary::Step,
127    ];
128
129    pub fn from_name(name: &str) -> Option<Unary> {
130        Unary::ALL.iter().copied().find(|u| u.name() == name)
131    }
132
133    /// A is closed under these three on an affine argument and under nothing else.
134    pub fn is_closed(self) -> bool {
135        matches!(self, Unary::Sin | Unary::Cos | Unary::Exp)
136    }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum Fold {
141    Max,
142    Min,
143    Mod,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
147pub struct IndexId(pub u32);
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum Bound {
151    Finite(i64),
152    Infinite,
153}
154
155/// A value, not an expansion: truncated once, at collapse.
156#[derive(Clone, Debug, PartialEq)]
157pub struct Series {
158    pub index: IndexId,
159    pub lo: i64,
160    pub hi: Bound,
161    pub term: Part,
162}
163
164#[derive(Clone, Copy, Debug, PartialEq)]
165pub struct Mode {
166    pub omega: f64,
167    pub tau: f64,
168    pub amp: f64,
169    pub phase: f64,
170}
171
172#[derive(Clone, Copy, Debug, PartialEq)]
173pub enum Excitation {
174    HammerPulse { f0: f64, t0: f64, contact: f64 },
175    Impulse { t0: f64 },
176}
177
178#[derive(Clone, Debug, PartialEq)]
179pub struct ModalBank {
180    pub modes: Vec<Mode>,
181    pub excite: Excitation,
182}
183
184/// Residues expand on demand, never stored, so one rational has one spelling.
185#[derive(Clone, Debug, PartialEq)]
186pub struct Rational {
187    pub zeros: Vec<C64>,
188    pub poles: Vec<C64>,
189    pub gain: C64,
190}
191
192#[derive(Clone, Debug, PartialEq)]
193pub enum Body {
194    Const(C64),
195    Line,
196    Index(IndexId),
197    /// The unit-interval hash `rand` folds to, and the phase a noise line reads.
198    Keyed {
199        seed: u64,
200        of: Part,
201    },
202    Param(ParamId),
203    Node(NodeId),
204    Add(Vec<Part>),
205    Mul(Vec<Part>),
206    Div(Part, Part),
207    Pow(Part, i32),
208    Apply(Unary, Part),
209    Fold(Fold, Vec<Part>),
210    Delta {
211        at: Part,
212        order: u16,
213    },
214    Pv(Part),
215    Shift {
216        by: f64,
217        of: Part,
218    },
219    /// `of` read at the time `at` names, which is a shift only where `at` is `t` plus a
220    /// constant.
221    Warp {
222        at: Part,
223        of: Part,
224    },
225    Deriv {
226        order: u16,
227        of: Part,
228    },
229    /// `rise` and `fall` are raised-cosine shoulders inside `[l, r)`; zero is a hard edge.
230    Crop {
231        of: Part,
232        l: Edge,
233        r: Edge,
234        rise: f64,
235        fall: f64,
236    },
237    Join(Vec<Part>),
238    Channel(Part, u8),
239    Rational(Rational),
240    Series(Box<Series>),
241    Modal(ModalBank),
242    Run(Box<crate::run::Run>),
243    Banded(Box<crate::banded::Banded>),
244}
245
246/// The subterms of one node: a walker states its recursion once, and every new variant
247/// reaches every walker at once.
248pub fn children(f: &Body) -> Vec<&Part> {
249    match f {
250        Body::Const(_)
251        | Body::Line
252        | Body::Index(_)
253        | Body::Param(_)
254        | Body::Node(_)
255        | Body::Rational(_)
256        | Body::Modal(_)
257        | Body::Run(_) => Vec::new(),
258        Body::Keyed { of, .. } => vec![of],
259        Body::Add(parts) | Body::Mul(parts) | Body::Join(parts) | Body::Fold(_, parts) => {
260            parts.iter().collect()
261        }
262        Body::Div(a, b) | Body::Warp { at: a, of: b } => vec![a, b],
263        Body::Pow(of, _)
264        | Body::Apply(_, of)
265        | Body::Pv(of)
266        | Body::Channel(of, _)
267        | Body::Delta { at: of, .. }
268        | Body::Shift { of, .. }
269        | Body::Deriv { of, .. }
270        | Body::Crop { of, .. } => vec![of],
271        Body::Series(s) => vec![&s.term],
272        Body::Banded(b) => vec![&b.series.term],
273    }
274}
275
276pub fn map_children(f: &Body, mut g: impl FnMut(&Part) -> Part) -> Body {
277    let one = |p: &Part, g: &mut dyn FnMut(&Part) -> Part| g(p);
278    match f {
279        Body::Const(_)
280        | Body::Line
281        | Body::Index(_)
282        | Body::Param(_)
283        | Body::Node(_)
284        | Body::Rational(_)
285        | Body::Modal(_)
286        | Body::Run(_) => f.clone(),
287        Body::Keyed { seed, of } => Body::Keyed {
288            seed: *seed,
289            of: one(of, &mut g),
290        },
291        Body::Add(parts) => Body::Add(parts.iter().map(g).collect()),
292        Body::Mul(parts) => Body::Mul(parts.iter().map(g).collect()),
293        Body::Join(parts) => Body::Join(parts.iter().map(g).collect()),
294        Body::Fold(op, parts) => Body::Fold(*op, parts.iter().map(g).collect()),
295        Body::Div(a, b) => Body::Div(g(a), g(b)),
296        Body::Warp { at, of } => Body::Warp {
297            at: g(at),
298            of: g(of),
299        },
300        Body::Pow(of, n) => Body::Pow(g(of), *n),
301        Body::Apply(op, of) => Body::Apply(*op, g(of)),
302        Body::Pv(of) => Body::Pv(g(of)),
303        Body::Channel(of, k) => Body::Channel(g(of), *k),
304        Body::Delta { at, order } => Body::Delta {
305            at: g(at),
306            order: *order,
307        },
308        Body::Shift { by, of } => Body::Shift { by: *by, of: g(of) },
309        Body::Deriv { order, of } => Body::Deriv {
310            order: *order,
311            of: g(of),
312        },
313        Body::Crop {
314            of,
315            l,
316            r,
317            rise,
318            fall,
319        } => Body::Crop {
320            of: g(of),
321            l: *l,
322            r: *r,
323            rise: *rise,
324            fall: *fall,
325        },
326        Body::Series(s) => Body::Series(Box::new(Series {
327            term: g(&s.term),
328            ..(**s).clone()
329        })),
330        Body::Banded(b) => {
331            let mut held = (**b).clone();
332            held.series.term = g(&b.series.term);
333            Body::Banded(Box::new(held))
334        }
335    }
336}
337
338/// `1[l,r)(u - by)` reads the free variable too, so a window moves its own bounds; a warp
339/// moves only its time.
340pub fn shift_line(f: &Body, by: f64) -> Body {
341    let walk = |p: &Part| Part::new(p.origin, shift_line(&p.body, by));
342    match f {
343        Body::Line => Body::Add(vec![
344            Part::bare(Body::Line),
345            Part::bare(Body::Const(crate::complex::C64::real(-by))),
346        ]),
347        Body::Crop {
348            of,
349            l,
350            r,
351            rise,
352            fall,
353        } => Body::Crop {
354            of: walk(of),
355            l: Edge::at(l.value() + by),
356            r: Edge::at(r.value() + by),
357            rise: *rise,
358            fall: *fall,
359        },
360        Body::Warp { at, of } => Body::Warp {
361            at: walk(at),
362            of: of.clone(),
363        },
364        Body::Node(_) => Body::Shift {
365            by,
366            of: Part::bare(f.clone()),
367        },
368        other => map_children(other, walk),
369    }
370}
371
372/// An edge holds no formula, so a window is warped whole, and so is a ref.
373pub fn read_at(f: &Body, at: &Body) -> Body {
374    read_at_with(f, at, &|_| None)
375}
376
377pub fn read_at_with(f: &Body, at: &Body, node: &dyn Fn(NodeId) -> Option<NodeId>) -> Body {
378    match f {
379        Body::Line => at.clone(),
380        Body::Node(id) if let Some(moved) = node(*id) => Body::Node(moved),
381        Body::Crop { .. } | Body::Node(_) => Body::Warp {
382            at: Part::bare(at.clone()),
383            of: Part::bare(f.clone()),
384        },
385        Body::Warp { at: inner, of } => Body::Warp {
386            at: Part::new(inner.origin, read_at_with(&inner.body, at, node)),
387            of: of.clone(),
388        },
389        other => map_children(other, |p| {
390            Part::new(p.origin, read_at_with(&p.body, at, node))
391        }),
392    }
393}
394
395/// A leaf substitution cannot walk into: the free variable hides behind a name.
396pub fn shifts_opaquely(f: &Body) -> bool {
397    matches!(
398        f,
399        Body::Param(_) | Body::Rational(_) | Body::Modal(_) | Body::Run(_) | Body::Banded(_)
400    ) || children(f).iter().any(|p| shifts_opaquely(&p.body))
401}