refactor: refactor kernel dataset into spec

This commit is contained in:
2026-07-22 14:30:29 +08:00
parent 3dd8275787
commit 1514d25da1
6 changed files with 521 additions and 482 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ use anyhow::Result;
use lcrconn::{ use lcrconn::{
BfsResolver, DeviceKind, LutResolver, Request, Resolver, Response, ResponsePriority, BfsResolver, DeviceKind, LutResolver, Request, Resolver, Response, ResponsePriority,
common::{Circuit, CircuitDeviceScale, JointKind, validate_device_value, validate_floating_point}, common::{Circuit, CircuitDeviceScale, JointKind, validate_device_value, validate_floating_point},
dataset::{DatasetCollection, from_human_readable_value, to_human_readable_value}, spec::{SpecCatalog, from_human_readable_value, to_human_readable_value},
query::MAX_RESPONSE_CNT, query::MAX_RESPONSE_CNT,
}; };
use std::io::Write; use std::io::Write;
@@ -105,7 +105,7 @@ pub struct App {
impl App { impl App {
/// Create a new app with the given configuration. /// Create a new app with the given configuration.
pub fn new(config: AppConfig) -> Result<Self> { pub fn new(config: AppConfig) -> Result<Self> {
let datasets = DatasetCollection::from_file( let datasets = SpecCatalog::from_file(
config.get_resistor_dataset(), config.get_resistor_dataset(),
config.get_capacitor_dataset(), config.get_capacitor_dataset(),
config.get_inductor_dataset(), config.get_inductor_dataset(),
-457
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
pub mod common; pub mod common;
pub mod dataset; pub mod spec;
pub mod query; pub mod query;
pub mod resolver; pub mod resolver;
+16 -16
View File
@@ -2,7 +2,7 @@ use super::{Resolver, ResolverError};
use crate::common::{ use crate::common::{
Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind, JointKind, Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind, JointKind,
}; };
use crate::dataset::{Dataset, DatasetCollection}; use crate::spec::{SpecGroup, SpecCatalog};
use crate::query::{Request, Response, ResponseError}; use crate::query::{Request, Response, ResponseError};
use itertools::Itertools; use itertools::Itertools;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
@@ -208,7 +208,7 @@ impl ResultBucket {
/// A resolver that uses brute-force search to find the best matching circuits. /// A resolver that uses brute-force search to find the best matching circuits.
pub struct BfsResolver { pub struct BfsResolver {
/// The datasets for all device kinds. /// The datasets for all device kinds.
datasets: DatasetCollection, datasets: SpecCatalog,
} }
impl BfsResolver { impl BfsResolver {
@@ -222,21 +222,21 @@ impl BfsResolver {
/// Iterate all possible circuits with one device without repeating equivalent topology. /// Iterate all possible circuits with one device without repeating equivalent topology.
pub fn iter_one_device_circuit( pub fn iter_one_device_circuit(
dataset: &Dataset, dataset: &SpecGroup,
) -> impl Iterator<Item = Result<Circuit, CircuitError>> { ) -> impl Iterator<Item = Result<Circuit, CircuitError>> {
// Every single device is unique so we directly output them. // Every single device is unique so we directly output them.
// This feature is insured by dataset itself. // 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. /// Iterate all possible circuits with two devices without repeating equivalent topology.
pub fn iter_two_devices_circuit( pub fn iter_two_devices_circuit(
dataset: &Dataset, dataset: &SpecGroup,
) -> impl Iterator<Item = Result<Circuit, CircuitError>> { ) -> impl Iterator<Item = Result<Circuit, CircuitError>> {
// The two devices in this circuit is always swapable, // The two devices in this circuit is always swapable,
// so we iterate them without repeating. // so we iterate them without repeating.
itertools::iproduct!( itertools::iproduct!(
dataset.values().array_combinations_with_replacement::<2>(), dataset.specs().array_combinations_with_replacement::<2>(),
JointKind::iter() JointKind::iter()
) )
.map(|([v1, v2], j2)| Circuit::from_two_devices(v1, v2, j2)) .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. /// Iterate all possible circuits with three devices without repeating equivalent topology.
pub fn iter_three_devices_circuit( pub fn iter_three_devices_circuit(
dataset: &Dataset, dataset: &SpecGroup,
) -> impl Iterator<Item = Result<Circuit, CircuitError>> { ) -> impl Iterator<Item = Result<Circuit, CircuitError>> {
// For generating three devices circuit, // For generating three devices circuit,
// it should be consisted by 2 parts. // it should be consisted by 2 parts.
@@ -252,15 +252,15 @@ impl BfsResolver {
// First, the whole circuit has only one joint type. // First, the whole circuit has only one joint type.
// In this case, 3 devices are swapable and we should iterate them without repeating // In this case, 3 devices are swapable and we should iterate them without repeating
itertools::iproduct!( itertools::iproduct!(
dataset.values().array_combinations_with_replacement::<3>(), dataset.specs().array_combinations_with_replacement::<3>(),
JointKind::iter() JointKind::iter()
) )
.map(|([v1, v2, v3], j)| Circuit::from_three_devices(v1, v2, j, v3, j)), .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. // Second, if the joint type is different, then the first 2 devices are swapable.
// So we need iterate them without repeating. // So we need iterate them without repeating.
itertools::iproduct!( itertools::iproduct!(
dataset.values().array_combinations_with_replacement::<2>(), dataset.specs().array_combinations_with_replacement::<2>(),
dataset.values(), dataset.specs(),
JointKind::iter() JointKind::iter()
) )
.map(|([v1, v2], v3, j)| Circuit::from_three_devices( .map(|([v1, v2], v3, j)| Circuit::from_three_devices(
@@ -276,20 +276,20 @@ impl BfsResolver {
impl BfsResolver { impl BfsResolver {
/// Create a new BFS resolver with the given datasets. /// Create a new BFS resolver with the given datasets.
pub fn new(datasets: DatasetCollection) -> Self { pub fn new(datasets: SpecCatalog) -> Self {
Self { datasets } Self { datasets }
} }
fn pick_dataset(&self, device_kind: DeviceKind) -> &Dataset { fn pick_dataset(&self, device_kind: DeviceKind) -> &SpecGroup {
match device_kind { match device_kind {
DeviceKind::Resistor => self.datasets.resistor_dataset(), DeviceKind::Resistor => self.datasets.resistor_specs(),
DeviceKind::Capacitor => self.datasets.capacitor_dataset(), DeviceKind::Capacitor => self.datasets.capacitor_specs(),
DeviceKind::Inductor => self.datasets.inductor_dataset(), DeviceKind::Inductor => self.datasets.inductor_specs(),
} }
} }
fn bfs_iteration( fn bfs_iteration(
dataset: &Dataset, dataset: &SpecGroup,
ccalc: &CircuitCalculator, ccalc: &CircuitCalculator,
) -> impl Iterator<Item = Result<BfsItem, BfsResolverError>> { ) -> impl Iterator<Item = Result<BfsItem, BfsResolverError>> {
itertools::chain!( itertools::chain!(
+6 -6
View File
@@ -1,7 +1,7 @@
use super::bfs::BfsResolver; use super::bfs::BfsResolver;
use super::{Resolver, ResolverError}; use super::{Resolver, ResolverError};
use crate::common::{Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind}; 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 crate::query::{Request, Response, ResponseError};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use thiserror::Error as TeError; use thiserror::Error as TeError;
@@ -127,16 +127,16 @@ pub struct LutResolver {
impl LutResolver { impl LutResolver {
/// Create a new LUT resolver by building lookup tables from the given datasets. /// 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 { Ok(Self {
resistor_lut: Self::build_lut(datasets.resistor_dataset(), DeviceKind::Resistor)?, resistor_lut: Self::build_lut(datasets.resistor_specs(), DeviceKind::Resistor)?,
capacitor_lut: Self::build_lut(datasets.capacitor_dataset(), DeviceKind::Capacitor)?, capacitor_lut: Self::build_lut(datasets.capacitor_specs(), DeviceKind::Capacitor)?,
inductor_lut: Self::build_lut(datasets.inductor_dataset(), DeviceKind::Inductor)?, inductor_lut: Self::build_lut(datasets.inductor_specs(), DeviceKind::Inductor)?,
}) })
} }
fn build_lut( fn build_lut(
dataset: &Dataset, dataset: &SpecGroup,
device_kind: DeviceKind, device_kind: DeviceKind,
) -> Result<Vec<LutItem>, LutResolverError> { ) -> Result<Vec<LutItem>, LutResolverError> {
// Fetch all items // Fetch all items
+496
View File
@@ -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 4700Ohms).
///
/// 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 (E12derived).
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 devicetype spec groups.
pub fn new(resistor: SpecGroup, capacitor: SpecGroup, inductor: SpecGroup) -> Self {
Self {
resistor,
capacitor,
inductor,
}
}
/// Build a catalogue from three iterables of humanreadable 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 ratedvalues text.
/// * `capacitor` — the capacitor ratedvalues text.
/// * `inductor` — the inductor ratedvalues 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 ratedvalues file.
/// * `capacitor` — path to the capacitor ratedvalues file.
/// * `inductor` — path to the inductor ratedvalues 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 readytouse 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 saveiterator 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 devicetype 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 devicetype 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 ratedvalue set.
pub fn resistor_specs(&self) -> &SpecGroup {
&self.resistor
}
/// Access the capacitor ratedvalue set.
pub fn capacitor_specs(&self) -> &SpecGroup {
&self.capacitor
}
/// Access the inductor ratedvalue 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