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 {}