1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
//! Provides definitions and implementations for instruction-related functionalities.
//!
//! ## Main Structures and Enumerations:
//!
//! - [`InstrType`]: An enumeration that defines the types of instructions supported, including `CONST` for constant values and `SINE` for sinusoidal waves.
//!
//! - [`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.
//!
//! - [`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.
//!
//! ## Utilities:
//!
//! - The `InstrArgs` type alias provides a convenient way to define instruction arguments using a dictionary with string keys and float values.
//!
//! - The module makes use of the `maplit` crate to enable easy creation of hashmaps.
//!
//! ## Features:
//!
//! - Easy creation of instruction objects with utility methods such as `new_const` and `new_sine`.
//! - Ability to evaluate instructions and in-place populate given time array views with the resulting float-point values.
//! - Support for default values in instructions, allowing for flexibility and ease of use.

use std::cmp::Ordering;
use std::collections::HashMap;
use std::f64::consts::PI;
use std::fmt;

use maplit::hashmap;

/// Type alias for instruction arguments: a dictionary with key-value pairs of
/// string (argument name) and float (value)
pub type InstrArgs = HashMap<String, f64>;

/// Enum type for different instructions. Supported instructions: `CONST`, `SINE`
#[derive(Clone, PartialEq)]
pub enum InstrType {
    CONST,
    SINE,
}
impl fmt::Display for InstrType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                InstrType::CONST => "CONST",
                InstrType::SINE => "SINE",
            }
        )
    }
}

// / This function uses [`other_function`] to ...
// /
// / [`other_function`]: ./path/to/other/function

// Instruction struct consists of instr_type (enumerated type) and argument dictionary
/// Struct for a general instruction, consisting of type and arguments.
///
/// Different instruction types expects different fields in their argument dictionary.
/// Behavior for minimally expected keys are defined in `Instruction::new`, behavior of
/// default values are defined in `Instruction::eval_inplace`.
///
/// ## Implemented instruction types and their expected fields:
/// 1. `InstrType::CONST`:
///    - `const`
/// 2. `InstrType::SINE`:
///    - `freq`
///    - `amplitude`: Default is `1.0`
///    - `offset`: Default is `0.0`
///    - `phase`: Default is `0.0`
///
#[derive(Clone, PartialEq)]
pub struct Instruction {
    pub instr_type: InstrType,
    pub args: InstrArgs,
}
impl Instruction {
    /// Constructs an `Instruction` object.
    ///
    /// This method serves as the foundational constructor upon which custom constructor
    /// wrappers for new instructions should be built. For each instruction type,
    /// it ensures that the `args` dictionary contains the required keys.
    ///
    /// Missing keys will cause a panic.
    ///
    /// # Examples
    ///
    /// Constructing a new `CONST` instruction
    /// (this is effectively the underlying implementation for [`Instruction::new_const`],
    /// the more convenient constructor):
    ///
    /// ```
    /// use nicompiler_backend::instruction::*;
    /// use std::collections::HashMap;
    ///
    /// let mut const_args = InstrArgs::new();
    /// const_args.insert("value".to_string(), 1.0);
    /// let const_instr = Instruction::new(InstrType::CONST, const_args);
    /// ```
    ///
    /// If you fail to provide the required argument fields, it will panic:
    ///
    /// ```should_panic
    /// # use nicompiler_backend::instruction::*;
    /// # use std::collections::HashMap;
    /// let mut const_args = InstrArgs::new();
    /// let const_instr = Instruction::new(InstrType::CONST, const_args);
    /// ```
    ///
    /// The panic message will be:
    /// `thread 'main' panicked at 'Expected instr type CONST to contain key value'`.
    ///
    /// Constructing a new `SINE` instruction:
    ///
    /// ```
    /// # use nicompiler_backend::instruction::*;
    /// # use std::collections::HashMap;

    /// let mut sine_args = InstrArgs::new();
    /// sine_args.insert("freq".to_string(), 10.0);
    /// sine_args.insert("offset".to_string(), 1.0); // amplitude and phase will use default values
    /// let sine_instr = Instruction::new(InstrType::SINE, sine_args);
    /// ```
    pub fn new(instr_type: InstrType, args: InstrArgs) -> Self {
        let panic_no_key = |key| {
            if !args.contains_key(key) {
                panic!("Expected instr type {} to contain key {}", instr_type, key)
            }
        };
        match instr_type {
            InstrType::CONST => panic_no_key("value"),
            InstrType::SINE => panic_no_key("freq"),
        };
        Instruction { instr_type, args }
    }

    /// Evaluates the instruction and populates the given array view with float-point values.
    ///
    /// This method takes a mutable array view (`t_arr`) and modifies its values in-place based on the instruction type and its arguments.
    ///
    /// - For `InstrType::CONST`, the array will be filled with the constant value specified by the `value` argument.
    /// - 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.
    ///
    /// # Arguments
    ///
    /// * `t_arr` - A mutable 1D array view that will be populated with the evaluated values.
    ///
    /// # Examples
    ///
    /// Given an instruction set with a constant and a sine instruction, and an array representing time values from 0 to 1:
    ///
    /// ```
    /// use ndarray::{Array2, Array1};
    /// use nicompiler_backend::instruction::*;
    ///
    /// let t_row = ndarray::Array1::linspace(0.0, 1.0, 10);
    /// let mut t_values = ndarray::stack(ndarray::Axis(0), &[t_row.view(), t_row.view()]).unwrap();
    /// // Use wrappers to create sine and constant instructions same as the examples above
    /// let const_instr = Instruction::new_const(1.0);
    /// const_instr.eval_inplace(&mut t_values.row_mut(0));
    ///
    /// let sine_instr = Instruction::new_sine(10.0, None, None, Some(1.0));
    /// sine_instr.eval_inplace(&mut t_values.row_mut(1));
    /// assert!(t_values[[0, 0]] == 1. && t_values[[0, 1]] == 1.);
    /// ```
    pub fn eval_inplace(&self, t_arr: &mut ndarray::ArrayViewMut1<f64>) {
        match self.instr_type {
            InstrType::CONST => {
                let value = *self.args.get("value").unwrap();
                t_arr.fill(value);
            }
            InstrType::SINE => {
                let freq = *self.args.get("freq").unwrap();
                // Default values can be set by default with unwrap_or
                let amplitude = *self.args.get("amplitude").unwrap_or(&1.0);
                let offset = *self.args.get("offset").unwrap_or(&0.0);
                let phase = *self.args.get("phase").unwrap_or(&0.0);

                t_arr.map_inplace(|t| {
                    *t = (2.0 * PI * freq * (*t) + phase).sin() * amplitude + offset
                });
            }
        }
    }

    /// Wrapper for conveniently creating new constant instructions.
    /// Example usage equivalent to the constant example above:
    /// ```
    /// # use nicompiler_backend::instruction::*;
    /// let const_instr = Instruction::new_const(1.0);
    /// ```
    pub fn new_const(value: f64) -> Instruction {
        Instruction::new(InstrType::CONST, hashmap! {String::from("value") => value})
    }

    /// Constructs a new sine instruction with provided parameters.
    ///
    /// 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.
    ///
    /// # Arguments
    ///
    /// - `freq`: The frequency of the sine wave.
    /// - `amplitude`: Optional amplitude of the sine wave. If `None`, it will not be set in the instruction.
    /// - `phase`: Optional phase offset of the sine wave in radians. If `None`, it will not be set in the instruction.
    /// - `dc_offset`: Optional DC offset for the sine wave. If `None`, it will not be set in the instruction.
    ///
    /// # Examples
    ///
    /// Constructing a sine instruction with a specified frequency, and DC offset. Amplitude and phase will use any default values defined elsewhere:
    ///
    /// ---
    /// # use nicompiler_backend::instruction::*;
    ///
    /// let sine_instr = Instruction::new_sine(10.0, None, None, Some(1.0));
    /// ---
    ///
    pub fn new_sine(
        freq: f64,
        amplitude: Option<f64>,
        phase: Option<f64>,
        dc_offset: Option<f64>,
    ) -> Instruction {
        let mut instr_args: InstrArgs = hashmap! {"freq".to_string() => freq};
        // For each optional argument, if specified, insert into dictionary
        [
            ("amplitude", amplitude),
            ("phase", phase),
            ("offset", dc_offset),
        ]
        .iter()
        .for_each(|(key, opt_value)| {
            if let Some(value) = *opt_value {
                instr_args.insert(key.to_string(), value);
            }
        });
        Instruction::new(InstrType::SINE, instr_args)
    }
}
impl fmt::Display for Instruction {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let args_string = self
            .args
            .iter()
            .map(|(k, v)| format!("{}: {}", k, v))
            .collect::<Vec<String>>()
            .join(", ");
        write!(f, "[{}, {{{}}}]", self.instr_type, args_string)
    }
}

/// Manages an instruction along with its associated metadata during experiment editing.
///
/// The `InstrBook` struct captures the following metadata:
/// - Defined interval using `start_pos` and `end_pos`.
/// - A flag, `keep_val`, to determine whether to retain the value after the defined interval.
///
/// For the instruction interval:
/// - `start_pos` is inclusive.
/// - `end_pos` is exclusive.
///
/// `InstrBook` implements ordering based on `start_pos` to facilitate sorting.
/// editing phase: defined interval, and whether to keep value after defined interval.
/// For instruction interval, `start_pos` is inclusive while `end_pos` is exclusive.
/// We implemented ordering for `InstrBook` to allow sorting based on `start_pos`.
///
pub struct InstrBook {
    pub start_pos: usize,
    pub end_pos: usize,
    pub keep_val: bool,
    pub instr: Instruction,
}
impl InstrBook {
    /// Constructs a new `InstrBook` object.
    ///
    /// Checks that `end_pos` is strictly greater than `start_pos`.
    ///
    /// # Arguments
    ///
    /// - `start_pos`: Starting position (inclusive).
    /// - `end_pos`: Ending position (exclusive).
    /// - `keep_val`: Flag to determine if value should be retained after the interval.
    /// - `instr`: The associated instruction.
    ///
    /// # Examples
    ///
    /// Constructing a valid `InstrBook`:
    ///
    /// ```
    /// # use nicompiler_backend::instruction::*;
    /// let instruction = Instruction::new(InstrType::CONST, [("value".to_string(), 1.0)].iter().cloned().collect());
    /// let book = InstrBook::new(0, 5, true, instruction);
    /// ```
    ///
    /// Attempting to construct an `InstrBook` with `end_pos` not greater than `start_pos` will panic:
    ///
    /// ```should_panic
    /// # use nicompiler_backend::instruction::*;
    /// let instruction = Instruction::new(InstrType::CONST, [("value".to_string(), 1.0)].iter().cloned().collect());
    /// let book = InstrBook::new(5, 5, true, instruction);
    /// ```
    ///
    /// The panic message will be:
    /// `Instruction { /* ... */ } end_pos 5 should be strictly greater than start_pos 5`.
    pub fn new(start_pos: usize, end_pos: usize, keep_val: bool, instr: Instruction) -> Self {
        assert!(
            end_pos > start_pos,
            "Instruction {} end_pos {} should be strictly greater than start_pos {}",
            instr,
            end_pos,
            start_pos
        );
        InstrBook {
            start_pos,
            end_pos,
            keep_val,
            instr,
        }
    }
}
// Support total ordering for InstrBook
impl Ord for InstrBook {
    fn cmp(&self, other: &Self) -> Ordering {
        // We reverse the order to make BinaryHeap a min-heap based on start_pos
        self.start_pos.cmp(&other.start_pos)
    }
}
impl PartialOrd for InstrBook {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}
impl PartialEq for InstrBook {
    fn eq(&self, other: &Self) -> bool {
        self.start_pos == other.start_pos
    }
}
impl fmt::Display for InstrBook {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "InstrBook({}, {}-{}, {})",
            self.instr, self.start_pos, self.end_pos, self.keep_val
        )
    }
}
impl Eq for InstrBook {}