Skip to main content

geodesy/inner_op/
stack.rs

1//! Stack functionality for pipelines (push/pop/swap)
2use crate::authoring::*;
3
4// NOTE: roll and drop are not implemented yet
5#[rustfmt::skip]
6pub const STACK_GAMUT: [OpParameter; 7] = [
7    OpParameter::Series  { key: "push", default: Some("") },
8    OpParameter::Series  { key: "pop",  default: Some("") },
9    OpParameter::Series  { key: "roll", default: Some("") },
10    OpParameter::Series  { key: "unroll", default: Some("") },
11    OpParameter::Series  { key: "flip", default: Some("") },
12    OpParameter::Flag    { key: "swap" },
13    OpParameter::Flag    { key: "drop" },
14];
15
16/// Construct a new stack operator. Check the syntax and semantics
17pub fn new(parameters: &RawParameters, _ctx: &dyn Context) -> Result<Op, Error> {
18    let def = &parameters.definition;
19    let mut params = ParsedParameters::new(parameters, &STACK_GAMUT)?;
20
21    // The subcommands (push, pop, roll, swap, drop) are mutually exclusive,
22    // so we count them and err if more than one is given
23    let mut subcommands_given: usize = 0;
24
25    // The arguments to push and pop are specified as a series, but Geodesy
26    // series are represented internally as a Vec<f64>, so the valid
27    // coordinate indices (1..4, i.e. the max coordinate dimensionality)
28    // are also stored as f64, to simplify comparison
29    let valid_indices = [1., 2., 3., 4.];
30
31    // Now do a sanity check for all subcommands
32
33    if let Ok(push_args) = params.series("push") {
34        subcommands_given += 1;
35        for i in push_args.iter() {
36            if !valid_indices.contains(i) {
37                return Err(Error::BadParam("push".to_string(), i.to_string()));
38            }
39        }
40        params.text.insert("action", "push".to_string());
41    }
42
43    if let Ok(flip_args) = params.series("flip") {
44        subcommands_given += 1;
45        for i in flip_args.iter() {
46            if !valid_indices.contains(i) {
47                return Err(Error::BadParam("flip".to_string(), i.to_string()));
48            }
49        }
50        params.text.insert("action", "flip".to_string());
51    }
52
53    if let Ok(pop_args) = params.series("pop") {
54        subcommands_given += 1;
55        for i in pop_args.iter() {
56            if !valid_indices.contains(i) {
57                return Err(Error::BadParam("pop".to_string(), i.to_string()));
58            }
59        }
60        params.text.insert("action", "pop".to_string());
61    }
62
63    if let Ok(roll_args) = params.series("roll") {
64        subcommands_given += 1;
65        if roll_args.len() != 2
66            || roll_args[0].fract() != 0.
67            || roll_args[1].fract() != 0.
68            || roll_args[0] <= roll_args[1].abs()
69        {
70            return Err(Error::MissingParam(
71                "roll takes exactly two integer parameters, ´(m,n): |n|<=m´".to_string(),
72            ));
73        }
74        params.text.insert("action", "roll".to_string());
75    }
76
77    if let Ok(roll_args) = params.series("unroll") {
78        subcommands_given += 1;
79        if roll_args.len() != 2
80            || roll_args[0].fract() != 0.
81            || roll_args[1].fract() != 0.
82            || roll_args[0] <= roll_args[1].abs()
83        {
84            return Err(Error::MissingParam(
85                "unroll takes exactly two integer parameters, ´(m,n): |n|<=m´".to_string(),
86            ));
87        }
88        params.text.insert("action", "unroll".to_string());
89    }
90
91    if params.boolean("swap") {
92        subcommands_given += 1;
93        params.text.insert("action", "swap".to_string());
94    }
95
96    if params.boolean("drop") {
97        subcommands_given += 1;
98        params.text.insert("action", "drop".to_string());
99    }
100
101    if subcommands_given != 1 {
102        return Err(Error::MissingParam(
103            "stack: must specify exactly one of push/pop/roll/swap/unroll/drop".to_string(),
104        ));
105    }
106
107    // The true action is handled by 'pipeline', so the `InnerOp`s are placeholders
108    let descriptor = OpDescriptor::new(def, InnerOp::default(), Some(InnerOp::default()));
109    let steps = Vec::new();
110    let id = OpHandle::new();
111
112    Ok(Op {
113        descriptor,
114        params,
115        steps,
116        id,
117    })
118}
119
120/// Called by `pipeline_fwd` to execute stack operations in forward mode
121pub(super) fn stack_fwd(
122    stack: &mut Vec<Vec<f64>>,
123    operands: &mut dyn CoordinateSet,
124    params: &ParsedParameters,
125) -> usize {
126    let Some(action) = params.text.get("action") else {
127        return 0;
128    };
129
130    let successes = match action.as_str() {
131        "push" => {
132            let args = params.series_as_usize("push").unwrap();
133            stack_push(stack, operands, &args)
134        }
135
136        "pop" => {
137            let args = params.series_as_usize("pop").unwrap();
138            stack_pop(stack, operands, &args)
139        }
140
141        "roll" => {
142            let args = params.series_as_i64("roll").unwrap();
143            stack_roll(stack, operands, &args)
144        }
145
146        "unroll" => {
147            let mut args = params.series_as_i64("unroll").unwrap();
148            args[1] = args[0] - args[1];
149            stack_roll(stack, operands, &args)
150        }
151
152        "flip" => {
153            let args = params.series_as_usize("flip").unwrap();
154            stack_flip(stack, operands, &args)
155        }
156
157        "swap" => {
158            let n = stack.len();
159            if n > 1 {
160                stack.swap(n - 1, n - 2)
161            }
162            if n == 0 {
163                0
164            } else {
165                stack[0].len()
166            }
167        }
168
169        _ => 0,
170    };
171
172    successes
173}
174
175/// Called by `pipeline_inv` to execute stack operations in inverse mode.
176/// Inverse mode has two major differences from forward: push and pop switches
177/// functionality, and their argument order swaps direction
178pub(super) fn stack_inv(
179    stack: &mut Vec<Vec<f64>>,
180    operands: &mut dyn CoordinateSet,
181    params: &ParsedParameters,
182) -> usize {
183    let Some(action) = params.text.get("action") else {
184        return 0;
185    };
186
187    let successes = match action.as_str() {
188        // An inverse push is a pop with reversed args
189        "push" => {
190            let mut args = params.series_as_usize("push").unwrap();
191            args.reverse();
192            stack_pop(stack, operands, &args)
193        }
194
195        // And an inverse pop is a push with reversed args
196        "pop" => {
197            let mut args = params.series_as_usize("pop").unwrap();
198            args.reverse();
199            stack_push(stack, operands, &args)
200        }
201
202        "roll" => {
203            let mut args = params.series_as_i64("roll").unwrap();
204            args[1] = args[0] - args[1];
205            stack_roll(stack, operands, &args)
206        }
207
208        "unroll" => {
209            let args = params.series_as_i64("roll").unwrap();
210            stack_roll(stack, operands, &args)
211        }
212
213        "flip" => {
214            let args = params.series_as_usize("flip").unwrap();
215            stack_flip(stack, operands, &args)
216        }
217
218        // Swap TOS and 2OS
219        "swap" => {
220            let n = stack.len();
221            if n > 1 {
222                stack.swap(n - 1, n - 2)
223            }
224            if n == 0 {
225                0
226            } else {
227                stack[0].len()
228            }
229        }
230
231        _ => 0,
232    };
233
234    successes
235}
236
237/// Push elements from a CoordinateSet onto the stack
238fn stack_push(
239    stack: &mut Vec<Vec<f64>>,
240    operands: &mut dyn CoordinateSet,
241    args: &[usize],
242) -> usize {
243    let number_of_pushes = args.len();
244    let number_of_operands = operands.len();
245
246    // Make room for the new sets of elements to be pushed on to the stack
247    let mut ext = vec![vec![0f64; number_of_operands]; number_of_pushes];
248
249    // Extract the coordinate elements into the new stack elements
250    for i in 0..number_of_operands {
251        let coord = operands.get_coord(i);
252        for j in 0..number_of_pushes {
253            // args are 1 based so we adjust
254            ext[j][i] = coord[args[j] - 1];
255        }
256    }
257
258    // And push them onto the existing stack
259    stack.extend(ext);
260    number_of_operands
261}
262
263/// Flip the operator and the TOS
264fn stack_flip(stack: &mut [Vec<f64>], operands: &mut dyn CoordinateSet, args: &[usize]) -> usize {
265    let number_of_flips = args.len();
266    let number_of_operands = operands.len();
267    let stack_depth = stack.len();
268
269    // In case of underflow, we stomp on all input coordinates
270    if stack_depth < number_of_flips {
271        warn!("Stack flip underflow in pipeline");
272        operands.stomp();
273        return 0;
274    }
275
276    // Swap the stack elements and their corresponding coordinate elements
277    for i in 0..number_of_operands {
278        let mut coord = operands.get_coord(i);
279        for j in 0..number_of_flips {
280            // args are 1 based so we adjust
281            let flip = coord[args[j] - 1];
282            let depth = stack_depth - 1 - j;
283            coord[args[j] - 1] = stack[depth][i];
284            stack[depth][i] = flip;
285        }
286        operands.set_coord(i, &coord);
287    }
288
289    number_of_operands
290}
291
292/// roll m,n: On the sub-stack consisting of the m upper elements,
293/// roll n elements from the top, to the bottom of the sub-stack.
294/// Hence, roll is a "big swap", essentially swapping the n upper
295/// elements with the m - n lower.
296fn stack_roll(stack: &mut Vec<Vec<f64>>, operands: &mut dyn CoordinateSet, args: &[i64]) -> usize {
297    let m = args[0].abs();
298    let mut n = args[1];
299    let depth = stack.len();
300
301    // Negative n: count the number of rolled elements from the bottom,
302    // i.e. roll 3,-2 = roll 3,1
303    n = if n < 0 { m + n } else { n };
304
305    // The remaining becomes simpler if m, n and depth are all usize
306    let m = m as usize;
307    let n = n as usize;
308
309    if m > depth {
310        warn!("Roll too deep");
311        operands.stomp();
312        return 0;
313    }
314
315    for _ in 0..n {
316        let e = stack.pop().unwrap();
317        stack.insert(depth - m, e);
318    }
319
320    operands.len()
321}
322
323/// Pop elements from the stack into elements of a CoordinateSet
324fn stack_pop(stack: &mut Vec<Vec<f64>>, operands: &mut dyn CoordinateSet, args: &[usize]) -> usize {
325    let number_of_pops = args.len();
326    let number_of_operands = operands.len();
327    let stack_depth = stack.len();
328
329    // In case of underflow, we stomp on all input coordinates
330    if stack_depth < number_of_pops {
331        warn!("Stack underflow in pipeline");
332        operands.stomp();
333        return 0;
334    }
335
336    // Remove the correct number of elements and obtain a reversed version.
337    let mut ext = Vec::with_capacity(number_of_pops);
338    for _ in args {
339        ext.push(stack.pop().unwrap());
340    }
341
342    // Inject the required stack elements into the proper
343    // positions of the coordinate elements
344    for i in 0..number_of_operands {
345        let mut coord = operands.get_coord(i);
346        for j in 0..number_of_pops {
347            // args are 1 based so we adjust
348            coord[args[j] - 1] = ext[j][i];
349        }
350        operands.set_coord(i, &coord);
351    }
352
353    number_of_operands
354}
355
356// ----- T E S T S ---------------------------------------------------------------------
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn stack() -> Result<(), Error> {
364        let mut ctx = Minimal::default();
365        let master_data = vec![Coor4D([11., 12., 13., 14.]), Coor4D([21., 22., 23., 24.])];
366
367        // ----- Three initial sanity checks -----
368
369        // Yes, we may push any number of elements onto the stack. And
370        // no, I do not see any immediate actual uses for this, but
371        // disallowing it would require more code, more complicated code,
372        // all for no gain other than stomping on a potential future use
373        // case
374        assert!(ctx.op("stack push=2,2,1,1,3,3,4,4,4,4,4,4,4").is_ok());
375
376        // But we must not have more than one subcommand for each
377        // stack operator
378        assert!(ctx.op("stack push=2,2,1,1 pop=1,1,2").is_err());
379
380        // ...while in two consecutive steps it works as it should
381        // (the push/pop-imbalance is not an error. Again a potential
382        // use case, that would require code complication to disallow)
383        assert!(ctx.op("stack push=2,2,1,1 | stack pop=1,1,2").is_ok());
384
385        // ----- Four tests of the actual functionality -----
386
387        let mut data = master_data.clone();
388
389        // 1: Swap the first and second coordinate dimensions by a push/pop dance
390
391        // Explanation:
392        // The first step pushes the first and second coordinate of
393        // the operand onto the stack **in that order**.
394        // Hence,
395        // - The second coordinate becomes top-of-stack (TOS), as
396        //   it is the last one pushed, and
397        // - The first coordinate becomes second-of-stack (2OS)
398        //
399        // The second step pops the TOS into the first coordinate of
400        // the operand, and the 2OS into the first coordinate of the
401        // operand
402        let op = ctx.op("stack push=1,2|stack pop=1,2")?;
403        ctx.apply(op, Fwd, &mut data)?;
404        assert_eq!(data[0][0], 12.);
405        assert_eq!(data[1][1], 21.);
406
407        // Then we do the inverse
408        ctx.apply(op, Inv, &mut data)?;
409        assert_eq!(data[0], master_data[0]);
410        assert_eq!(data[1], master_data[1]);
411
412        // 2: An exercise in reverse thinking - doing the inverse call first
413
414        let op = ctx.op("stack push=2,1 | stack pop=2,1")?;
415        // The inverse call should in effect execute "stack push=1,2 | stack pop=1,2"
416        ctx.apply(op, Inv, &mut data)?;
417        assert_eq!(data[0][0], 12.);
418        assert_eq!(data[1][1], 21.);
419
420        // Then we do the inverse
421        ctx.apply(op, Fwd, &mut data)?;
422        assert_eq!(data[0], master_data[0]);
423        assert_eq!(data[1], master_data[1]);
424
425        // 3: Test the `swap` subcommand
426
427        let op = ctx.op("stack push=2,1 | stack swap | stack pop=1,2")?;
428        ctx.apply(op, Fwd, &mut data)?;
429        assert_eq!(data[0][0], 12.);
430        assert_eq!(data[1][1], 21.);
431
432        // Then we do the inverse
433        ctx.apply(op, Inv, &mut data)?;
434        assert_eq!(data[0], master_data[0]);
435        assert_eq!(data[1], master_data[1]);
436
437        // 4: Test the `roll` subcommand
438        let op = ctx.op("stack push=1,1,1,2,1,3,1,4 | stack roll=8,2 | stack pop=1,2")?;
439        ctx.apply(op, Fwd, &mut data)?;
440        assert_eq!(data[0][0], 13.);
441        assert_eq!(data[0][1], 11.);
442
443        // Then we do the inverse. We must, however, redo, since the push-pop asymmetry
444        // would otherwise wreak havoc:
445
446        // Just calling apply in the inverse direction leads to underflow:
447        assert_eq!(0, ctx.apply(op, Inv, &mut data)?);
448
449        // Instead, we must substitute (m,n) with (m,m-n)
450        let mut data = master_data.clone();
451        let op = ctx.op("stack push=1,2,3,4,1,2,3,4 | stack roll=8,6 | stack pop=1,2")?;
452        ctx.apply(op, Fwd, &mut data)?;
453        assert_eq!(data[0][0], 12.);
454        assert_eq!(data[0][1], 11.);
455
456        let mut data = master_data.clone();
457        let op = ctx.op("stack push=1,2,3,4,1,2,3,4 | stack roll=3,2 | stack pop=1,2")?;
458        ctx.apply(op, Fwd, &mut data)?;
459        assert_eq!(data[0][0], 12.);
460        assert_eq!(data[0][1], 14.);
461
462        let mut data = master_data.clone();
463        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,-2 | stack pop=2,1")?;
464        ctx.apply(op, Fwd, &mut data)?;
465        assert_eq!(data[0][0], 12.);
466        assert_eq!(data[0][1], 13.);
467
468        // Roundrip roll
469        let mut data = master_data.clone();
470        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,2 | stack roll=3,1 | stack pop=1,2")?;
471        ctx.apply(op, Fwd, &mut data)?;
472        assert_eq!(data[0][0], 14.);
473        assert_eq!(data[0][1], 13.);
474
475        // Roundrip roll using the unroll syntactic sugar
476        let mut data = master_data.clone();
477        let op =
478            ctx.op("stack push=1,2,3,4 | stack roll=3,2 | stack unroll=3,2 | stack pop=1,2")?;
479        ctx.apply(op, Fwd, &mut data)?;
480        assert_eq!(data[0][0], 14.);
481        assert_eq!(data[0][1], 13.);
482
483        Ok(())
484    }
485
486    #[test]
487    fn stack_examples_from_rumination_002() -> Result<(), Error> {
488        let mut ctx = Minimal::default();
489        let master_data = vec![Coor4D([1., 2., 3., 4.])];
490
491        // Roll
492        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,2 | stack pop=4,3,2,1")?;
493        let mut data = master_data.clone();
494        ctx.apply(op, Fwd, &mut data)?;
495        assert_eq!(data[0].0, [1., 3., 4., 2.]);
496
497        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,-2 | stack pop=4,3,2,1")?;
498        let mut data = master_data.clone();
499        ctx.apply(op, Fwd, &mut data)?;
500        assert_eq!(data[0].0, [1., 4., 2., 3.]);
501
502        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,2 | stack pop=4,3,2,1")?;
503        let mut data = master_data.clone();
504        ctx.apply(op, Fwd, &mut data)?;
505        assert_eq!(data[0].0, [1., 3., 4., 2.]);
506        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,1 | stack pop=4,3,2,1")?;
507        ctx.apply(op, Fwd, &mut data)?;
508        assert_eq!(data[0].0, [1., 2., 3., 4.]);
509
510        // Unroll
511        let op = ctx.op("stack push=1,2,3,4 | stack unroll=3,2 | stack pop=4,3,2,1")?;
512        let mut data = master_data.clone();
513        ctx.apply(op, Fwd, &mut data)?;
514        assert_eq!(data[0].0, [1., 4., 2., 3.]);
515
516        let op = ctx.op("stack push=1,2,3,4 | stack unroll=3,-2 | stack pop=4,3,2,1")?;
517        let mut data = master_data.clone();
518        ctx.apply(op, Fwd, &mut data)?;
519        assert_eq!(data[0].0, [1., 3., 4., 2.]);
520
521        let op = ctx.op("stack push=1,2,3,4 | stack unroll=3,2 | stack pop=4,3,2,1")?;
522        ctx.apply(op, Fwd, &mut data)?;
523        assert_eq!(data[0].0, [1., 2., 3., 4.]);
524
525        let op = ctx.op("stack push=1,2,3,4 | stack roll=3,2 | stack pop=4,3,2,1")?;
526        ctx.apply(op, Fwd, &mut data)?;
527        assert_eq!(data[0].0, [1., 3., 4., 2.]);
528
529        let op = ctx.op("stack push=1,2,3,4 | stack unroll=3,2 | stack pop=4,3,2,1")?;
530        ctx.apply(op, Fwd, &mut data)?;
531        assert_eq!(data[0].0, [1., 2., 3., 4.]);
532
533        let op = ctx.op("stack push=1,2,3,4 | helmert x=4 y=4 z=4 | stack flip=1,2")?;
534        ctx.apply(op, Fwd, &mut data)?;
535        assert_eq!(data[0].0, [4., 3., 7., 4.]);
536
537        let mut data = master_data.clone();
538        let op = ctx.op(
539            "stack push=1,2,3,4 | helmert translation=4,4,4 | stack flip=1,2 | stack flip=1,2",
540        )?;
541        ctx.apply(op, Fwd, &mut data)?;
542        assert_eq!(data[0].0, [5., 6., 7., 4.]);
543
544        Ok(())
545    }
546}