nicompiler_backend/
instruction.rs

1//! Provides definitions and implementations for instruction-related functionalities.
2//!
3//! ## Main Structures and Enumerations:
4//!
5//! - [`InstrType`]: An enumeration that defines the types of instructions supported, including `CONST` for constant values and `SINE` for sinusoidal waves.
6//!
7//! - [`Instruction`]: Represents a general instruction composed of a type (`InstrType`) and a set of arguments (`InstrArgs`). It offers methods for creating specific instruction types conveniently and for evaluating them.
8//!
9//! - [`InstrBook`]: Manages an instruction along with its associated metadata during the experiment editing phase, capturing details like the defined interval and whether to retain a value after the defined interval.
10//!
11//! ## Utilities:
12//!
13//! - The `InstrArgs` type alias provides a convenient way to define instruction arguments using a dictionary with string keys and float values.
14//!
15//! - The module makes use of the `maplit` crate to enable easy creation of hashmaps.
16//!
17//! ## Features:
18//!
19//! - Easy creation of instruction objects with utility methods such as `new_const` and `new_sine`.
20//! - Ability to evaluate instructions and in-place populate given time array views with the resulting float-point values.
21//! - Support for default values in instructions, allowing for flexibility and ease of use.
22
23use std::cmp::Ordering;
24use std::collections::HashMap;
25use std::f64::consts::PI;
26use std::fmt;
27
28use maplit::hashmap;
29
30/// Type alias for instruction arguments: a dictionary with key-value pairs of
31/// string (argument name) and float (value)
32pub type InstrArgs = HashMap<String, f64>;
33
34/// Enum type for different instructions. Supported instructions: `CONST`, `SINE`
35#[derive(Clone, PartialEq)]
36pub enum InstrType {
37    CONST,
38    SINE,
39}
40impl fmt::Display for InstrType {
41    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
42        write!(
43            f,
44            "{}",
45            match self {
46                InstrType::CONST => "CONST",
47                InstrType::SINE => "SINE",
48            }
49        )
50    }
51}
52
53// / This function uses [`other_function`] to ...
54// /
55// / [`other_function`]: ./path/to/other/function
56
57// Instruction struct consists of instr_type (enumerated type) and argument dictionary
58/// Struct for a general instruction, consisting of type and arguments.
59///
60/// Different instruction types expects different fields in their argument dictionary.
61/// Behavior for minimally expected keys are defined in `Instruction::new`, behavior of
62/// default values are defined in `Instruction::eval_inplace`.
63///
64/// ## Implemented instruction types and their expected fields:
65/// 1. `InstrType::CONST`:
66///    - `const`
67/// 2. `InstrType::SINE`:
68///    - `freq`
69///    - `amplitude`: Default is `1.0`
70///    - `offset`: Default is `0.0`
71///    - `phase`: Default is `0.0`
72///
73#[derive(Clone, PartialEq)]
74pub struct Instruction {
75    pub instr_type: InstrType,
76    pub args: InstrArgs,
77}
78impl Instruction {
79    /// Constructs an `Instruction` object.
80    ///
81    /// This method serves as the foundational constructor upon which custom constructor
82    /// wrappers for new instructions should be built. For each instruction type,
83    /// it ensures that the `args` dictionary contains the required keys.
84    ///
85    /// Missing keys will cause a panic.
86    ///
87    /// # Examples
88    ///
89    /// Constructing a new `CONST` instruction
90    /// (this is effectively the underlying implementation for [`Instruction::new_const`],
91    /// the more convenient constructor):
92    ///
93    /// ```
94    /// use nicompiler_backend::instruction::*;
95    /// use std::collections::HashMap;
96    ///
97    /// let mut const_args = InstrArgs::new();
98    /// const_args.insert("value".to_string(), 1.0);
99    /// let const_instr = Instruction::new(InstrType::CONST, const_args);
100    /// ```
101    ///
102    /// If you fail to provide the required argument fields, it will panic:
103    ///
104    /// ```should_panic
105    /// # use nicompiler_backend::instruction::*;
106    /// # use std::collections::HashMap;
107    /// let mut const_args = InstrArgs::new();
108    /// let const_instr = Instruction::new(InstrType::CONST, const_args);
109    /// ```
110    ///
111    /// The panic message will be:
112    /// `thread 'main' panicked at 'Expected instr type CONST to contain key value'`.
113    ///
114    /// Constructing a new `SINE` instruction:
115    ///
116    /// ```
117    /// # use nicompiler_backend::instruction::*;
118    /// # use std::collections::HashMap;
119
120    /// let mut sine_args = InstrArgs::new();
121    /// sine_args.insert("freq".to_string(), 10.0);
122    /// sine_args.insert("offset".to_string(), 1.0); // amplitude and phase will use default values
123    /// let sine_instr = Instruction::new(InstrType::SINE, sine_args);
124    /// ```
125    pub fn new(instr_type: InstrType, args: InstrArgs) -> Self {
126        let panic_no_key = |key| {
127            if !args.contains_key(key) {
128                panic!("Expected instr type {} to contain key {}", instr_type, key)
129            }
130        };
131        match instr_type {
132            InstrType::CONST => panic_no_key("value"),
133            InstrType::SINE => panic_no_key("freq"),
134        };
135        Instruction { instr_type, args }
136    }
137
138    /// Evaluates the instruction and populates the given array view with float-point values.
139    ///
140    /// This method takes a mutable array view (`t_arr`) and modifies its values in-place based on the instruction type and its arguments.
141    ///
142    /// - For `InstrType::CONST`, the array will be filled with the constant value specified by the `value` argument.
143    /// - For `InstrType::SINE`, a sinusoidal waveform is generated using the arguments `freq`, `amplitude`, `offset`, and `phase`. Default values are used if certain arguments are not provided.
144    ///
145    /// # Arguments
146    ///
147    /// * `t_arr` - A mutable 1D array view that will be populated with the evaluated values.
148    ///
149    /// # Examples
150    ///
151    /// Given an instruction set with a constant and a sine instruction, and an array representing time values from 0 to 1:
152    ///
153    /// ```
154    /// use ndarray::{Array2, Array1};
155    /// use nicompiler_backend::instruction::*;
156    ///
157    /// let t_row = ndarray::Array1::linspace(0.0, 1.0, 10);
158    /// let mut t_values = ndarray::stack(ndarray::Axis(0), &[t_row.view(), t_row.view()]).unwrap();
159    /// // Use wrappers to create sine and constant instructions same as the examples above
160    /// let const_instr = Instruction::new_const(1.0);
161    /// const_instr.eval_inplace(&mut t_values.row_mut(0));
162    ///
163    /// let sine_instr = Instruction::new_sine(10.0, None, None, Some(1.0));
164    /// sine_instr.eval_inplace(&mut t_values.row_mut(1));
165    /// assert!(t_values[[0, 0]] == 1. && t_values[[0, 1]] == 1.);
166    /// ```
167    pub fn eval_inplace(&self, t_arr: &mut ndarray::ArrayViewMut1<f64>) {
168        match self.instr_type {
169            InstrType::CONST => {
170                let value = *self.args.get("value").unwrap();
171                t_arr.fill(value);
172            }
173            InstrType::SINE => {
174                let freq = *self.args.get("freq").unwrap();
175                // Default values can be set by default with unwrap_or
176                let amplitude = *self.args.get("amplitude").unwrap_or(&1.0);
177                let offset = *self.args.get("offset").unwrap_or(&0.0);
178                let phase = *self.args.get("phase").unwrap_or(&0.0);
179
180                t_arr.map_inplace(|t| {
181                    *t = (2.0 * PI * freq * (*t) + phase).sin() * amplitude + offset
182                });
183            }
184        }
185    }
186
187    /// Wrapper for conveniently creating new constant instructions.
188    /// Example usage equivalent to the constant example above:
189    /// ```
190    /// # use nicompiler_backend::instruction::*;
191    /// let const_instr = Instruction::new_const(1.0);
192    /// ```
193    pub fn new_const(value: f64) -> Instruction {
194        Instruction::new(InstrType::CONST, hashmap! {String::from("value") => value})
195    }
196
197    /// Constructs a new sine instruction with provided parameters.
198    ///
199    /// Allows for convenient creation of sine instructions by specifying the frequency and optionally, amplitude, phase, and DC offset. Unspecified parameters will not be included in the instruction's argument dictionary, allowing for default values to be used elsewhere if necessary.
200    ///
201    /// # Arguments
202    ///
203    /// - `freq`: The frequency of the sine wave.
204    /// - `amplitude`: Optional amplitude of the sine wave. If `None`, it will not be set in the instruction.
205    /// - `phase`: Optional phase offset of the sine wave in radians. If `None`, it will not be set in the instruction.
206    /// - `dc_offset`: Optional DC offset for the sine wave. If `None`, it will not be set in the instruction.
207    ///
208    /// # Examples
209    ///
210    /// Constructing a sine instruction with a specified frequency, and DC offset. Amplitude and phase will use any default values defined elsewhere:
211    ///
212    /// ---
213    /// # use nicompiler_backend::instruction::*;
214    ///
215    /// let sine_instr = Instruction::new_sine(10.0, None, None, Some(1.0));
216    /// ---
217    ///
218    pub fn new_sine(
219        freq: f64,
220        amplitude: Option<f64>,
221        phase: Option<f64>,
222        dc_offset: Option<f64>,
223    ) -> Instruction {
224        let mut instr_args: InstrArgs = hashmap! {"freq".to_string() => freq};
225        // For each optional argument, if specified, insert into dictionary
226        [
227            ("amplitude", amplitude),
228            ("phase", phase),
229            ("offset", dc_offset),
230        ]
231        .iter()
232        .for_each(|(key, opt_value)| {
233            if let Some(value) = *opt_value {
234                instr_args.insert(key.to_string(), value);
235            }
236        });
237        Instruction::new(InstrType::SINE, instr_args)
238    }
239}
240impl fmt::Display for Instruction {
241    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
242        let args_string = self
243            .args
244            .iter()
245            .map(|(k, v)| format!("{}: {}", k, v))
246            .collect::<Vec<String>>()
247            .join(", ");
248        write!(f, "[{}, {{{}}}]", self.instr_type, args_string)
249    }
250}
251
252/// Manages an instruction along with its associated metadata during experiment editing.
253///
254/// The `InstrBook` struct captures the following metadata:
255/// - Defined interval using `start_pos` and `end_pos`.
256/// - A flag, `keep_val`, to determine whether to retain the value after the defined interval.
257///
258/// For the instruction interval:
259/// - `start_pos` is inclusive.
260/// - `end_pos` is exclusive.
261///
262/// `InstrBook` implements ordering based on `start_pos` to facilitate sorting.
263/// editing phase: defined interval, and whether to keep value after defined interval.
264/// For instruction interval, `start_pos` is inclusive while `end_pos` is exclusive.
265/// We implemented ordering for `InstrBook` to allow sorting based on `start_pos`.
266///
267pub struct InstrBook {
268    pub start_pos: usize,
269    pub end_pos: usize,
270    pub keep_val: bool,
271    pub instr: Instruction,
272}
273impl InstrBook {
274    /// Constructs a new `InstrBook` object.
275    ///
276    /// Checks that `end_pos` is strictly greater than `start_pos`.
277    ///
278    /// # Arguments
279    ///
280    /// - `start_pos`: Starting position (inclusive).
281    /// - `end_pos`: Ending position (exclusive).
282    /// - `keep_val`: Flag to determine if value should be retained after the interval.
283    /// - `instr`: The associated instruction.
284    ///
285    /// # Examples
286    ///
287    /// Constructing a valid `InstrBook`:
288    ///
289    /// ```
290    /// # use nicompiler_backend::instruction::*;
291    /// let instruction = Instruction::new(InstrType::CONST, [("value".to_string(), 1.0)].iter().cloned().collect());
292    /// let book = InstrBook::new(0, 5, true, instruction);
293    /// ```
294    ///
295    /// Attempting to construct an `InstrBook` with `end_pos` not greater than `start_pos` will panic:
296    ///
297    /// ```should_panic
298    /// # use nicompiler_backend::instruction::*;
299    /// let instruction = Instruction::new(InstrType::CONST, [("value".to_string(), 1.0)].iter().cloned().collect());
300    /// let book = InstrBook::new(5, 5, true, instruction);
301    /// ```
302    ///
303    /// The panic message will be:
304    /// `Instruction { /* ... */ } end_pos 5 should be strictly greater than start_pos 5`.
305    pub fn new(start_pos: usize, end_pos: usize, keep_val: bool, instr: Instruction) -> Self {
306        assert!(
307            end_pos > start_pos,
308            "Instruction {} end_pos {} should be strictly greater than start_pos {}",
309            instr,
310            end_pos,
311            start_pos
312        );
313        InstrBook {
314            start_pos,
315            end_pos,
316            keep_val,
317            instr,
318        }
319    }
320}
321// Support total ordering for InstrBook
322impl Ord for InstrBook {
323    fn cmp(&self, other: &Self) -> Ordering {
324        // We reverse the order to make BinaryHeap a min-heap based on start_pos
325        self.start_pos.cmp(&other.start_pos)
326    }
327}
328impl PartialOrd for InstrBook {
329    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
330        Some(self.cmp(other))
331    }
332}
333impl PartialEq for InstrBook {
334    fn eq(&self, other: &Self) -> bool {
335        self.start_pos == other.start_pos
336    }
337}
338impl fmt::Display for InstrBook {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        write!(
341            f,
342            "InstrBook({}, {}-{}, {})",
343            self.instr, self.start_pos, self.end_pos, self.keep_val
344        )
345    }
346}
347impl Eq for InstrBook {}