refactor: refactor kernel dataset into spec
This commit is contained in:
@@ -1,457 +0,0 @@
|
||||
use crate::common::{
|
||||
DeviceValueError, FloatingPointError, validate_device_value, validate_floating_point,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Error as IoError, Write};
|
||||
use std::num::ParseFloatError;
|
||||
use std::path::Path;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// Error occurs when building dataset.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum DatasetError {
|
||||
#[error("invalid device value {0} in dataset item")]
|
||||
BadDeviceValue(#[from] DeviceValueError),
|
||||
#[error("unexpected empty string in dataset item")]
|
||||
BlankDeviceValue,
|
||||
#[error("bad string form of device value: {0}")]
|
||||
ParseHumanReadableValue(#[from] ParseHumanReadableValueError),
|
||||
#[error("duplicate item {0} in standard value list")]
|
||||
DupDatasetItem(String),
|
||||
#[error("unexpected empty standard value list")]
|
||||
EmptyDataset,
|
||||
#[error("fail to open dataset file: {0}")]
|
||||
OpenDatasetFile(IoError),
|
||||
#[error("fail to read dataset file: {0}")]
|
||||
ReadDatasetFile(IoError),
|
||||
#[error("fail to write dataset file: {0}")]
|
||||
WriteDatasetFile(IoError),
|
||||
}
|
||||
|
||||
/// An item in the dataset.
|
||||
#[derive(Debug, Clone)]
|
||||
struct DatasetItem {
|
||||
/// The actual value of this item.
|
||||
value: f64,
|
||||
/// The string form of this value given from original input for re-saving.
|
||||
str_value: String,
|
||||
}
|
||||
|
||||
impl DatasetItem {
|
||||
/// Create a new dataset item with validation.
|
||||
fn new(value: f64, str_value: String) -> Result<Self, DatasetError> {
|
||||
// Check arguments
|
||||
let value = validate_device_value(value)?;
|
||||
if str_value.is_empty() {
|
||||
return Err(DatasetError::BlankDeviceValue);
|
||||
}
|
||||
Ok(Self { value, str_value })
|
||||
}
|
||||
}
|
||||
|
||||
/// A list holding available standard values for resistor, capacitor or inductor.
|
||||
///
|
||||
/// Standard values is a collection of all possible values of specific device manufactured
|
||||
/// by electronic factory. In reality, it also can be replaced by all possible values of
|
||||
/// specific device provided by your laboratory. For example, your laboratory only provide
|
||||
/// resistor with 100 Ohm and 4.7k Ohm. This list will only contain 100 and 4.7k.
|
||||
pub struct Dataset {
|
||||
/// A list of available device gauge values.
|
||||
items: Vec<DatasetItem>,
|
||||
}
|
||||
|
||||
impl Dataset {
|
||||
/// Internal used generic dataset creation function.
|
||||
fn new<I>(str_values: I) -> Result<Self, DatasetError>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
// Check string form value one by one
|
||||
let mut items: Vec<DatasetItem> = Vec::new();
|
||||
let mut seen: HashSet<OrderedFloat<f64>> = HashSet::new();
|
||||
|
||||
for str_value in str_values {
|
||||
// Try parsing value
|
||||
let value = from_human_readable_value(&str_value)?;
|
||||
// Check and update set
|
||||
if !seen.insert(OrderedFloat(value)) {
|
||||
return Err(DatasetError::DupDatasetItem(str_value.to_string()));
|
||||
}
|
||||
// Add into result
|
||||
items.push(DatasetItem::new(value, str_value)?);
|
||||
}
|
||||
|
||||
// Check empty case
|
||||
if items.is_empty() {
|
||||
return Err(DatasetError::EmptyDataset);
|
||||
}
|
||||
|
||||
// Ok, assign it
|
||||
Ok(Self { items })
|
||||
}
|
||||
|
||||
/// Create a dataset from an iterable of string values.
|
||||
pub fn from_iterator<I, S>(str_values: I) -> Result<Self, DatasetError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
Self::new(str_values.into_iter().map(|i| i.into()))
|
||||
}
|
||||
|
||||
/// Load a dataset from a block of text.
|
||||
///
|
||||
/// Each non-empty line (after trimming whitespace) is treated as a value.
|
||||
pub fn from_text(text: &str) -> Result<Self, DatasetError> {
|
||||
let lines = text
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty());
|
||||
Self::from_iterator(lines)
|
||||
}
|
||||
|
||||
/// Load a dataset from a file.
|
||||
///
|
||||
/// Each non-empty line (after trimming whitespace) is treated as a value.
|
||||
pub fn from_file<P>(path: P) -> Result<Self, DatasetError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(path).map_err(|err| DatasetError::OpenDatasetFile(err))?;
|
||||
let reader = BufReader::new(file);
|
||||
let lines = reader
|
||||
.lines()
|
||||
.map(|line| line.map(|line| line.trim().to_string()))
|
||||
.filter(|line| !matches!(line, Ok(line) if line.is_empty()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|err| DatasetError::ReadDatasetFile(err))?;
|
||||
Self::from_iterator(lines.into_iter())
|
||||
}
|
||||
|
||||
/// The preset dataset for resistors.
|
||||
pub fn resistor_preset() -> Result<Self, DatasetError> {
|
||||
Self::from_iterator([
|
||||
"100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k",
|
||||
"6.8k", "10k", "47k", "100k", "1M",
|
||||
])
|
||||
}
|
||||
|
||||
/// The preset dataset for capacitors.
|
||||
pub fn capacitor_preset() -> Result<Self, DatasetError> {
|
||||
Self::from_iterator([
|
||||
"10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p",
|
||||
"1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u",
|
||||
])
|
||||
}
|
||||
|
||||
/// The preset dataset for inductors.
|
||||
pub fn inductor_preset() -> Result<Self, DatasetError> {
|
||||
Self::from_iterator([
|
||||
"0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u",
|
||||
"8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u",
|
||||
])
|
||||
}
|
||||
|
||||
fn save(&self) -> impl Iterator<Item = &str> {
|
||||
self.items.iter().map(|i| i.str_value.as_str())
|
||||
}
|
||||
|
||||
/// Get the string form of all values one by one for saving
|
||||
pub fn save_iterator(&self) -> impl Iterator<Item = &str> {
|
||||
self.save()
|
||||
}
|
||||
|
||||
/// Get the string form of all values joined by newlines for saving./
|
||||
pub fn save_text(&self) -> String {
|
||||
itertools::join(self.save_iterator(), "\n")
|
||||
}
|
||||
|
||||
/// Save all values joined by newlines to a file.
|
||||
pub fn save_file<P>(&self, path: P) -> Result<(), DatasetError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(path).map_err(|err| DatasetError::OpenDatasetFile(err))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
for line in self.save_iterator() {
|
||||
writer
|
||||
.write_all(line.as_bytes())
|
||||
.map_err(|err| DatasetError::WriteDatasetFile(err))?;
|
||||
writer
|
||||
.write_all("\n".as_bytes())
|
||||
.map_err(|err| DatasetError::WriteDatasetFile(err))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The number of available standard values.
|
||||
pub fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
/// Get the available standard value by index.
|
||||
pub fn get(&self, index: usize) -> Option<f64> {
|
||||
self.items.get(index).map(|i| i.value)
|
||||
}
|
||||
|
||||
/// Get the available standard values as an iterator of `f64`.
|
||||
pub fn values(&self) -> impl Iterator<Item = f64> + Clone {
|
||||
self.items.iter().map(|i| i.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The collection holding all standard values for resistor, capacitor and inductor respectively.
|
||||
pub struct DatasetCollection {
|
||||
/// A list of available device gauge values for resistor.
|
||||
resistor: Dataset,
|
||||
/// A list of available device gauge values for capacitor.
|
||||
capacitor: Dataset,
|
||||
/// A list of available device gauge values for inductor.
|
||||
inductor: Dataset,
|
||||
}
|
||||
|
||||
impl DatasetCollection {
|
||||
/// Create dataset collection with 3 datasets for resistor, capacitor and inductor respectively.
|
||||
pub fn new(resistor: Dataset, capacitor: Dataset, inductor: Dataset) -> Self {
|
||||
Self {
|
||||
resistor,
|
||||
capacitor,
|
||||
inductor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the standard values for resistor, capacitor and inductor respectively from iterables.
|
||||
///
|
||||
/// * `resistor` - The iterable to load available standard values for resistor.
|
||||
/// * `capacitor` - The iterable to load available standard values for capacitor.
|
||||
/// * `inductor` - The iterable to load available standard values for inductor.
|
||||
pub fn from_iterable<I1, S1, I2, S2, I3, S3>(
|
||||
resistor: I1,
|
||||
capacitor: I2,
|
||||
inductor: I3,
|
||||
) -> Result<Self, DatasetError>
|
||||
where
|
||||
I1: IntoIterator<Item = S1>,
|
||||
S1: Into<String>,
|
||||
I2: IntoIterator<Item = S2>,
|
||||
S2: Into<String>,
|
||||
I3: IntoIterator<Item = S3>,
|
||||
S3: Into<String>,
|
||||
{
|
||||
Ok(Self {
|
||||
resistor: Dataset::from_iterator(resistor)?,
|
||||
capacitor: Dataset::from_iterator(capacitor)?,
|
||||
inductor: Dataset::from_iterator(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the standard values from strings.
|
||||
///
|
||||
/// * `resistor` - The string to load available standard values for resistor.
|
||||
/// * `capacitor` - The string to load available standard values for capacitor.
|
||||
/// * `inductor` - The string to load available standard values for inductor.
|
||||
pub fn from_text(
|
||||
resistor: &str,
|
||||
capacitor: &str,
|
||||
inductor: &str,
|
||||
) -> Result<Self, DatasetError> {
|
||||
Ok(Self {
|
||||
resistor: Dataset::from_text(resistor)?,
|
||||
capacitor: Dataset::from_text(capacitor)?,
|
||||
inductor: Dataset::from_text(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the standard values from files.
|
||||
///
|
||||
/// * `resistor` - The file to load available standard values for resistor.
|
||||
/// * `capacitor` - The file to load available standard values for capacitor.
|
||||
/// * `inductor` - The file to load available standard values for inductor.
|
||||
pub fn from_file<P1, P2, P3>(
|
||||
resistor: P1,
|
||||
capacitor: P2,
|
||||
inductor: P3,
|
||||
) -> Result<Self, DatasetError>
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
P3: AsRef<Path>,
|
||||
{
|
||||
Ok(Self {
|
||||
resistor: Dataset::from_file(resistor)?,
|
||||
capacitor: Dataset::from_file(capacitor)?,
|
||||
inductor: Dataset::from_file(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The preset dataset collection for all devices.
|
||||
pub fn devices_preset() -> Result<Self, DatasetError> {
|
||||
Ok(Self {
|
||||
resistor: Dataset::resistor_preset()?,
|
||||
capacitor: Dataset::capacitor_preset()?,
|
||||
inductor: Dataset::inductor_preset()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the iterators for saving resistor, capacitor and inductor dataset respectively.
|
||||
pub fn save_iterator(
|
||||
&self,
|
||||
) -> (
|
||||
impl Iterator<Item = &str>,
|
||||
impl Iterator<Item = &str>,
|
||||
impl Iterator<Item = &str>,
|
||||
) {
|
||||
(
|
||||
self.resistor.save_iterator(),
|
||||
self.capacitor.save_iterator(),
|
||||
self.inductor.save_iterator(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the string form of all values for saving resistor, capacitor and inductor dataset respectively.
|
||||
pub fn save_text(&self) -> (String, String, String) {
|
||||
(
|
||||
self.resistor.save_text(),
|
||||
self.capacitor.save_text(),
|
||||
self.inductor.save_text(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Save all values to files.
|
||||
///
|
||||
/// * `resistor` - The file to save available standard values for resistor.
|
||||
/// * `capacitor` - The file to save available standard values for capacitor.
|
||||
/// * `inductor` - The file to save available standard values for inductor.
|
||||
pub fn save_file<P1, P2, P3>(
|
||||
&self,
|
||||
resistor: P1,
|
||||
capacitor: P2,
|
||||
inductor: P3,
|
||||
) -> Result<(), DatasetError>
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
P3: AsRef<Path>,
|
||||
{
|
||||
self.resistor.save_file(resistor)?;
|
||||
self.capacitor.save_file(capacitor)?;
|
||||
self.inductor.save_file(inductor)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the dataset for resistor.
|
||||
pub fn resistor_dataset(&self) -> &Dataset {
|
||||
&self.resistor
|
||||
}
|
||||
|
||||
/// Get the dataset for capacitor.
|
||||
pub fn capacitor_dataset(&self) -> &Dataset {
|
||||
&self.capacitor
|
||||
}
|
||||
|
||||
/// Get the dataset for inductor.
|
||||
pub fn inductor_dataset(&self) -> &Dataset {
|
||||
&self.inductor
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseHumanReadableValueError {
|
||||
#[error("fail to parse floating point part of given human readable value: {0}")]
|
||||
ParseFloat(#[from] ParseFloatError),
|
||||
#[error("arithmetic error: {0}")]
|
||||
BadArithmetic(#[from] FloatingPointError),
|
||||
}
|
||||
|
||||
/// Convert human readable value to float.
|
||||
///
|
||||
/// `strl` is the human readable value.
|
||||
/// The return value is the parsed float value.
|
||||
/// or error occurs when parsing.
|
||||
pub fn from_human_readable_value(strl: &str) -> Result<f64, ParseHumanReadableValueError> {
|
||||
let strl = strl.trim();
|
||||
|
||||
let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') {
|
||||
(stripped, 1e-12)
|
||||
} else if let Some(stripped) = strl.strip_suffix('p') {
|
||||
(stripped, 1e-9)
|
||||
} else if let Some(stripped) = strl.strip_suffix('u') {
|
||||
(stripped, 1e-6)
|
||||
} else if let Some(stripped) = strl.strip_suffix('m') {
|
||||
(stripped, 1e-3)
|
||||
} else if let Some(stripped) = strl.strip_suffix('k') {
|
||||
(stripped, 1e3)
|
||||
} else if let Some(stripped) = strl.strip_suffix('M') {
|
||||
(stripped, 1e6)
|
||||
} else if let Some(stripped) = strl.strip_suffix('G') {
|
||||
(stripped, 1e9)
|
||||
} else {
|
||||
(strl, 1.0)
|
||||
};
|
||||
|
||||
let num = num_part.parse::<f64>()?;
|
||||
Ok(validate_floating_point(num * multiplier)?)
|
||||
}
|
||||
|
||||
/// The unit scale for human readable value.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UnitScale {
|
||||
NanoLower,
|
||||
Nano,
|
||||
Micro,
|
||||
Milli,
|
||||
None,
|
||||
Kilo,
|
||||
Mega,
|
||||
Giga,
|
||||
GigaHigher,
|
||||
}
|
||||
|
||||
/// Get the unit scale of human readable value.
|
||||
///
|
||||
/// `v` is the value for analyzing scale.
|
||||
pub fn get_human_readable_value_scale(v: f64) -> UnitScale {
|
||||
let v = v.abs();
|
||||
if v < 1e-12 {
|
||||
UnitScale::NanoLower
|
||||
} else if v < 1e-9 {
|
||||
UnitScale::Nano
|
||||
} else if v < 1e-6 {
|
||||
UnitScale::Micro
|
||||
} else if v < 1e-3 {
|
||||
UnitScale::Milli
|
||||
} else if v < 1e3 {
|
||||
UnitScale::None
|
||||
} else if v < 1e6 {
|
||||
UnitScale::Kilo
|
||||
} else if v < 1e9 {
|
||||
UnitScale::Mega
|
||||
} else if v < 1e12 {
|
||||
UnitScale::Giga
|
||||
} else {
|
||||
UnitScale::GigaHigher
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert float value to human readable value.
|
||||
///
|
||||
/// `v`is the float value for formatting as human readable value.
|
||||
pub fn to_human_readable_value(v: f64) -> String {
|
||||
let scale = get_human_readable_value_scale(v);
|
||||
match scale {
|
||||
UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12),
|
||||
UnitScale::Nano => format!("{:+.4} p", v / 1e-9),
|
||||
UnitScale::Micro => format!("{:+.4} u", v / 1e-6),
|
||||
UnitScale::Milli => format!("{:+.4} m", v / 1e-3),
|
||||
// YYC MARK:
|
||||
// The space of this format string is by design
|
||||
// for keeping the same style with other format strings.
|
||||
UnitScale::None => format!("{:+.4} ", v),
|
||||
UnitScale::Kilo => format!("{:+.4} k", v / 1e3),
|
||||
UnitScale::Mega => format!("{:+.4} M", v / 1e6),
|
||||
UnitScale::Giga => format!("{:+.4} G", v / 1e9),
|
||||
UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9),
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod common;
|
||||
pub mod dataset;
|
||||
pub mod spec;
|
||||
pub mod query;
|
||||
pub mod resolver;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{Resolver, ResolverError};
|
||||
use crate::common::{
|
||||
Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind, JointKind,
|
||||
};
|
||||
use crate::dataset::{Dataset, DatasetCollection};
|
||||
use crate::spec::{SpecGroup, SpecCatalog};
|
||||
use crate::query::{Request, Response, ResponseError};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
@@ -208,7 +208,7 @@ impl ResultBucket {
|
||||
/// A resolver that uses brute-force search to find the best matching circuits.
|
||||
pub struct BfsResolver {
|
||||
/// The datasets for all device kinds.
|
||||
datasets: DatasetCollection,
|
||||
datasets: SpecCatalog,
|
||||
}
|
||||
|
||||
impl BfsResolver {
|
||||
@@ -222,21 +222,21 @@ impl BfsResolver {
|
||||
|
||||
/// Iterate all possible circuits with one device without repeating equivalent topology.
|
||||
pub fn iter_one_device_circuit(
|
||||
dataset: &Dataset,
|
||||
dataset: &SpecGroup,
|
||||
) -> impl Iterator<Item = Result<Circuit, CircuitError>> {
|
||||
// Every single device is unique so we directly output them.
|
||||
// This feature is insured by dataset itself.
|
||||
dataset.values().map(|v1| Circuit::from_one_device(v1))
|
||||
dataset.specs().map(|v1| Circuit::from_one_device(v1))
|
||||
}
|
||||
|
||||
/// Iterate all possible circuits with two devices without repeating equivalent topology.
|
||||
pub fn iter_two_devices_circuit(
|
||||
dataset: &Dataset,
|
||||
dataset: &SpecGroup,
|
||||
) -> impl Iterator<Item = Result<Circuit, CircuitError>> {
|
||||
// The two devices in this circuit is always swapable,
|
||||
// so we iterate them without repeating.
|
||||
itertools::iproduct!(
|
||||
dataset.values().array_combinations_with_replacement::<2>(),
|
||||
dataset.specs().array_combinations_with_replacement::<2>(),
|
||||
JointKind::iter()
|
||||
)
|
||||
.map(|([v1, v2], j2)| Circuit::from_two_devices(v1, v2, j2))
|
||||
@@ -244,7 +244,7 @@ impl BfsResolver {
|
||||
|
||||
/// Iterate all possible circuits with three devices without repeating equivalent topology.
|
||||
pub fn iter_three_devices_circuit(
|
||||
dataset: &Dataset,
|
||||
dataset: &SpecGroup,
|
||||
) -> impl Iterator<Item = Result<Circuit, CircuitError>> {
|
||||
// For generating three devices circuit,
|
||||
// it should be consisted by 2 parts.
|
||||
@@ -252,15 +252,15 @@ impl BfsResolver {
|
||||
// First, the whole circuit has only one joint type.
|
||||
// In this case, 3 devices are swapable and we should iterate them without repeating
|
||||
itertools::iproduct!(
|
||||
dataset.values().array_combinations_with_replacement::<3>(),
|
||||
dataset.specs().array_combinations_with_replacement::<3>(),
|
||||
JointKind::iter()
|
||||
)
|
||||
.map(|([v1, v2, v3], j)| Circuit::from_three_devices(v1, v2, j, v3, j)),
|
||||
// Second, if the joint type is different, then the first 2 devices are swapable.
|
||||
// So we need iterate them without repeating.
|
||||
itertools::iproduct!(
|
||||
dataset.values().array_combinations_with_replacement::<2>(),
|
||||
dataset.values(),
|
||||
dataset.specs().array_combinations_with_replacement::<2>(),
|
||||
dataset.specs(),
|
||||
JointKind::iter()
|
||||
)
|
||||
.map(|([v1, v2], v3, j)| Circuit::from_three_devices(
|
||||
@@ -276,20 +276,20 @@ impl BfsResolver {
|
||||
|
||||
impl BfsResolver {
|
||||
/// Create a new BFS resolver with the given datasets.
|
||||
pub fn new(datasets: DatasetCollection) -> Self {
|
||||
pub fn new(datasets: SpecCatalog) -> Self {
|
||||
Self { datasets }
|
||||
}
|
||||
|
||||
fn pick_dataset(&self, device_kind: DeviceKind) -> &Dataset {
|
||||
fn pick_dataset(&self, device_kind: DeviceKind) -> &SpecGroup {
|
||||
match device_kind {
|
||||
DeviceKind::Resistor => self.datasets.resistor_dataset(),
|
||||
DeviceKind::Capacitor => self.datasets.capacitor_dataset(),
|
||||
DeviceKind::Inductor => self.datasets.inductor_dataset(),
|
||||
DeviceKind::Resistor => self.datasets.resistor_specs(),
|
||||
DeviceKind::Capacitor => self.datasets.capacitor_specs(),
|
||||
DeviceKind::Inductor => self.datasets.inductor_specs(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bfs_iteration(
|
||||
dataset: &Dataset,
|
||||
dataset: &SpecGroup,
|
||||
ccalc: &CircuitCalculator,
|
||||
) -> impl Iterator<Item = Result<BfsItem, BfsResolverError>> {
|
||||
itertools::chain!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::bfs::BfsResolver;
|
||||
use super::{Resolver, ResolverError};
|
||||
use crate::common::{Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind};
|
||||
use crate::dataset::{Dataset, DatasetCollection};
|
||||
use crate::spec::{SpecGroup, SpecCatalog};
|
||||
use crate::query::{Request, Response, ResponseError};
|
||||
use ordered_float::OrderedFloat;
|
||||
use thiserror::Error as TeError;
|
||||
@@ -127,16 +127,16 @@ pub struct LutResolver {
|
||||
|
||||
impl LutResolver {
|
||||
/// Create a new LUT resolver by building lookup tables from the given datasets.
|
||||
pub fn new(datasets: &DatasetCollection) -> Result<Self, LutResolverError> {
|
||||
pub fn new(datasets: &SpecCatalog) -> Result<Self, LutResolverError> {
|
||||
Ok(Self {
|
||||
resistor_lut: Self::build_lut(datasets.resistor_dataset(), DeviceKind::Resistor)?,
|
||||
capacitor_lut: Self::build_lut(datasets.capacitor_dataset(), DeviceKind::Capacitor)?,
|
||||
inductor_lut: Self::build_lut(datasets.inductor_dataset(), DeviceKind::Inductor)?,
|
||||
resistor_lut: Self::build_lut(datasets.resistor_specs(), DeviceKind::Resistor)?,
|
||||
capacitor_lut: Self::build_lut(datasets.capacitor_specs(), DeviceKind::Capacitor)?,
|
||||
inductor_lut: Self::build_lut(datasets.inductor_specs(), DeviceKind::Inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_lut(
|
||||
dataset: &Dataset,
|
||||
dataset: &SpecGroup,
|
||||
device_kind: DeviceKind,
|
||||
) -> Result<Vec<LutItem>, LutResolverError> {
|
||||
// Fetch all items
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
//! Types for managing the rated values of components available in your lab.
|
||||
//!
|
||||
//! In this module, a "spec" means a single rated parameter value of a component,
|
||||
//! such as `100` Ohms, `4.7k` Ohms, or `10u` Farads. It is **not** a general
|
||||
//! technical specification document — it is simply the nominal value printed on
|
||||
//! the component's body.
|
||||
//!
|
||||
//! - [`Spec`] — one rated value (e.g., 4.7k).
|
||||
//! - [`SpecGroup`] — all rated values of a given component type that your lab
|
||||
//! actually has in stock (e.g., all resistor values available in your drawer).
|
||||
//! - [`SpecCatalog`] — the complete collection of rated values for resistors,
|
||||
//! capacitors, and inductors.
|
||||
//!
|
||||
//! In short: these types answer the question "which exact component values can
|
||||
//! I pick from the shelf?".
|
||||
|
||||
use crate::common::{
|
||||
DeviceValueError, FloatingPointError, validate_device_value, validate_floating_point,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Error as IoError, Write};
|
||||
use std::num::ParseFloatError;
|
||||
use std::path::Path;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// Errors that can occur when working with rated component values.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum SpecError {
|
||||
#[error("invalid device value: {0}")]
|
||||
BadDeviceValue(#[from] DeviceValueError),
|
||||
#[error("bad string form of device value: {0}")]
|
||||
ParseHumanReadableValue(#[from] ParseHumanReadableValueError),
|
||||
#[error("duplicate rated value: {0}")]
|
||||
DupSpecItem(String),
|
||||
#[error("empty rated value group")]
|
||||
EmptySpecGroup,
|
||||
#[error("fail to open rated values file: {0}")]
|
||||
OpenSpecFile(IoError),
|
||||
#[error("fail to read rated values file: {0}")]
|
||||
ReadSpecFile(IoError),
|
||||
#[error("fail to write rated values file: {0}")]
|
||||
WriteSpecFile(IoError),
|
||||
}
|
||||
|
||||
/// One rated value of a component (e.g., `4.7k` standing for 4700 Ohms).
|
||||
///
|
||||
/// A `Spec` stores both the parsed numeric value and the original human-readable
|
||||
/// string so that the value can be re-serialized exactly as it was entered.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Spec {
|
||||
/// The numeric rated value (e.g., `4700.0` for `"4.7k"`).
|
||||
value: f64,
|
||||
/// The original human-readable form (e.g., `"4.7k"`), kept for faithful
|
||||
/// round-trip serialization.
|
||||
str_value: String,
|
||||
}
|
||||
|
||||
impl Spec {
|
||||
/// Create a new rated value from its human-readable representations.
|
||||
pub fn new(str_value: String) -> Result<Self, SpecError> {
|
||||
// Try parsing value and check its range
|
||||
let value = from_human_readable_value(&str_value)?;
|
||||
let value = validate_device_value(value)?;
|
||||
Ok(Self { value, str_value })
|
||||
}
|
||||
|
||||
/// Get the numeric rated value (e.g., `4700.0` for `"4.7k"`).
|
||||
pub fn get_value(&self) -> f64 {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Get the original human-readable value form (e.g., `"4.7k"`).
|
||||
pub fn get_str_value(&self) -> &str {
|
||||
&self.str_value
|
||||
}
|
||||
}
|
||||
|
||||
/// All rated values that your lab stocks for a single component type.
|
||||
///
|
||||
/// For example, a `SpecGroup` for resistors might hold `{100, 220, 470, 1k, 4.7k, 10k}`
|
||||
/// — these are the actual resistor values you have on hand. The same concept applies
|
||||
/// to capacitors and inductors.
|
||||
pub struct SpecGroup {
|
||||
/// The rated values belonging to this group.
|
||||
specs: Vec<Spec>,
|
||||
}
|
||||
|
||||
impl SpecGroup {
|
||||
/// Internal constructor: parse and deduplicate a sequence of human-readable rated values.
|
||||
fn new<I>(str_values: I) -> Result<Self, SpecError>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
// Check string form value one by one
|
||||
let mut specs: Vec<Spec> = Vec::new();
|
||||
let mut seen: HashSet<OrderedFloat<f64>> = HashSet::new();
|
||||
|
||||
for str_value in str_values {
|
||||
// Build spec instance
|
||||
let spec = Spec::new(str_value)?;
|
||||
// Check and update set
|
||||
if !seen.insert(OrderedFloat(spec.get_value())) {
|
||||
return Err(SpecError::DupSpecItem(spec.get_str_value().to_string()));
|
||||
}
|
||||
// Add into result
|
||||
specs.push(spec);
|
||||
}
|
||||
|
||||
// Check empty case
|
||||
if specs.is_empty() {
|
||||
return Err(SpecError::EmptySpecGroup);
|
||||
}
|
||||
|
||||
// Ok, assign it
|
||||
Ok(Self { specs })
|
||||
}
|
||||
|
||||
/// Build a spec group from any iterable of human-readable rated values (e.g., `"4.7k"`, `"100"`).
|
||||
pub fn from_iterator<I, S>(str_values: I) -> Result<Self, SpecError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
Self::new(str_values.into_iter().map(|i| i.into()))
|
||||
}
|
||||
|
||||
/// Read rated values from a text block, one value per non-empty line.
|
||||
pub fn from_text(text: &str) -> Result<Self, SpecError> {
|
||||
let lines = text
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty());
|
||||
Self::from_iterator(lines)
|
||||
}
|
||||
|
||||
/// Read rated values from a file, one value per non-empty line.
|
||||
pub fn from_file<P>(path: P) -> Result<Self, SpecError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?;
|
||||
let reader = BufReader::new(file);
|
||||
let lines = reader
|
||||
.lines()
|
||||
.map(|line| line.map(|line| line.trim().to_string()))
|
||||
.filter(|line| !matches!(line, Ok(line) if line.is_empty()))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|err| SpecError::ReadSpecFile(err))?;
|
||||
Self::from_iterator(lines.into_iter())
|
||||
}
|
||||
|
||||
/// A commonly used set of resistor rated values (E12‑derived).
|
||||
pub fn resistor_preset() -> Self {
|
||||
Self::from_iterator([
|
||||
"100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k",
|
||||
"6.8k", "10k", "47k", "100k", "1M",
|
||||
]).expect("unexpected bad rated values set")
|
||||
}
|
||||
|
||||
/// A commonly used set of capacitor rated values.
|
||||
pub fn capacitor_preset() -> Self {
|
||||
Self::from_iterator([
|
||||
"10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p",
|
||||
"1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u",
|
||||
]).expect("unexpected bad rated values set")
|
||||
}
|
||||
|
||||
/// A commonly used set of inductor rated values.
|
||||
pub fn inductor_preset() -> Self {
|
||||
Self::from_iterator([
|
||||
"0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u",
|
||||
"8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u",
|
||||
]).expect("unexpected bad rated values set")
|
||||
}
|
||||
|
||||
fn save(&self) -> impl Iterator<Item = &str> {
|
||||
self.specs.iter().map(|i| i.str_value.as_str())
|
||||
}
|
||||
|
||||
/// Iterate over the human-readable form of every rated value (for re-serialization).
|
||||
pub fn save_iterator(&self) -> impl Iterator<Item = &str> {
|
||||
self.save()
|
||||
}
|
||||
|
||||
/// Join all rated values with newlines into a single string (for re-serialization).
|
||||
pub fn save_text(&self) -> String {
|
||||
itertools::join(self.save_iterator(), "\n")
|
||||
}
|
||||
|
||||
/// Write all rated values to a file, one per line.
|
||||
pub fn save_file<P>(&self, path: P) -> Result<(), SpecError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
for line in self.save_iterator() {
|
||||
writer
|
||||
.write_all(line.as_bytes())
|
||||
.map_err(|err| SpecError::WriteSpecFile(err))?;
|
||||
writer
|
||||
.write_all("\n".as_bytes())
|
||||
.map_err(|err| SpecError::WriteSpecFile(err))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many rated values this group contains.
|
||||
pub fn len(&self) -> usize {
|
||||
self.specs.len()
|
||||
}
|
||||
|
||||
/// Get the numeric rated value at the given index.
|
||||
pub fn get(&self, index: usize) -> Option<f64> {
|
||||
self.specs.get(index).map(|i| i.value)
|
||||
}
|
||||
|
||||
/// Iterate over all numeric rated values in this group.
|
||||
pub fn specs(&self) -> impl Iterator<Item = f64> + Clone {
|
||||
self.specs.iter().map(|i| i.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The full catalogue of rated component values your lab stocks.
|
||||
///
|
||||
/// Bundles three [`SpecGroup`]s — one each for resistors, capacitors, and
|
||||
/// inductors. This is the top-level entry point for answering "which component
|
||||
/// values are available?".
|
||||
pub struct SpecCatalog {
|
||||
/// Rated values available for resistors.
|
||||
resistor: SpecGroup,
|
||||
/// Rated values available for capacitors.
|
||||
capacitor: SpecGroup,
|
||||
/// Rated values available for inductors.
|
||||
inductor: SpecGroup,
|
||||
}
|
||||
|
||||
impl SpecCatalog {
|
||||
/// Assemble a catalogue from the three device‑type spec groups.
|
||||
pub fn new(resistor: SpecGroup, capacitor: SpecGroup, inductor: SpecGroup) -> Self {
|
||||
Self {
|
||||
resistor,
|
||||
capacitor,
|
||||
inductor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a catalogue from three iterables of human‑readable rated values.
|
||||
///
|
||||
/// * `resistor` — values such as `"100"`, `"4.7k"`, etc.
|
||||
/// * `capacitor` — values such as `"10p"`, `"4.7u"`, etc.
|
||||
/// * `inductor` — values such as `"1u"`, `"10u"`, etc.
|
||||
pub fn from_iterable<I1, S1, I2, S2, I3, S3>(
|
||||
resistor: I1,
|
||||
capacitor: I2,
|
||||
inductor: I3,
|
||||
) -> Result<Self, SpecError>
|
||||
where
|
||||
I1: IntoIterator<Item = S1>,
|
||||
S1: Into<String>,
|
||||
I2: IntoIterator<Item = S2>,
|
||||
S2: Into<String>,
|
||||
I3: IntoIterator<Item = S3>,
|
||||
S3: Into<String>,
|
||||
{
|
||||
Ok(Self {
|
||||
resistor: SpecGroup::from_iterator(resistor)?,
|
||||
capacitor: SpecGroup::from_iterator(capacitor)?,
|
||||
inductor: SpecGroup::from_iterator(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a catalogue from three text blocks, one value per line.
|
||||
///
|
||||
/// * `resistor` — the resistor rated‑values text.
|
||||
/// * `capacitor` — the capacitor rated‑values text.
|
||||
/// * `inductor` — the inductor rated‑values text.
|
||||
pub fn from_text(resistor: &str, capacitor: &str, inductor: &str) -> Result<Self, SpecError> {
|
||||
Ok(Self {
|
||||
resistor: SpecGroup::from_text(resistor)?,
|
||||
capacitor: SpecGroup::from_text(capacitor)?,
|
||||
inductor: SpecGroup::from_text(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a catalogue from three files, one value per line.
|
||||
///
|
||||
/// * `resistor` — path to the resistor rated‑values file.
|
||||
/// * `capacitor` — path to the capacitor rated‑values file.
|
||||
/// * `inductor` — path to the inductor rated‑values file.
|
||||
pub fn from_file<P1, P2, P3>(
|
||||
resistor: P1,
|
||||
capacitor: P2,
|
||||
inductor: P3,
|
||||
) -> Result<Self, SpecError>
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
P3: AsRef<Path>,
|
||||
{
|
||||
Ok(Self {
|
||||
resistor: SpecGroup::from_file(resistor)?,
|
||||
capacitor: SpecGroup::from_file(capacitor)?,
|
||||
inductor: SpecGroup::from_file(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// A ready‑to‑use catalogue with common resistor, capacitor and inductor rated values.
|
||||
pub fn devices_preset() -> Self {
|
||||
Self {
|
||||
resistor: SpecGroup::resistor_preset(),
|
||||
capacitor: SpecGroup::capacitor_preset(),
|
||||
inductor: SpecGroup::inductor_preset(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return one save‑iterator for each of the three device types.
|
||||
pub fn save_iterator(
|
||||
&self,
|
||||
) -> (
|
||||
impl Iterator<Item = &str>,
|
||||
impl Iterator<Item = &str>,
|
||||
impl Iterator<Item = &str>,
|
||||
) {
|
||||
(
|
||||
self.resistor.save_iterator(),
|
||||
self.capacitor.save_iterator(),
|
||||
self.inductor.save_iterator(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the text representation of all three device‑type value sets.
|
||||
pub fn save_text(&self) -> (String, String, String) {
|
||||
(
|
||||
self.resistor.save_text(),
|
||||
self.capacitor.save_text(),
|
||||
self.inductor.save_text(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Save all three device‑type value sets to files, one value per line.
|
||||
///
|
||||
/// * `resistor` — file path for the resistor values.
|
||||
/// * `capacitor` — file path for the capacitor values.
|
||||
/// * `inductor` — file path for the inductor values.
|
||||
pub fn save_file<P1, P2, P3>(
|
||||
&self,
|
||||
resistor: P1,
|
||||
capacitor: P2,
|
||||
inductor: P3,
|
||||
) -> Result<(), SpecError>
|
||||
where
|
||||
P1: AsRef<Path>,
|
||||
P2: AsRef<Path>,
|
||||
P3: AsRef<Path>,
|
||||
{
|
||||
self.resistor.save_file(resistor)?;
|
||||
self.capacitor.save_file(capacitor)?;
|
||||
self.inductor.save_file(inductor)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Access the resistor rated‑value set.
|
||||
pub fn resistor_specs(&self) -> &SpecGroup {
|
||||
&self.resistor
|
||||
}
|
||||
|
||||
/// Access the capacitor rated‑value set.
|
||||
pub fn capacitor_specs(&self) -> &SpecGroup {
|
||||
&self.capacitor
|
||||
}
|
||||
|
||||
/// Access the inductor rated‑value set.
|
||||
pub fn inductor_specs(&self) -> &SpecGroup {
|
||||
&self.inductor
|
||||
}
|
||||
}
|
||||
|
||||
// region: Human Readable Value
|
||||
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseHumanReadableValueError {
|
||||
#[error("fail to parse floating point part of given human readable value: {0}")]
|
||||
ParseFloat(#[from] ParseFloatError),
|
||||
#[error("arithmetic error: {0}")]
|
||||
BadArithmetic(#[from] FloatingPointError),
|
||||
}
|
||||
|
||||
/// Convert human readable value to float.
|
||||
///
|
||||
/// `strl` is the human readable value.
|
||||
/// The return value is the parsed float value. or error occurs when parsing.
|
||||
///
|
||||
/// This function guarantee that return value must be a valid floating value.
|
||||
/// But do not guarantee that it can be used as device value.
|
||||
/// It is possible that it is negative or zero floating point value.
|
||||
pub fn from_human_readable_value(strl: &str) -> Result<f64, ParseHumanReadableValueError> {
|
||||
let strl = strl.trim();
|
||||
|
||||
let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') {
|
||||
(stripped, 1e-12)
|
||||
} else if let Some(stripped) = strl.strip_suffix('p') {
|
||||
(stripped, 1e-9)
|
||||
} else if let Some(stripped) = strl.strip_suffix('u') {
|
||||
(stripped, 1e-6)
|
||||
} else if let Some(stripped) = strl.strip_suffix('m') {
|
||||
(stripped, 1e-3)
|
||||
} else if let Some(stripped) = strl.strip_suffix('k') {
|
||||
(stripped, 1e3)
|
||||
} else if let Some(stripped) = strl.strip_suffix('M') {
|
||||
(stripped, 1e6)
|
||||
} else if let Some(stripped) = strl.strip_suffix('G') {
|
||||
(stripped, 1e9)
|
||||
} else {
|
||||
(strl, 1.0)
|
||||
};
|
||||
|
||||
let num = num_part.parse::<f64>()?;
|
||||
Ok(validate_floating_point(num * multiplier)?)
|
||||
}
|
||||
|
||||
/// The unit scale for human readable value.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UnitScale {
|
||||
NanoLower,
|
||||
Nano,
|
||||
Micro,
|
||||
Milli,
|
||||
None,
|
||||
Kilo,
|
||||
Mega,
|
||||
Giga,
|
||||
GigaHigher,
|
||||
}
|
||||
|
||||
/// Get the unit scale of human readable value.
|
||||
///
|
||||
/// `v` is the value for analyzing scale.
|
||||
/// It must be a valid floating point value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics when given floating point value is bad.
|
||||
pub fn get_human_readable_value_scale(v: f64) -> UnitScale {
|
||||
let v = validate_floating_point(v).expect("unexpected bad floating point value");
|
||||
let v = v.abs();
|
||||
if v < 1e-12 {
|
||||
UnitScale::NanoLower
|
||||
} else if v < 1e-9 {
|
||||
UnitScale::Nano
|
||||
} else if v < 1e-6 {
|
||||
UnitScale::Micro
|
||||
} else if v < 1e-3 {
|
||||
UnitScale::Milli
|
||||
} else if v < 1e3 {
|
||||
UnitScale::None
|
||||
} else if v < 1e6 {
|
||||
UnitScale::Kilo
|
||||
} else if v < 1e9 {
|
||||
UnitScale::Mega
|
||||
} else if v < 1e12 {
|
||||
UnitScale::Giga
|
||||
} else {
|
||||
UnitScale::GigaHigher
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert float value to human readable value.
|
||||
///
|
||||
/// `v`is the float value for formatting as human readable value.
|
||||
/// It must be a valid floating point value.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This function panics when given floating point value is bad.
|
||||
pub fn to_human_readable_value(v: f64) -> String {
|
||||
let scale = get_human_readable_value_scale(v);
|
||||
match scale {
|
||||
UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12),
|
||||
UnitScale::Nano => format!("{:+.4} p", v / 1e-9),
|
||||
UnitScale::Micro => format!("{:+.4} u", v / 1e-6),
|
||||
UnitScale::Milli => format!("{:+.4} m", v / 1e-3),
|
||||
// YYC MARK:
|
||||
// The space of this format string is by design
|
||||
// for keeping the same style with other format strings.
|
||||
UnitScale::None => format!("{:+.4} ", v),
|
||||
UnitScale::Kilo => format!("{:+.4} k", v / 1e3),
|
||||
UnitScale::Mega => format!("{:+.4} M", v / 1e6),
|
||||
UnitScale::Giga => format!("{:+.4} G", v / 1e9),
|
||||
UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9),
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
Reference in New Issue
Block a user