Compare commits
10
Commits
9d9c0473e8
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782bd86407 | ||
|
|
5a41c6266c | ||
|
|
1514d25da1 | ||
|
|
3dd8275787 | ||
|
|
5bfdfb71c2 | ||
|
|
bbd47006d4 | ||
|
|
4af8dd97a2 | ||
|
|
a62ed05d50 | ||
|
|
24e4c595bb | ||
|
|
1d9322f300 |
Generated
+50
-1
@@ -52,6 +52,18 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
@@ -98,6 +110,12 @@ version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -110,10 +128,21 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lcrconn"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"ordered-float",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"thiserror",
|
||||
@@ -123,9 +152,20 @@ dependencies = [
|
||||
name = "lcrconn-cli"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"lcrconn",
|
||||
"thiserror",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -134,6 +174,15 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@ resolver = "3"
|
||||
members = ["lcrconn", "lcrconn-cli"]
|
||||
|
||||
[workspace.dependencies]
|
||||
thiserror = "2.0.12"
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
anyhow = "1.0.103"
|
||||
lcrconn = { path="../lcrconn" }
|
||||
clap = { version="4.5.48", features=["derive"]}
|
||||
strum = "=0.28.0"
|
||||
strum_macros = "=0.28.0"
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
use crate::cli::{AppConfig, AppResolver};
|
||||
use anyhow::Result;
|
||||
use lcrconn::{
|
||||
BfsResolver, DeviceKind, LutResolver, Request, Resolver, Response, ResponsePriority,
|
||||
common::{Circuit, CircuitDeviceScale, JointKind, validate_device_value, validate_floating_point},
|
||||
spec::{SpecCatalog, from_human_readable_value, to_human_readable_value},
|
||||
query::MAX_RESPONSE_CNT,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use strum_macros::EnumString;
|
||||
|
||||
// region: App Utility Enums
|
||||
|
||||
/// The command for the main menu.
|
||||
#[derive(Debug, Clone, Copy, EnumString)]
|
||||
enum MainCmd {
|
||||
#[strum(serialize = "query")]
|
||||
Query,
|
||||
#[strum(serialize = "help")]
|
||||
Help,
|
||||
#[strum(serialize = "exit")]
|
||||
Exit,
|
||||
}
|
||||
|
||||
/// The device choice for query.
|
||||
#[derive(Debug, Clone, Copy, EnumString)]
|
||||
enum QueryDeviceChoice {
|
||||
#[strum(serialize = "r")]
|
||||
Resistor,
|
||||
#[strum(serialize = "c")]
|
||||
Capacitor,
|
||||
#[strum(serialize = "l")]
|
||||
Inductor,
|
||||
}
|
||||
|
||||
impl QueryDeviceChoice {
|
||||
fn to_device_kind(self) -> DeviceKind {
|
||||
match self {
|
||||
Self::Resistor => DeviceKind::Resistor,
|
||||
Self::Capacitor => DeviceKind::Capacitor,
|
||||
Self::Inductor => DeviceKind::Inductor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The sort priority for query results.
|
||||
#[derive(Debug, Clone, Copy, EnumString)]
|
||||
enum QuerySortPriority {
|
||||
#[strum(serialize = "l")]
|
||||
LessDevices,
|
||||
#[strum(serialize = "a")]
|
||||
MoreAccuracy,
|
||||
}
|
||||
|
||||
impl QuerySortPriority {
|
||||
fn to_response_priority(self) -> ResponsePriority {
|
||||
match self {
|
||||
Self::LessDevices => ResponsePriority::LessDevices,
|
||||
Self::MoreAccuracy => ResponsePriority::MoreAccuracy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The command for the page viewer.
|
||||
#[derive(Debug, Clone, Copy, EnumString)]
|
||||
enum PageViewerCmd {
|
||||
#[strum(serialize = "f")]
|
||||
PreviousPage,
|
||||
#[strum(serialize = "b")]
|
||||
NextPage,
|
||||
#[strum(serialize = "q")]
|
||||
Quit,
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: App Utility Functions
|
||||
|
||||
/// Read a single line from stdin, trimmed of surrounding whitespace.
|
||||
fn read_line() -> Result<String> {
|
||||
let mut line = String::new();
|
||||
std::io::stdin().read_line(&mut line)?;
|
||||
Ok(line.trim().to_string())
|
||||
}
|
||||
|
||||
/// Get the unit string for a device kind.
|
||||
fn get_device_unit(device_kind: DeviceKind) -> &'static str {
|
||||
match device_kind {
|
||||
// YYC MARK: This is ohm char.
|
||||
DeviceKind::Resistor => "\u{2126}",
|
||||
DeviceKind::Capacitor => "F",
|
||||
DeviceKind::Inductor => "H",
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
/// The app.
|
||||
pub struct App {
|
||||
/// The resolver for the app.
|
||||
resolver: Box<dyn Resolver>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create a new app with the given configuration.
|
||||
pub fn new(config: AppConfig) -> Result<Self> {
|
||||
let sepcs = SpecCatalog::from_file(
|
||||
config.get_resistor_spec(),
|
||||
config.get_capacitor_specs(),
|
||||
config.get_inductor_specs(),
|
||||
)?;
|
||||
|
||||
let resolver: Box<dyn Resolver> = match config.get_resolver() {
|
||||
AppResolver::Lut => Box::new(LutResolver::new(&sepcs)?),
|
||||
AppResolver::Bfs => Box::new(BfsResolver::new(sepcs)),
|
||||
};
|
||||
|
||||
Ok(Self { resolver })
|
||||
}
|
||||
|
||||
/// Run the app.
|
||||
pub fn run(&self) -> Result<()> {
|
||||
println!("LCR Connector");
|
||||
println!(r#"Type "help" for more info. Type "exit" to quit."#);
|
||||
self.op_main()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// region: Subcommand Processors
|
||||
|
||||
fn op_main(&self) -> Result<()> {
|
||||
loop {
|
||||
match self.accept_command::<MainCmd>()? {
|
||||
MainCmd::Query => self.op_query()?,
|
||||
MainCmd::Help => {
|
||||
println!("LCR Connector Help:");
|
||||
println!();
|
||||
println!("query: do a query.");
|
||||
println!("help: show all command.");
|
||||
println!("exit: exit this app.");
|
||||
}
|
||||
MainCmd::Exit => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn op_query(&self) -> Result<()> {
|
||||
// collecting request infos
|
||||
println!("What are you connecting?");
|
||||
println!("r: resistor");
|
||||
println!("l: inductor");
|
||||
println!("c: capacitor");
|
||||
let device_kind = self.accept_command::<QueryDeviceChoice>()?.to_device_kind();
|
||||
|
||||
println!("Your target value?");
|
||||
println!(r#"Example: "2.1k", "0.75m", "3.2M" and etc."#);
|
||||
let target_value = self.accept_device_value()?;
|
||||
|
||||
println!("Your tolerance?");
|
||||
println!(r#"It can be absolute value like "2.1k"."#);
|
||||
println!(r#"Or relative value to your target value like "19.5%"."#);
|
||||
let tolerance = self.accept_device_value_tolerance(target_value)?;
|
||||
|
||||
println!("How to sort result?");
|
||||
println!("a: more accuracy");
|
||||
println!("l: less component");
|
||||
let response_priority = self
|
||||
.accept_command::<QuerySortPriority>()?
|
||||
.to_response_priority();
|
||||
|
||||
println!("How may result are you expected?");
|
||||
let count_limit = self.accept_count_value()?;
|
||||
|
||||
// build request and ask resolver
|
||||
let request = Request::new(
|
||||
device_kind,
|
||||
target_value,
|
||||
tolerance,
|
||||
response_priority,
|
||||
count_limit,
|
||||
)?;
|
||||
let response = self.resolver.resolve(&request)?;
|
||||
|
||||
// use page viewer to show result
|
||||
self.op_page_viewer(&response)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn op_page_viewer(&self, response: &Response) -> Result<()> {
|
||||
let cnt = response.len();
|
||||
if cnt == 0 {
|
||||
println!("Sorry, no result!");
|
||||
println!("Please consider adjusting your requirements and try again.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE: usize = 10;
|
||||
let all_page = cnt / ITEMS_PER_PAGE;
|
||||
let mut current_page = 0usize;
|
||||
|
||||
loop {
|
||||
// print list
|
||||
for i in 0..ITEMS_PER_PAGE - 1 {
|
||||
// build index and check it
|
||||
let index = current_page * (ITEMS_PER_PAGE - 1) + i;
|
||||
if index >= cnt {
|
||||
continue;
|
||||
}
|
||||
// and print it
|
||||
self.illustrate_response(response, index)?;
|
||||
}
|
||||
|
||||
// print page footer
|
||||
println!();
|
||||
println!("Page {} of {}.", current_page + 1, all_page + 1);
|
||||
println!("f: previous page. b: next page. q: quit this viewer.");
|
||||
// check command
|
||||
match self.accept_command::<PageViewerCmd>()? {
|
||||
PageViewerCmd::PreviousPage => current_page = current_page.saturating_sub(1),
|
||||
PageViewerCmd::NextPage => current_page = all_page.min(current_page + 1),
|
||||
PageViewerCmd::Quit => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Command Utilities
|
||||
|
||||
/// Accept a command from the user.
|
||||
///
|
||||
/// Loops until a valid command is entered.
|
||||
fn accept_command<T>(&self) -> Result<T>
|
||||
where
|
||||
T: FromStr,
|
||||
{
|
||||
loop {
|
||||
self.show_prompt_arrow()?;
|
||||
let words = read_line()?;
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match words.parse::<T>() {
|
||||
Ok(cmd) => return Ok(cmd),
|
||||
Err(_) => println!("Unknown command, please try again."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a count value from the user.
|
||||
fn accept_count_value(&self) -> Result<usize> {
|
||||
loop {
|
||||
self.show_prompt_arrow()?;
|
||||
let words = read_line()?;
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match words.parse::<usize>() {
|
||||
Ok(value) => {
|
||||
if value > MAX_RESPONSE_CNT || value == 0 {
|
||||
println!("Wrong value, please try again.");
|
||||
} else {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Wrong value, please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a device value from the user.
|
||||
fn accept_device_value(&self) -> Result<f64> {
|
||||
loop {
|
||||
self.show_prompt_arrow()?;
|
||||
let words = read_line()?;
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = self.parse_human_readable_value(&words);
|
||||
match value {
|
||||
Some(v) => return Ok(v),
|
||||
None => println!("Wrong value, please try again."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a tolerance value from the user.
|
||||
///
|
||||
/// The tolerance can be an absolute value (like "2.1k") or a percentage
|
||||
/// relative to the target value (like "19.5%").
|
||||
fn accept_device_value_tolerance(&self, target_value: f64) -> Result<f64> {
|
||||
loop {
|
||||
self.show_prompt_arrow()?;
|
||||
let words = read_line()?;
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value: Option<f64> = if let Some(pct_str) = words.strip_suffix('%') {
|
||||
let value = self.parse_plain_float(pct_str, |x| *x >= 0.0 && *x <= 100.0);
|
||||
value
|
||||
.map(|v| v / 100.0 * target_value)
|
||||
.map(|v| validate_device_value(v))
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
self.parse_human_readable_value(&words)
|
||||
};
|
||||
|
||||
match value {
|
||||
Some(v) => return Ok(v),
|
||||
None => println!("Wrong value, please try again."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_prompt_arrow(&self) -> Result<()> {
|
||||
print!("> ");
|
||||
std::io::stdout().flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse a plain float value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_value` - The value to parse.
|
||||
/// * `checker` - A function that checks if the input is valid.
|
||||
/// It takes a float as input and returns a bool. True means the input is valid,
|
||||
/// otherwise False.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed value if it is valid, otherwise `None`.
|
||||
fn parse_plain_float(&self, user_value: &str, checker: impl Fn(&f64) -> bool) -> Option<f64> {
|
||||
// try parsing it first then check it by checker
|
||||
let value = match user_value.parse::<f64>() {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let value = validate_floating_point(value).ok()?;
|
||||
if checker(&value) { Some(value) } else { None }
|
||||
}
|
||||
|
||||
/// Parse a human-readable device value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_value` - The value to parse.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed value if it is valid and positive, otherwise `None`.
|
||||
fn parse_human_readable_value(&self, user_value: &str) -> Option<f64> {
|
||||
// parse it
|
||||
let value = from_human_readable_value(user_value).ok()?;
|
||||
// then check its range
|
||||
if value > 0.0 { Some(value) } else { None }
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Response Display Utilities
|
||||
|
||||
/// Format a device value for display in the circuit graph.
|
||||
fn to_circuit_graph_value(&self, value: f64, device_kind: DeviceKind) -> String {
|
||||
// Remove sign and append device unit
|
||||
let hr = to_human_readable_value(value);
|
||||
let without_sign = &hr[1..];
|
||||
format!("{}{}", without_sign, get_device_unit(device_kind))
|
||||
}
|
||||
|
||||
/// Format a device value for the plan header.
|
||||
fn to_plan_head_value(&self, value: f64, device_kind: DeviceKind) -> String {
|
||||
// Remove sign and append device unit
|
||||
let hr = to_human_readable_value(value);
|
||||
let without_sign = &hr[1..];
|
||||
format!("{}{}", without_sign, get_device_unit(device_kind))
|
||||
}
|
||||
|
||||
/// Format a difference value for the plan header.
|
||||
fn to_plan_head_diff(&self, value: f64, device_kind: DeviceKind) -> String {
|
||||
// Keep the sign and append device unit
|
||||
format!(
|
||||
"{}{}",
|
||||
to_human_readable_value(value),
|
||||
get_device_unit(device_kind)
|
||||
)
|
||||
}
|
||||
|
||||
/// Format a relative difference as percentage.
|
||||
fn to_plan_head_diff_pct(&self, value: f64) -> String {
|
||||
// Keep the sign and format it as percentage style without trailing device unit
|
||||
format!("{:.2}%", value * 100.0)
|
||||
}
|
||||
|
||||
// YYC MARK:
|
||||
// The function showing circuit graph should be maintained carefully.
|
||||
// First, we want they are show in console properly,
|
||||
// And we also want they have good code view.
|
||||
//
|
||||
// I notices that the number part of the output of `to_human_readable_value` will only be
|
||||
// "+999.9999" or "+9.9999e+00". So its maximum of its length is 11, considering the possibility,
|
||||
// that the absolute value of exponential part is larger than 99, is close to zero.
|
||||
// After putting the scale unit and device unit together like " nF",
|
||||
// the whole maximum size of the built string is 14.
|
||||
//
|
||||
// So we need pick a larger number and odd number for the space for showing device value,
|
||||
// because odd value can be divided by two so it can be split as two parts equally
|
||||
// for the convenient alignment of some circuit graphs.
|
||||
// My picked value is 16.
|
||||
// So you will see that I use `:^16` for a center alignment to given string.
|
||||
//
|
||||
// After this, we also need set the padding value carefully.
|
||||
// This value should consider the length of f-string syntax, pre-defined chars and required chars.
|
||||
// To make sure a pretty showcase both in code and display.
|
||||
|
||||
/// Illustrate a response item.
|
||||
fn illustrate_response(&self, response: &Response, index: usize) -> Result<()> {
|
||||
let item = response.get(index).expect("unexpected invalid index");
|
||||
let device_kind = response.device_kind();
|
||||
// print header
|
||||
println!(
|
||||
"Plan {:<4} Value: {:<16} Diff: {} ({})",
|
||||
index + 1,
|
||||
self.to_plan_head_value(item.value(), device_kind),
|
||||
self.to_plan_head_diff(item.difference(), device_kind),
|
||||
self.to_plan_head_diff_pct(item.relative_difference()),
|
||||
);
|
||||
// print circuit graph
|
||||
self.illustrate_circuit(item.circuit(), device_kind)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Illustrate a circuit based on its device scale.
|
||||
fn illustrate_circuit(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
device_kind: DeviceKind,
|
||||
) -> Result<()> {
|
||||
match circuit.device_scale() {
|
||||
CircuitDeviceScale::One => {
|
||||
self.illustrate_one_device_circuit(circuit, device_kind);
|
||||
}
|
||||
CircuitDeviceScale::Two => {
|
||||
self.illustrate_two_device_circuit(circuit, device_kind)?;
|
||||
}
|
||||
CircuitDeviceScale::Three => {
|
||||
self.illustrate_three_device_circuit(circuit, device_kind)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Illustrate a one-device circuit.
|
||||
fn illustrate_one_device_circuit(&self, circuit: &Circuit, device_kind: DeviceKind) {
|
||||
let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||
println!("──[{:^16}]──", dev1);
|
||||
}
|
||||
|
||||
/// Illustrate a two-device circuit.
|
||||
fn illustrate_two_device_circuit(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
device_kind: DeviceKind,
|
||||
) -> Result<()> {
|
||||
let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||
let j2 = circuit.second_device_joint()?;
|
||||
let dev2 = self.to_circuit_graph_value(circuit.second_device_value()?, device_kind);
|
||||
match j2 {
|
||||
JointKind::Series => {
|
||||
println!("──[{:^16}]──[{:^16}]──", dev1, dev2);
|
||||
}
|
||||
JointKind::Parallel => {
|
||||
let sep0 = " ".repeat(6 + (16 - 10));
|
||||
println!(" ┌──[{:^16}]──┐ ", dev1);
|
||||
println!("──┤ {} ├──", sep0);
|
||||
println!(" └──[{:^16}]──┘ ", dev2);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Illustrate a three-device circuit.
|
||||
fn illustrate_three_device_circuit(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
device_kind: DeviceKind,
|
||||
) -> Result<()> {
|
||||
let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||
let j2 = circuit.second_device_joint()?;
|
||||
let dev2 = self.to_circuit_graph_value(circuit.second_device_value()?, device_kind);
|
||||
let j3 = circuit.third_device_joint()?;
|
||||
let dev3 = self.to_circuit_graph_value(circuit.third_device_value()?, device_kind);
|
||||
match j2 {
|
||||
JointKind::Series => match j3 {
|
||||
JointKind::Series => {
|
||||
// All in series
|
||||
println!("──[{dev1:^16}]──[{dev2:^16}]──[{dev3:^16}]──");
|
||||
}
|
||||
JointKind::Parallel => {
|
||||
// First series then parallel
|
||||
let sep0 = "─".repeat(6 + ((16 - 10) / 2));
|
||||
let sep1 = " ".repeat(6 + 2 * (16 - 10));
|
||||
println!(" ┌──[{dev1:^16}]──[{dev2:^16}]──┐ ");
|
||||
println!("──┤ {sep1} ├──");
|
||||
println!(" └───{sep0}[{dev3:^16}]{sep0}───┘ ");
|
||||
}
|
||||
},
|
||||
JointKind::Parallel => match j3 {
|
||||
JointKind::Series => {
|
||||
// First parallel then series
|
||||
let sep0 = " ".repeat(6 + (16 - 10));
|
||||
println!(" {sep0} ┌──[{dev1:^16}]──┐ ");
|
||||
println!("──[{dev3:^16}]──┤ {sep0} ├──");
|
||||
println!(" {sep0} └──[{dev2:^16}]──┘ ");
|
||||
}
|
||||
JointKind::Parallel => {
|
||||
// All in parallel
|
||||
println!(" ┌──[{dev1:^16}]──┐ ");
|
||||
println!("──┼──[{dev2:^16}]──┼──");
|
||||
println!(" └──[{dev3:^16}]──┘ ");
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::{Parser, ValueEnum};
|
||||
|
||||
/// The configuration for the app.
|
||||
pub struct AppConfig {
|
||||
/// The resolver for the app.
|
||||
resolver: AppResolver,
|
||||
/// The path to the resistor specs file.
|
||||
resistor_specs: PathBuf,
|
||||
/// The path to the capacitor specs file.
|
||||
capacitor_specs: PathBuf,
|
||||
/// The path to the inductor specs file.
|
||||
inductor_specs: PathBuf,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
/// Get the resolver.
|
||||
pub fn get_resolver(&self) -> &AppResolver {
|
||||
&self.resolver
|
||||
}
|
||||
/// Get the path to the resistor specs file.
|
||||
pub fn get_resistor_spec(&self) -> &Path {
|
||||
&self.resistor_specs
|
||||
}
|
||||
/// Get the path to the capacitor specs file.
|
||||
pub fn get_capacitor_specs(&self) -> &Path {
|
||||
&self.capacitor_specs
|
||||
}
|
||||
/// Get the path to the inductor specs file.
|
||||
pub fn get_inductor_specs(&self) -> &Path {
|
||||
&self.inductor_specs
|
||||
}
|
||||
}
|
||||
|
||||
/// The resolver for the app.
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
pub enum AppResolver {
|
||||
/// The look-up table resolver.
|
||||
#[value(name = "lut")]
|
||||
Lut,
|
||||
/// The BFS resolver.
|
||||
#[value(name = "bfs")]
|
||||
Bfs,
|
||||
}
|
||||
|
||||
/// Get the resistor, capacitor, or inductor circuit which has the closest value
|
||||
/// for your given value within at most 3 devices.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "LCR Connector",
|
||||
version,
|
||||
about = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices."
|
||||
)]
|
||||
struct Cli {
|
||||
/// The resolver you want to use.
|
||||
#[arg(short = 's', long = "resolver", required = true, value_enum)]
|
||||
resolver: AppResolver,
|
||||
|
||||
/// The path to the resistor specs file.
|
||||
#[arg(
|
||||
short = 'r',
|
||||
long = "resistor",
|
||||
required = true,
|
||||
value_name = "RESISTOR.TXT"
|
||||
)]
|
||||
resistor_specs: PathBuf,
|
||||
|
||||
/// The path to the inductor specs file.
|
||||
#[arg(
|
||||
short = 'l',
|
||||
long = "inductor",
|
||||
required = true,
|
||||
value_name = "INDUCTOR.TXT"
|
||||
)]
|
||||
inductor_specs: PathBuf,
|
||||
|
||||
/// The path to the capacitor specs file.
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "capacitor",
|
||||
required = true,
|
||||
value_name = "CAPACITOR.TXT"
|
||||
)]
|
||||
capacitor_specs: PathBuf,
|
||||
}
|
||||
|
||||
impl From<Cli> for AppConfig {
|
||||
fn from(args: Cli) -> Self {
|
||||
Self {
|
||||
resolver: args.resolver,
|
||||
resistor_specs: args.resistor_specs,
|
||||
capacitor_specs: args.capacitor_specs,
|
||||
inductor_specs: args.inductor_specs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_args() -> AppConfig {
|
||||
let args = Cli::parse();
|
||||
let config = AppConfig::from(args);
|
||||
config
|
||||
}
|
||||
@@ -1,641 +1,15 @@
|
||||
use std::io::{self, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
use lcrconn::{
|
||||
from_human_readable_value, to_human_readable_value, BfsResolver, Circuit, CircuitDeviceScale,
|
||||
DatasetCollection, DeviceKind, JointKind, LcrConnError, LutResolver, Request, Resolver,
|
||||
Response, ResponsePriority, MAX_RESPONSE_CNT,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Command-line arguments
|
||||
// ============================================================================
|
||||
|
||||
/// The resolver for the app.
|
||||
#[derive(Clone, Debug, clap::ValueEnum)]
|
||||
pub enum AppResolver {
|
||||
/// The look-up table resolver.
|
||||
#[value(name = "lut")]
|
||||
Lut,
|
||||
/// The BFS resolver.
|
||||
#[value(name = "bfs")]
|
||||
Bfs,
|
||||
}
|
||||
|
||||
/// The configuration for the app.
|
||||
struct AppConfig {
|
||||
/// The resolver for the app.
|
||||
resolver: AppResolver,
|
||||
/// The path to the resistor dataset file.
|
||||
resistor_dataset: PathBuf,
|
||||
/// The path to the capacitor dataset file.
|
||||
capacitor_dataset: PathBuf,
|
||||
/// The path to the inductor dataset file.
|
||||
inductor_dataset: PathBuf,
|
||||
}
|
||||
|
||||
/// Get the resistor, capacitor, or inductor circuit which has the closest value
|
||||
/// for your given value within at most 3 devices.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "LCR Connector",
|
||||
about = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices."
|
||||
)]
|
||||
struct Args {
|
||||
/// The resolver you want to use.
|
||||
#[arg(short = 's', long)]
|
||||
resolver: AppResolver,
|
||||
|
||||
/// The path to the resistor dataset file.
|
||||
#[arg(short = 'r', long, value_name = "RESISTOR.TXT")]
|
||||
resistor_dataset: PathBuf,
|
||||
|
||||
/// The path to the inductor dataset file.
|
||||
#[arg(short = 'l', long, value_name = "INDUCTOR.TXT")]
|
||||
inductor_dataset: PathBuf,
|
||||
|
||||
/// The path to the capacitor dataset file.
|
||||
#[arg(short = 'c', long, value_name = "CAPACITOR.TXT")]
|
||||
capacitor_dataset: PathBuf,
|
||||
}
|
||||
|
||||
impl From<Args> for AppConfig {
|
||||
fn from(args: Args) -> Self {
|
||||
Self {
|
||||
resolver: args.resolver,
|
||||
resistor_dataset: args.resistor_dataset,
|
||||
capacitor_dataset: args.capacitor_dataset,
|
||||
inductor_dataset: args.inductor_dataset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Interactive command enums
|
||||
// ============================================================================
|
||||
|
||||
/// The command for the main menu.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum MainCmd {
|
||||
Query,
|
||||
Help,
|
||||
Exit,
|
||||
}
|
||||
|
||||
fn parse_main_cmd(s: &str) -> Option<MainCmd> {
|
||||
match s {
|
||||
"query" => Some(MainCmd::Query),
|
||||
"help" => Some(MainCmd::Help),
|
||||
"exit" => Some(MainCmd::Exit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The device choice for query.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum QueryDeviceChoice {
|
||||
Resistor,
|
||||
Capacitor,
|
||||
Inductor,
|
||||
}
|
||||
|
||||
impl QueryDeviceChoice {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"r" => Some(Self::Resistor),
|
||||
"c" => Some(Self::Capacitor),
|
||||
"l" => Some(Self::Inductor),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_device_kind(self) -> DeviceKind {
|
||||
match self {
|
||||
Self::Resistor => DeviceKind::Resistor,
|
||||
Self::Capacitor => DeviceKind::Capacitor,
|
||||
Self::Inductor => DeviceKind::Inductor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The sort priority for query results.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum QuerySortPriority {
|
||||
LessDevices,
|
||||
MoreAccuracy,
|
||||
}
|
||||
|
||||
impl QuerySortPriority {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"l" => Some(Self::LessDevices),
|
||||
"a" => Some(Self::MoreAccuracy),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_response_priority(self) -> ResponsePriority {
|
||||
match self {
|
||||
Self::LessDevices => ResponsePriority::LessDevices,
|
||||
Self::MoreAccuracy => ResponsePriority::MoreAccuracy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The command for the page viewer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum PageViewerCmd {
|
||||
PreviousPage,
|
||||
NextPage,
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl PageViewerCmd {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"f" => Some(Self::PreviousPage),
|
||||
"b" => Some(Self::NextPage),
|
||||
"q" => Some(Self::Quit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Input utilities
|
||||
// ============================================================================
|
||||
|
||||
/// Read a single line from stdin, trimmed of surrounding whitespace.
|
||||
fn read_line() -> String {
|
||||
let mut line = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut line)
|
||||
.expect("Failed to read from stdin");
|
||||
line.trim().to_string()
|
||||
}
|
||||
|
||||
/// Parse a plain float value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_value` - The value to parse.
|
||||
/// * `checker` - A function that checks if the input is valid.
|
||||
/// It takes a float as input and returns a bool. True means the input is valid,
|
||||
/// otherwise False.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed value if it is valid, otherwise `None`.
|
||||
fn parse_plain_float(user_value: &str, checker: impl Fn(&f64) -> bool) -> Option<f64> {
|
||||
// try parsing it first
|
||||
let value = user_value.parse::<f64>().ok()?;
|
||||
// then check it by checker
|
||||
if checker(&value) {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a human-readable device value.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_value` - The value to parse.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed value if it is valid and positive, otherwise `None`.
|
||||
fn parse_human_readable_value(user_value: &str) -> Option<f64> {
|
||||
// parse it
|
||||
let value = from_human_readable_value(user_value).ok()?;
|
||||
// then check its range
|
||||
if value > 0.0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Response display utilities
|
||||
// ============================================================================
|
||||
|
||||
/// Get the unit string for a device kind.
|
||||
fn get_device_unit(device_kind: DeviceKind) -> &'static str {
|
||||
match device_kind {
|
||||
DeviceKind::Resistor => "\u{2126}",
|
||||
DeviceKind::Capacitor => "F",
|
||||
DeviceKind::Inductor => "H",
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a device value for display in the circuit graph.
|
||||
fn to_circuit_graph_value(value: f64, device_kind: DeviceKind) -> String {
|
||||
// Remove sign and append device unit
|
||||
let hr = to_human_readable_value(value);
|
||||
let without_sign = &hr[1..];
|
||||
format!("{}{}", without_sign, get_device_unit(device_kind))
|
||||
}
|
||||
|
||||
/// Format a device value for the plan header.
|
||||
fn to_plan_head_value(value: f64, device_kind: DeviceKind) -> String {
|
||||
// Remove sign and append device unit
|
||||
let hr = to_human_readable_value(value);
|
||||
let without_sign = &hr[1..];
|
||||
format!("{}{}", without_sign, get_device_unit(device_kind))
|
||||
}
|
||||
|
||||
/// Format a difference value for the plan header.
|
||||
fn to_plan_head_diff(value: f64, device_kind: DeviceKind) -> String {
|
||||
// Keep the sign and append device unit
|
||||
format!("{}{}", to_human_readable_value(value), get_device_unit(device_kind))
|
||||
}
|
||||
|
||||
/// Format a relative difference as percentage.
|
||||
fn to_plan_head_diff_pct(value: f64) -> String {
|
||||
// Keep the sign and format it as percentage style without trailing device unit
|
||||
format!("{:.2}%", value * 100.0)
|
||||
}
|
||||
|
||||
// YYC MARK:
|
||||
// The function showing circuit graph should be maintained carefully.
|
||||
// First, we want they are show in console properly,
|
||||
// And we also want they have good code view.
|
||||
//
|
||||
// I notices that the number part of the output of `to_human_readable_value` will only be
|
||||
// "+999.9999" or "+9.9999e+00". So its maximum of its length is 11, considering the possibility,
|
||||
// that the absolute value of exponential part is larger than 99, is close to zero.
|
||||
// After putting the scale unit and device unit together like " nF",
|
||||
// the whole maximum size of the built string is 14.
|
||||
//
|
||||
// So we need pick a larger number and odd number for the space for showing device value,
|
||||
// because odd value can be divided by two so it can be split as two parts equally
|
||||
// for the convenient alignment of some circuit graphs.
|
||||
// My picked value is 16.
|
||||
// So you will see that I use `:^16` for a center alignment to given string.
|
||||
//
|
||||
// After this, we also need set the padding value carefully.
|
||||
// This value should consider the length of f-string syntax, pre-defined chars and required chars.
|
||||
// To make sure a pretty showcase both in code and display.
|
||||
|
||||
/// Illustrate a one-device circuit.
|
||||
fn illustrate_one_device_circuit(circuit: &Circuit, device_kind: DeviceKind) {
|
||||
let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||
println!("──[{:^16}]──", dev1);
|
||||
}
|
||||
|
||||
/// Illustrate a two-device circuit.
|
||||
fn illustrate_two_device_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> {
|
||||
let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||
let j2 = circuit.second_device_joint()?;
|
||||
let dev2 = to_circuit_graph_value(circuit.second_device_value()?, device_kind);
|
||||
match j2 {
|
||||
JointKind::Series => {
|
||||
println!("──[{:^16}]──[{:^16}]──", dev1, dev2);
|
||||
}
|
||||
JointKind::Parallel => {
|
||||
let sep0 = " ".repeat(6 + (16 - 10));
|
||||
println!(" ┌──[{:^16}]──┐ ", dev1);
|
||||
println!("──┤ {} ├──", sep0);
|
||||
println!(" └──[{:^16}]──┘ ", dev2);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Illustrate a three-device circuit.
|
||||
fn illustrate_three_device_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> {
|
||||
let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||
let j2 = circuit.second_device_joint()?;
|
||||
let dev2 = to_circuit_graph_value(circuit.second_device_value()?, device_kind);
|
||||
let j3 = circuit.third_device_joint()?;
|
||||
let dev3 = to_circuit_graph_value(circuit.third_device_value()?, device_kind);
|
||||
match j2 {
|
||||
JointKind::Series => match j3 {
|
||||
JointKind::Series => {
|
||||
// All in series
|
||||
println!("──[{:^16}]──[{:^16}]──[{:^16}]──", dev1, dev2, dev3);
|
||||
}
|
||||
JointKind::Parallel => {
|
||||
// First series then parallel
|
||||
let sep0 = "\u{2500}".repeat(6 + ((16 - 10) / 2));
|
||||
let sep1 = " ".repeat(6 + 2 * (16 - 10));
|
||||
println!(" ┌──[{:^16}]──[{:^16}]──┐ ", dev1, dev2);
|
||||
println!("──┤ {} ├──", sep1);
|
||||
println!(" └───{}[{:^16}]{}───┘ ", sep0, dev3, sep0);
|
||||
}
|
||||
},
|
||||
JointKind::Parallel => match j3 {
|
||||
JointKind::Series => {
|
||||
// First parallel then series
|
||||
let sep0 = " ".repeat(6 + (16 - 10));
|
||||
println!(" {} ┌──[{:^16}]──┐ ", sep0, dev1);
|
||||
println!("──[{:^16}]──┤ {} ├──", dev3, sep0);
|
||||
println!(" {} └──[{:^16}]──┘ ", sep0, dev2);
|
||||
}
|
||||
JointKind::Parallel => {
|
||||
// All in parallel
|
||||
println!(" ┌──[{:^16}]──┐ ", dev1);
|
||||
println!("──┼──[{:^16}]──┼──", dev2);
|
||||
println!(" └──[{:^16}]──┘ ", dev3);
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Illustrate a circuit based on its device scale.
|
||||
fn illustrate_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> {
|
||||
match circuit.device_scale() {
|
||||
CircuitDeviceScale::One => {
|
||||
illustrate_one_device_circuit(circuit, device_kind);
|
||||
}
|
||||
CircuitDeviceScale::Two => {
|
||||
illustrate_two_device_circuit(circuit, device_kind)?;
|
||||
}
|
||||
CircuitDeviceScale::Three => {
|
||||
illustrate_three_device_circuit(circuit, device_kind)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Illustrate a response item.
|
||||
fn illustrate_response(response: &Response, index: usize) -> Result<(), LcrConnError> {
|
||||
let item = &response[index];
|
||||
let device_kind = response.device_kind();
|
||||
// print header
|
||||
println!(
|
||||
"Plan {:<4} Value: {:<16} Diff: {} ({})",
|
||||
index + 1,
|
||||
to_plan_head_value(item.value(), device_kind),
|
||||
to_plan_head_diff(item.difference(), device_kind),
|
||||
to_plan_head_diff_pct(item.relative_difference()),
|
||||
);
|
||||
// print circuit graph
|
||||
illustrate_circuit(item.circuit(), device_kind)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// App
|
||||
// ============================================================================
|
||||
|
||||
/// The app.
|
||||
struct App {
|
||||
/// The resolver for the app.
|
||||
resolver: Box<dyn Resolver>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Create a new app with the given configuration.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`DatasetCollection::from_file`] and [`LutResolver::new`].
|
||||
fn new(config: AppConfig) -> Result<Self, LcrConnError> {
|
||||
let datasets = DatasetCollection::from_file(
|
||||
&config.resistor_dataset,
|
||||
&config.capacitor_dataset,
|
||||
&config.inductor_dataset,
|
||||
)?;
|
||||
|
||||
let resolver: Box<dyn Resolver> = match config.resolver {
|
||||
AppResolver::Lut => Box::new(LutResolver::new(&datasets)?),
|
||||
AppResolver::Bfs => Box::new(BfsResolver::new(datasets)),
|
||||
};
|
||||
|
||||
Ok(Self { resolver })
|
||||
}
|
||||
|
||||
/// Run the app.
|
||||
fn run(&self) -> Result<(), LcrConnError> {
|
||||
println!("LCR Connector");
|
||||
println!("Type \"help\" for more info. Type \"exit\" to quit.");
|
||||
self.op_main()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Subcommand Processors
|
||||
// ========================================================================
|
||||
|
||||
fn op_main(&self) -> Result<(), LcrConnError> {
|
||||
loop {
|
||||
match self.accept_command(parse_main_cmd) {
|
||||
MainCmd::Query => self.op_query()?,
|
||||
MainCmd::Help => {
|
||||
println!("LCR Connector Help:");
|
||||
println!();
|
||||
println!("query: do a query.");
|
||||
println!("help: show all command.");
|
||||
println!("exit: exit this app.");
|
||||
}
|
||||
MainCmd::Exit => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn op_query(&self) -> Result<(), LcrConnError> {
|
||||
// collecting request infos
|
||||
println!("What are you connecting?");
|
||||
println!("r: resistor");
|
||||
println!("l: inductor");
|
||||
println!("c: capacitor");
|
||||
let device_kind = self
|
||||
.accept_command(QueryDeviceChoice::parse)
|
||||
.to_device_kind();
|
||||
|
||||
println!("Your target value?");
|
||||
println!("Example: \"2.1k\", \"0.75m\", \"3.2M\" and etc.");
|
||||
let target_value = self.accept_device_value();
|
||||
|
||||
println!("Your tolerance?");
|
||||
println!("It can be absolute value like \"2.1k\".");
|
||||
println!("Or relative value to your target value like \"19.5%\".");
|
||||
let tolerance = self.accept_device_value_tolerance(target_value);
|
||||
|
||||
println!("How to sort result?");
|
||||
println!("a: more accuracy");
|
||||
println!("l: less component");
|
||||
let response_priority = self
|
||||
.accept_command(QuerySortPriority::parse)
|
||||
.to_response_priority();
|
||||
|
||||
println!("How may result are you expected?");
|
||||
let count_limit = self.accept_count_value();
|
||||
|
||||
// build request and ask resolver
|
||||
let request = Request::new(
|
||||
device_kind,
|
||||
target_value,
|
||||
tolerance,
|
||||
response_priority,
|
||||
count_limit,
|
||||
)?;
|
||||
let response = self.resolver.resolve(&request)?;
|
||||
|
||||
// use page viewer to show result
|
||||
self.op_page_viewer(&response)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn op_page_viewer(&self, response: &Response) -> Result<(), LcrConnError> {
|
||||
let cnt = response.len();
|
||||
if cnt == 0 {
|
||||
println!("Sorry, no result!");
|
||||
println!("Please consider adjusting your requirements and try again.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
const ITEMS_PER_PAGE: usize = 10;
|
||||
let all_page = cnt / ITEMS_PER_PAGE;
|
||||
let mut current_page = 0usize;
|
||||
|
||||
loop {
|
||||
// print list
|
||||
for i in 0..ITEMS_PER_PAGE - 1 {
|
||||
// build index and check it
|
||||
let index = current_page * (ITEMS_PER_PAGE - 1) + i;
|
||||
if index >= cnt {
|
||||
continue;
|
||||
}
|
||||
// and print it
|
||||
illustrate_response(response, index)?;
|
||||
}
|
||||
|
||||
// print page footer
|
||||
println!();
|
||||
println!("Page {} of {}.", current_page + 1, all_page + 1);
|
||||
println!("f: previous page. b: next page. q: quit this viewer.");
|
||||
// check command
|
||||
match self.accept_command(PageViewerCmd::parse) {
|
||||
PageViewerCmd::PreviousPage => current_page = current_page.saturating_sub(1),
|
||||
PageViewerCmd::NextPage => current_page = all_page.min(current_page + 1),
|
||||
PageViewerCmd::Quit => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Command Utilities
|
||||
// ========================================================================
|
||||
|
||||
/// Accept a command from the user.
|
||||
///
|
||||
/// Loops until a valid command is entered.
|
||||
fn accept_command<T>(&self, parser: impl Fn(&str) -> Option<T>) -> T {
|
||||
loop {
|
||||
self.show_prompt_arrow();
|
||||
let words = read_line();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match parser(&words) {
|
||||
Some(cmd) => return cmd,
|
||||
None => println!("Unknown command, please try again."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a count value from the user.
|
||||
fn accept_count_value(&self) -> usize {
|
||||
loop {
|
||||
self.show_prompt_arrow();
|
||||
let words = read_line();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match words.parse::<usize>() {
|
||||
Ok(value) => {
|
||||
if value > MAX_RESPONSE_CNT || value == 0 {
|
||||
println!("Wrong value, please try again.");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("Wrong value, please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a device value from the user.
|
||||
fn accept_device_value(&self) -> f64 {
|
||||
loop {
|
||||
self.show_prompt_arrow();
|
||||
let words = read_line();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = parse_human_readable_value(&words);
|
||||
match value {
|
||||
Some(v) => return v,
|
||||
None => println!("Wrong value, please try again."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accept a tolerance value from the user.
|
||||
///
|
||||
/// The tolerance can be an absolute value (like "2.1k") or a percentage
|
||||
/// relative to the target value (like "19.5%").
|
||||
fn accept_device_value_tolerance(&self, target_value: f64) -> f64 {
|
||||
loop {
|
||||
self.show_prompt_arrow();
|
||||
let words = read_line();
|
||||
if words.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value: Option<f64> = if let Some(pct_str) = words.strip_suffix('%') {
|
||||
let value = parse_plain_float(pct_str, |x| *x >= 0.0 && *x <= 100.0);
|
||||
value.map(|v| v / 100.0 * target_value)
|
||||
} else {
|
||||
parse_human_readable_value(&words)
|
||||
};
|
||||
|
||||
match value {
|
||||
Some(v) => return v,
|
||||
None => println!("Wrong value, please try again."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_prompt_arrow(&self) {
|
||||
print!("> ");
|
||||
io::stdout().flush().expect("Failed to flush stdout");
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Entry point
|
||||
// ============================================================================
|
||||
mod app;
|
||||
mod cli;
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
let config = AppConfig::from(args);
|
||||
let config = cli::parse_args();
|
||||
|
||||
let app = match App::new(config) {
|
||||
Ok(app) => app,
|
||||
Err(e) => {
|
||||
eprintln!("Error: {}", e);
|
||||
let app = app::App::new(config).unwrap_or_else(|err| {
|
||||
eprintln!("Fail to initialize application: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = app.run() {
|
||||
eprintln!("Error: {}", e);
|
||||
});
|
||||
app.run().unwrap_or_else(|err| {
|
||||
eprintln!("Runtime error: {}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ version = "1.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
thiserror = "2.0.12"
|
||||
ordered-float = "=5.3.0"
|
||||
itertools = "0.15.0"
|
||||
strum = "=0.28.0"
|
||||
strum_macros = "=0.28.0"
|
||||
|
||||
+93
-166
@@ -1,13 +1,15 @@
|
||||
use strum_macros::EnumIter;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
// region: Sanitizer
|
||||
// region: Validator
|
||||
|
||||
/// Error occurs when validating floating point value.
|
||||
#[derive(Debug, TeError)]
|
||||
#[error("given floating value {0} is invalid")]
|
||||
#[error("given floating point value {0} is invalid")]
|
||||
pub struct FloatingPointError(f64);
|
||||
|
||||
pub fn sanitize_floating_point(f: f64) -> Result<f64, FloatingPointError> {
|
||||
/// Check whether given floating point value is okey for arithmetic operation.
|
||||
pub fn validate_floating_point(f: f64) -> Result<f64, FloatingPointError> {
|
||||
if f.is_finite() {
|
||||
Ok(f)
|
||||
} else {
|
||||
@@ -15,16 +17,21 @@ pub fn sanitize_floating_point(f: f64) -> Result<f64, FloatingPointError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Error occurs when validating device value.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum DeviceValueError {
|
||||
#[error("{0}")]
|
||||
#[error("given device value is bad floating point: {0}")]
|
||||
BadFloatingPoint(#[from] FloatingPointError),
|
||||
#[error("given device value {0} is out of range")]
|
||||
OutOfRange(f64),
|
||||
}
|
||||
|
||||
pub fn sanitize_device_value(f: f64) -> Result<f64, DeviceValueError> {
|
||||
let f = sanitize_floating_point(f)?;
|
||||
/// Check whether given value is good for device value.
|
||||
///
|
||||
/// A good device value should be finity floating point,
|
||||
/// and it should be greater than zero.
|
||||
pub fn validate_device_value(f: f64) -> Result<f64, DeviceValueError> {
|
||||
let f = validate_floating_point(f)?;
|
||||
if f > 0f64 {
|
||||
Ok(f)
|
||||
} else {
|
||||
@@ -97,14 +104,23 @@ impl CircuitDeviceScale {
|
||||
|
||||
// region: Circuit Stuff
|
||||
|
||||
/// Error occurs when manipulating [SubCircuit].
|
||||
/// Error occurs when manipulating [Circuit] and [SubCircuit].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum SubCircuitError {
|
||||
pub enum CircuitError {
|
||||
#[error("invalid device value in circuit: {0}")]
|
||||
BadDeviceValue(DeviceValueError),
|
||||
#[error("bad previous computed circuit value: {0}")]
|
||||
#[error("third device cannot exist without second device when building circuit")]
|
||||
InterleavedSubCircuit,
|
||||
#[error("the joint or device with given index is not presented in circuit")]
|
||||
NoSuchDevice,
|
||||
|
||||
#[error("invalid target value: {0}")]
|
||||
BadTargetValue(DeviceValueError),
|
||||
#[error("invalid pre-evaluated circuit value: {0}")]
|
||||
BadCircuitValue(DeviceValueError),
|
||||
#[error("bad previous evaluated joint value: {0}")]
|
||||
BadPreviousValue(DeviceValueError),
|
||||
#[error("arithmetic error: {0}")]
|
||||
#[error("floating point is invalid after arithmetic operation: {0}")]
|
||||
BadArithmetic(FloatingPointError),
|
||||
}
|
||||
|
||||
@@ -122,24 +138,24 @@ impl SubCircuit {
|
||||
///
|
||||
/// The input device value should greater than zero,
|
||||
/// otherwise an error will return.
|
||||
pub fn new(device_value: f64, joint_kind: JointKind) -> Result<Self, SubCircuitError> {
|
||||
let device_value = sanitize_device_value(device_value)
|
||||
.map_err(|err| SubCircuitError::BadDeviceValue(err))?;
|
||||
pub fn new(device_value: f64, joint_kind: JointKind) -> Result<Self, CircuitError> {
|
||||
let device_value =
|
||||
validate_device_value(device_value).map_err(|err| CircuitError::BadDeviceValue(err))?;
|
||||
Ok(Self {
|
||||
device_value,
|
||||
joint_kind,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute the joint value with given previous computed value and device kind.
|
||||
/// Evaluate the joint value with given previous joint evaluated value and device kind.
|
||||
///
|
||||
/// Parameter `value` should be the value computed from previous devices.
|
||||
/// Parameter `value` should be the value evaluated from previous joint.
|
||||
/// And it should greater than zero.
|
||||
/// `device_kind` is the kind of the device.
|
||||
pub fn compute(&self, value: f64, device_kind: DeviceKind) -> Result<f64, SubCircuitError> {
|
||||
pub fn evaluate(&self, value: f64, device_kind: DeviceKind) -> Result<f64, CircuitError> {
|
||||
// Check the range of provided value for computing
|
||||
let value =
|
||||
sanitize_device_value(value).map_err(|err| SubCircuitError::BadPreviousValue(err))?;
|
||||
validate_device_value(value).map_err(|err| CircuitError::BadPreviousValue(err))?;
|
||||
|
||||
// We perform series connect for: series resistor, series inductor and parallel capacitor.
|
||||
// We perform parallel connect for: parallel resistor, parallel inductor and series capacitor.
|
||||
@@ -148,11 +164,11 @@ impl SubCircuit {
|
||||
_ => self.joint_kind,
|
||||
};
|
||||
|
||||
sanitize_floating_point(match joint_kind {
|
||||
validate_floating_point(match joint_kind {
|
||||
JointKind::Series => self.device_value + value,
|
||||
JointKind::Parallel => (self.device_value * value) / (self.device_value + value),
|
||||
})
|
||||
.map_err(|err| SubCircuitError::BadArithmetic(err))
|
||||
.map_err(|err| CircuitError::BadArithmetic(err))
|
||||
}
|
||||
|
||||
/// Get the device value.
|
||||
@@ -166,19 +182,6 @@ impl SubCircuit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Error occurs when manipulating [Circuit].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum CircuitError {
|
||||
#[error("invalid device value in circuit: {0}")]
|
||||
BadDeviceValue(DeviceValueError),
|
||||
#[error("third device cannot exist without second device when building circuit")]
|
||||
BlankSecondSubCircuit,
|
||||
#[error("{0}")]
|
||||
SubCircuit(#[from] SubCircuitError),
|
||||
#[error("the joint or device with given index is not presented in circuit")]
|
||||
NoSuchDevice,
|
||||
}
|
||||
|
||||
/// The circuit composed of multiple joints.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Circuit {
|
||||
@@ -193,20 +196,20 @@ pub struct Circuit {
|
||||
impl Circuit {
|
||||
/// Initialize the circuit with subcircuit.
|
||||
///
|
||||
/// * `first_device_value` - The value of the first device.
|
||||
/// * `second_device_subckt` - The second device and its joint property.
|
||||
/// * `third_device_subckt` - The third device and its joint property.
|
||||
/// - `first_device_value`: The value of the first device.
|
||||
/// - `second_device_subckt`: The second device and its joint property.
|
||||
/// - `third_device_subckt`: The third device and its joint property.
|
||||
fn new(
|
||||
first_device_value: f64,
|
||||
second_device_subckt: Option<SubCircuit>,
|
||||
third_device_subckt: Option<SubCircuit>,
|
||||
) -> Result<Self, CircuitError> {
|
||||
// Check the value of first device
|
||||
let first_device_value = sanitize_device_value(first_device_value)
|
||||
let first_device_value = validate_device_value(first_device_value)
|
||||
.map_err(|err| CircuitError::BadDeviceValue(err))?;
|
||||
// Check impossible form
|
||||
if second_device_subckt.is_none() && third_device_subckt.is_some() {
|
||||
return Err(CircuitError::BlankSecondSubCircuit);
|
||||
return Err(CircuitError::InterleavedSubCircuit);
|
||||
}
|
||||
|
||||
// Everything is okey
|
||||
@@ -250,17 +253,17 @@ impl Circuit {
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute the circuit value with given value and device kind
|
||||
pub fn compute(&self, device_kind: DeviceKind) -> Result<f64, CircuitError> {
|
||||
/// Evaluate the circuit value with device kind
|
||||
pub fn evaluate(&self, device_kind: DeviceKind) -> Result<f64, CircuitError> {
|
||||
let mut value = self.first_device_value;
|
||||
|
||||
match &self.second_device_subckt {
|
||||
Some(subckt) => value = subckt.compute(value, device_kind)?,
|
||||
Some(subckt) => value = subckt.evaluate(value, device_kind)?,
|
||||
None => return Ok(value),
|
||||
}
|
||||
|
||||
match &self.third_device_subckt {
|
||||
Some(subckt) => value = subckt.compute(value, device_kind)?,
|
||||
Some(subckt) => value = subckt.evaluate(value, device_kind)?,
|
||||
None => return Ok(value),
|
||||
}
|
||||
|
||||
@@ -320,145 +323,69 @@ impl Circuit {
|
||||
}
|
||||
}
|
||||
|
||||
/// Error occurs when manipulating [CircuitCalculator].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum CircuitCalculatorError {
|
||||
#[error("invalid target value: {0}")]
|
||||
BadTargetValue(DeviceValueError),
|
||||
#[error("{0}")]
|
||||
Circuit(#[from] CircuitError),
|
||||
#[error("arithmetic error: {0}")]
|
||||
BadArithmetic(FloatingPointError),
|
||||
#[error("bad provided value reducing computation steps: {0}")]
|
||||
BadReuseValue(FloatingPointError),
|
||||
}
|
||||
|
||||
/// The helper for circuit value computation.
|
||||
/// The evaluation result of circuit with target value.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CircuitCalculator {
|
||||
/// The kind of the device.
|
||||
device_kind: DeviceKind,
|
||||
/// The target value.
|
||||
target_value: f64,
|
||||
}
|
||||
|
||||
impl CircuitCalculator {
|
||||
/// Initialize this calculator with given device kind and target value.
|
||||
pub fn new(device_kind: DeviceKind, target_value: f64) -> Result<Self, CircuitCalculatorError> {
|
||||
let target_value = sanitize_device_value(target_value)
|
||||
.map_err(|err| CircuitCalculatorError::BadTargetValue(err))?;
|
||||
Ok(Self {
|
||||
device_kind,
|
||||
target_value,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct CircuitEvaluation {
|
||||
/// The value of this circuit.
|
||||
pub fn value(&self, circuit: &Circuit) -> Result<f64, CircuitCalculatorError> {
|
||||
Ok(circuit.compute(self.device_kind)?)
|
||||
}
|
||||
|
||||
pub value: f64,
|
||||
/// The signed difference between the target value and the value of this circuit.
|
||||
///
|
||||
/// Positive value indicates that the value of this circuit is greater than the target value.
|
||||
/// Negative value indicates that the value of this circuit is less than the target value.
|
||||
///
|
||||
/// * `circuit` - The circuit for computation.
|
||||
/// * `value` - The value of the circuit computed by the [`value`](Self::value) method
|
||||
/// for reducing computation steps, or `None` if you request this method to compute the value.
|
||||
pub fn difference(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
value: Option<f64>,
|
||||
) -> Result<f64, CircuitCalculatorError> {
|
||||
let value = match value {
|
||||
Some(v) => sanitize_floating_point(v)
|
||||
.map_err(|err| CircuitCalculatorError::BadReuseValue(err))?,
|
||||
None => self.value(circuit)?,
|
||||
};
|
||||
|
||||
sanitize_floating_point(value - self.target_value)
|
||||
.map_err(|err| CircuitCalculatorError::BadArithmetic(err))
|
||||
}
|
||||
|
||||
pub difference: f64,
|
||||
/// The unsigned difference between the target value and the value of this circuit.
|
||||
///
|
||||
/// * `circuit` - The circuit for computation.
|
||||
/// * `value` - The value of the circuit computed by the [`value`](Self::value) method
|
||||
/// for reducing computation steps, or `None` if you request this method to compute the value.
|
||||
/// * `difference` - The difference of the circuit computed by the
|
||||
/// [`difference`](Self::difference) method for reducing computation steps,
|
||||
/// or `None` if you request this method to compute the difference.
|
||||
pub fn unsigned_difference(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
value: Option<f64>,
|
||||
difference: Option<f64>,
|
||||
) -> Result<f64, CircuitCalculatorError> {
|
||||
let diff = match difference {
|
||||
Some(d) => sanitize_floating_point(d)
|
||||
.map_err(|err| CircuitCalculatorError::BadReuseValue(err))?,
|
||||
None => self.difference(circuit, value)?,
|
||||
};
|
||||
|
||||
sanitize_floating_point(diff.abs())
|
||||
.map_err(|err| CircuitCalculatorError::BadArithmetic(err))
|
||||
}
|
||||
|
||||
pub unsigned_difference: f64,
|
||||
/// The signed relative difference between the target value and the value of this circuit.
|
||||
///
|
||||
/// Positive value indicates that the value of this circuit is greater than the target value.
|
||||
/// Negative value indicates that the value of this circuit is less than the target value.
|
||||
///
|
||||
/// * `circuit` - The circuit for computation.
|
||||
/// * `value` - The value of the circuit computed by the [`value`](Self::value) method
|
||||
/// for reducing computation steps, or `None` if you request this method to compute the value.
|
||||
/// * `difference` - The difference of the circuit computed by the
|
||||
/// [`difference`](Self::difference) method for reducing computation steps,
|
||||
/// or `None` if you request this method to compute the difference.
|
||||
pub fn relative_difference(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
value: Option<f64>,
|
||||
difference: Option<f64>,
|
||||
) -> Result<f64, CircuitCalculatorError> {
|
||||
let diff = match difference {
|
||||
Some(d) => sanitize_floating_point(d)
|
||||
.map_err(|err| CircuitCalculatorError::BadReuseValue(err))?,
|
||||
None => self.difference(circuit, value)?,
|
||||
};
|
||||
|
||||
sanitize_floating_point(diff / self.target_value)
|
||||
.map_err(|err| CircuitCalculatorError::BadArithmetic(err))
|
||||
pub relative_difference: f64,
|
||||
/// The unsigned relative difference between the target value and the value of this circuit.
|
||||
pub unsigned_relative_difference: f64,
|
||||
}
|
||||
|
||||
/// The unsigned relative difference between the target value and the value of this circuit.
|
||||
///
|
||||
/// * `circuit` - The circuit for computation.
|
||||
/// * `value` - The value of the circuit computed by the [`value`](Self::value) method
|
||||
/// for reducing computation steps, or `None` if you request this method to compute the value.
|
||||
/// * `difference` - The difference of the circuit computed by the
|
||||
/// [`difference`](Self::difference) method for reducing computation steps,
|
||||
/// or `None` if you request this method to compute the difference.
|
||||
/// * `relative_difference` - The relative difference of the circuit computed by the
|
||||
/// [`relative_difference`](Self::relative_difference) method for reducing computation steps,
|
||||
/// or `None` if you request this method to compute the relative difference.
|
||||
///
|
||||
pub fn unsigned_relative_difference(
|
||||
&self,
|
||||
circuit: &Circuit,
|
||||
value: Option<f64>,
|
||||
difference: Option<f64>,
|
||||
relative_difference: Option<f64>,
|
||||
) -> Result<f64, CircuitCalculatorError> {
|
||||
let rel_diff = match relative_difference {
|
||||
Some(rd) => sanitize_floating_point(rd)
|
||||
.map_err(|err| CircuitCalculatorError::BadReuseValue(err))?,
|
||||
None => self.relative_difference(circuit, value, difference)?,
|
||||
};
|
||||
impl CircuitEvaluation {
|
||||
/// Internal used constructor. Passed circuit `value` must be checked before calling this.
|
||||
fn new(value: f64, target_value: f64) -> Result<Self, CircuitError> {
|
||||
// Check target value
|
||||
let target_value =
|
||||
validate_device_value(target_value).map_err(|err| CircuitError::BadTargetValue(err))?;
|
||||
// Start evaluating
|
||||
let difference = validate_floating_point(value - target_value)
|
||||
.map_err(|err| CircuitError::BadArithmetic(err))?;
|
||||
let unsigned_difference = validate_floating_point(difference.abs())
|
||||
.map_err(|err| CircuitError::BadArithmetic(err))?;
|
||||
let relative_difference = validate_floating_point(difference / target_value)
|
||||
.map_err(|err| CircuitError::BadArithmetic(err))?;
|
||||
let unsigned_relative_difference = validate_floating_point(relative_difference.abs())
|
||||
.map_err(|err| CircuitError::BadArithmetic(err))?;
|
||||
// Return evaluation result
|
||||
Ok(CircuitEvaluation {
|
||||
value,
|
||||
difference,
|
||||
unsigned_difference,
|
||||
relative_difference,
|
||||
unsigned_relative_difference,
|
||||
})
|
||||
}
|
||||
|
||||
sanitize_floating_point(rel_diff.abs())
|
||||
.map_err(|err| CircuitCalculatorError::BadArithmetic(err))
|
||||
/// Evaluate circuit with device kind and target value.
|
||||
pub fn from_circuit(
|
||||
circuit: &Circuit,
|
||||
device_kind: DeviceKind,
|
||||
target_value: f64,
|
||||
) -> Result<Self, CircuitError> {
|
||||
// Fetch circuit value and evaluate it.
|
||||
let value = circuit.evaluate(device_kind)?;
|
||||
Self::new(value, target_value)
|
||||
}
|
||||
|
||||
/// Evaluate circuit with pre-evaluated circuit value and target value.
|
||||
pub fn from_circuit_value(value: f64, target_value: f64) -> Result<Self, CircuitError> {
|
||||
// Check user given circuit value and evaluate it.
|
||||
let value =
|
||||
validate_device_value(value).map_err(|err| CircuitError::BadCircuitValue(err))?;
|
||||
Self::new(value, target_value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,455 +0,0 @@
|
||||
use thiserror::Error as TeError;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
/// Error occurs when building dataset.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum DatasetError {
|
||||
|
||||
}
|
||||
|
||||
/// An item in the dataset.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DatasetItem {
|
||||
/// The actual value of this item.
|
||||
pub value: f64,
|
||||
/// The string form of this value given from original input for re-saving.
|
||||
pub str_value: String,
|
||||
}
|
||||
|
||||
impl DatasetItem {
|
||||
/// Create a new dataset item with validation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`DatasetError::InvalidDatasetValue`] if the value is not greater than 0.
|
||||
/// Returns [`DatasetError::EmptyDatasetItem`] if the string value is empty.
|
||||
pub fn new(value: f64, str_value: String) -> Result<Self, DatasetError> {
|
||||
if value <= 0.0 {
|
||||
return Err(DatasetError::InvalidDatasetValue(value));
|
||||
}
|
||||
if str_value.is_empty() {
|
||||
return Err(DatasetError::EmptyDatasetItem);
|
||||
}
|
||||
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 {
|
||||
/// Create a dataset from an iterable of stringified values.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`DatasetError::DuplicateDatasetItem`] if duplicate values are found.
|
||||
/// Returns [`DatasetError::EmptyDataset`] if the iterable produces no items.
|
||||
/// Returns [`DatasetError::InvalidHumanReadableValue`] if a value cannot be parsed.
|
||||
pub fn from_iterable<I, S>(str_values: I) -> Result<Self, DatasetError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
// Check string form value one by one
|
||||
let mut items: Vec<DatasetItem> = Vec::new();
|
||||
let mut seen: HashSet<f64> = HashSet::new();
|
||||
|
||||
for str_value_raw in str_values {
|
||||
let str_value = str_value_raw.into();
|
||||
// Try parsing value
|
||||
let value = from_human_readable_value(&str_value)?;
|
||||
// Check and update set
|
||||
if !seen.insert(value) {
|
||||
return Err(DatasetError::DuplicateDatasetItem(str_value));
|
||||
}
|
||||
// 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 })
|
||||
}
|
||||
|
||||
/// Load a dataset from a block of text.
|
||||
///
|
||||
/// Each non-empty line (after trimming whitespace) is treated as a value.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_iterable`].
|
||||
pub fn from_text(text: &str) -> Result<Self, DatasetError> {
|
||||
let lines: Vec<String> = text
|
||||
.lines()
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect();
|
||||
Self::from_iterable(lines)
|
||||
}
|
||||
|
||||
/// Load a dataset from a file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`DatasetError::Io`] if the file cannot be read.
|
||||
/// See [`Dataset::from_iterable`] for other errors.
|
||||
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, DatasetError> {
|
||||
let text = std::fs::read_to_string(path)?;
|
||||
Self::from_text(&text)
|
||||
}
|
||||
|
||||
/// The preset dataset for resistors.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_iterable`].
|
||||
pub fn resistor_preset() -> Result<Self, DatasetError> {
|
||||
Self::from_iterable([
|
||||
"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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_iterable`].
|
||||
pub fn capacitor_preset() -> Result<Self, DatasetError> {
|
||||
Self::from_iterable([
|
||||
"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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_iterable`].
|
||||
pub fn inductor_preset() -> Result<Self, DatasetError> {
|
||||
Self::from_iterable([
|
||||
"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",
|
||||
])
|
||||
}
|
||||
|
||||
/// Get the string form of all values joined by newlines.
|
||||
pub fn save_text(&self) -> String {
|
||||
self.items
|
||||
.iter()
|
||||
.map(|i| i.str_value.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Save all values to a file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`DatasetError::Io`] if the file cannot be written.
|
||||
pub fn save_file(&self, path: impl AsRef<Path>) -> Result<(), DatasetError> {
|
||||
std::fs::write(path, self.save_text())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the available standard values as an iterator of `f64`.
|
||||
pub fn values(&self) -> impl Iterator<Item = f64> + '_ {
|
||||
self.items.iter().map(|i| i.value)
|
||||
}
|
||||
|
||||
/// Get the underlying dataset items as a slice.
|
||||
pub fn items(&self) -> &[DatasetItem] {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_iterable`].
|
||||
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_iterable(resistor)?,
|
||||
capacitor: Dataset::from_iterable(capacitor)?,
|
||||
inductor: Dataset::from_iterable(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the standard values from strings.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_text`].
|
||||
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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_file`].
|
||||
pub fn from_file(
|
||||
resistor: impl AsRef<Path>,
|
||||
capacitor: impl AsRef<Path>,
|
||||
inductor: impl AsRef<Path>,
|
||||
) -> Result<Self, DatasetError> {
|
||||
Ok(Self {
|
||||
resistor: Dataset::from_file(resistor)?,
|
||||
capacitor: Dataset::from_file(capacitor)?,
|
||||
inductor: Dataset::from_file(inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The preset dataset collection for all devices.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Dataset::from_iterable`].
|
||||
pub fn devices_preset() -> Result<Self, DatasetError> {
|
||||
Ok(Self {
|
||||
resistor: Dataset::resistor_preset()?,
|
||||
capacitor: Dataset::capacitor_preset()?,
|
||||
inductor: Dataset::inductor_preset()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the string form of all values.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple of strings for resistor, capacitor and inductor 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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `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.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`DatasetError::Io`] if any file cannot be written.
|
||||
pub fn save_file(
|
||||
&self,
|
||||
resistor: impl AsRef<Path>,
|
||||
capacitor: impl AsRef<Path>,
|
||||
inductor: impl AsRef<Path>,
|
||||
) -> Result<(), DatasetError> {
|
||||
self.resistor.save_file(resistor)?;
|
||||
self.capacitor.save_file(capacitor)?;
|
||||
self.inductor.save_file(inductor)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the dataset for resistor.
|
||||
pub fn resistor(&self) -> &Dataset {
|
||||
&self.resistor
|
||||
}
|
||||
|
||||
/// Get the dataset for capacitor.
|
||||
pub fn capacitor(&self) -> &Dataset {
|
||||
&self.capacitor
|
||||
}
|
||||
|
||||
/// Get the dataset for inductor.
|
||||
pub fn inductor(&self) -> &Dataset {
|
||||
&self.inductor
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ParseHumanReadableValueError {
|
||||
|
||||
}
|
||||
|
||||
/// Convert human readable value to float.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `strl` - The human readable value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The parsed float value.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`DatasetError::InvalidHumanReadableValue`] if the input string is not a valid number.
|
||||
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)
|
||||
};
|
||||
|
||||
num_part
|
||||
.parse::<f64>()
|
||||
.map(|v| v * multiplier)
|
||||
.map_err(|_| DatasetError::InvalidHumanReadableValue(strl.to_string()))
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `v` - The value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The unit 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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `v` - The float value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The 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!("{:+.4f} p", v / 1e-9),
|
||||
UnitScale::Micro => format!("{:+.4f} u", v / 1e-6),
|
||||
UnitScale::Milli => format!("{:+.4f} m", v / 1e-3),
|
||||
UnitScale::None => {
|
||||
// YYC MARK:
|
||||
// The space of this format string is by design
|
||||
// for keeping the same style with other format strings.
|
||||
format!("{:+.4} ", v)
|
||||
}
|
||||
UnitScale::Kilo => format!("{:+.4f} k", v / 1e3),
|
||||
UnitScale::Mega => format!("{:+.4f} M", v / 1e6),
|
||||
UnitScale::Giga => format!("{:+.4f} G", v / 1e9),
|
||||
UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9),
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
pub mod common;
|
||||
pub mod dataset;
|
||||
pub mod spec;
|
||||
pub mod query;
|
||||
pub mod resolver;
|
||||
|
||||
pub use common::{
|
||||
Circuit, CircuitDeviceScale, CircuitCalculator, DeviceKind, JointKind, LcrConnError, SubCircuit,
|
||||
};
|
||||
pub use dataset::{
|
||||
from_human_readable_value, get_human_readable_value_scale, to_human_readable_value, Dataset,
|
||||
DatasetCollection, DatasetItem, UnitScale,
|
||||
};
|
||||
pub use query::{Request, Response, ResponseItem, ResponsePriority, MAX_RESPONSE_CNT};
|
||||
pub use resolver::{BfsResolver, LutResolver, Resolver};
|
||||
pub use common::DeviceKind;
|
||||
pub use query::{Request, Response, ResponsePriority};
|
||||
pub use resolver::{Resolver, BfsResolver, LutResolver};
|
||||
|
||||
+98
-92
@@ -1,10 +1,11 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Index;
|
||||
|
||||
use crate::common::{Circuit, CircuitCalculator, DeviceKind, LcrConnError};
|
||||
use crate::common::{
|
||||
Circuit, CircuitError, CircuitEvaluation, DeviceKind, DeviceValueError, validate_device_value,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// The priority of the result.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ResponsePriority {
|
||||
/// Less devices is the first priority.
|
||||
LessDevices,
|
||||
@@ -15,46 +16,50 @@ pub enum ResponsePriority {
|
||||
/// The maximum count for the response item count passed in request.
|
||||
pub const MAX_RESPONSE_CNT: usize = 50;
|
||||
|
||||
/// The error occurs when building [Request].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum RequestError {
|
||||
#[error("invalid target value in request: {0}")]
|
||||
BadTargetValue(DeviceValueError),
|
||||
#[error("invalid tolerance in request: {0}")]
|
||||
BadTolerance(DeviceValueError),
|
||||
#[error("invalid response count {0} limit in request")]
|
||||
BadCountLimit(usize),
|
||||
}
|
||||
|
||||
/// All request information for the resolver.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Request {
|
||||
/// The kind of device to resolve.
|
||||
pub device_kind: DeviceKind,
|
||||
device_kind: DeviceKind,
|
||||
/// The target value of the device.
|
||||
pub target_value: f64,
|
||||
target_value: f64,
|
||||
/// The tolerance of the device in absolute value.
|
||||
pub tolerance: f64,
|
||||
tolerance: f64,
|
||||
/// The priority principle when sorting response items.
|
||||
pub response_priority: ResponsePriority,
|
||||
response_priority: ResponsePriority,
|
||||
/// The limited count of results.
|
||||
pub count_limit: usize,
|
||||
count_limit: usize,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Create a new request with validation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`LcrConnError::InvalidTargetValue`] if the target value is not greater than 0.
|
||||
/// Returns [`LcrConnError::InvalidTolerance`] if the tolerance is negative.
|
||||
/// Returns [`LcrConnError::InvalidCountLimit`] if the count limit is 0 or exceeds
|
||||
/// [`MAX_RESPONSE_CNT`].
|
||||
pub fn new(
|
||||
device_kind: DeviceKind,
|
||||
target_value: f64,
|
||||
tolerance: f64,
|
||||
response_priority: ResponsePriority,
|
||||
count_limit: usize,
|
||||
) -> Result<Self, LcrConnError> {
|
||||
if target_value <= 0.0 {
|
||||
return Err(LcrConnError::InvalidTargetValue(target_value));
|
||||
}
|
||||
if tolerance < 0.0 {
|
||||
return Err(LcrConnError::InvalidTolerance(tolerance));
|
||||
}
|
||||
) -> Result<Self, RequestError> {
|
||||
// Check arguments
|
||||
let target_value =
|
||||
validate_device_value(target_value).map_err(|err| RequestError::BadTargetValue(err))?;
|
||||
let tolerance =
|
||||
validate_device_value(tolerance).map_err(|err| RequestError::BadTolerance(err))?;
|
||||
if count_limit == 0 || count_limit > MAX_RESPONSE_CNT {
|
||||
return Err(LcrConnError::InvalidCountLimit(count_limit));
|
||||
return Err(RequestError::BadCountLimit(count_limit));
|
||||
}
|
||||
// Everything is okey.
|
||||
Ok(Self {
|
||||
device_kind,
|
||||
target_value,
|
||||
@@ -63,6 +68,44 @@ impl Request {
|
||||
count_limit,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the kind of device of this request.
|
||||
pub fn get_device_kind(&self) -> DeviceKind {
|
||||
self.device_kind
|
||||
}
|
||||
|
||||
/// Get the target value of this request.
|
||||
///
|
||||
/// The return value was ensured that it must be valid device value.
|
||||
pub fn get_target_value(&self) -> f64 {
|
||||
self.target_value
|
||||
}
|
||||
|
||||
/// Get the tolerance of this request.
|
||||
///
|
||||
/// The return value was ensured that it must be unsigned non-relative valid device value.
|
||||
pub fn get_tolerance(&self) -> f64 {
|
||||
self.tolerance
|
||||
}
|
||||
|
||||
/// Get the priority principle when sorting response items.
|
||||
pub fn get_response_priority(&self) -> ResponsePriority {
|
||||
self.response_priority
|
||||
}
|
||||
|
||||
/// Get the limited count of results.
|
||||
///
|
||||
/// The return value was ensured that it must >= 0 and < [`MAX_RESPONSE_CNT`].
|
||||
pub fn get_count_limit(&self) -> usize {
|
||||
self.count_limit
|
||||
}
|
||||
}
|
||||
|
||||
/// Error occurs when building [Response] and [ResponseItem].
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ResponseError {
|
||||
#[error("failed on evaluating circuit: {0}")]
|
||||
EvaluateCircuit(#[from] CircuitError),
|
||||
}
|
||||
|
||||
/// The possible solution given by the resolver.
|
||||
@@ -70,49 +113,26 @@ impl Request {
|
||||
pub struct ResponseItem {
|
||||
/// The circuit of this response item.
|
||||
circuit: Circuit,
|
||||
/// The device count of this circuit.
|
||||
device_count: usize,
|
||||
/// The value of this circuit.
|
||||
value: f64,
|
||||
/// The signed difference between the target value and the value of this circuit.
|
||||
///
|
||||
/// Positive value indicates that the value of this circuit is greater than the target value.
|
||||
/// Negative value indicates that the value of this circuit is less than the target value.
|
||||
difference: f64,
|
||||
/// The unsigned difference between the target value and the value of this circuit.
|
||||
unsigned_difference: f64,
|
||||
/// The signed relative difference between the target value and the value of this circuit.
|
||||
///
|
||||
/// Positive value indicates that the value of this circuit is greater than the target value.
|
||||
/// Negative value indicates that the value of this circuit is less than the target value.
|
||||
relative_difference: f64,
|
||||
/// The unsigned relative difference between the target value and the value of this circuit.
|
||||
unsigned_relative_difference: f64,
|
||||
/// The evaluation result of this circuit.
|
||||
circuit_evaluation: CircuitEvaluation,
|
||||
}
|
||||
|
||||
impl ResponseItem {
|
||||
/// Create a new response item by computing all values eagerly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`CircuitValueTrait::value`].
|
||||
pub fn new(circuit: Circuit, cv_trait: &CircuitCalculator) -> Result<Self, LcrConnError> {
|
||||
let value = cv_trait.value(&circuit)?;
|
||||
let difference = cv_trait.difference(&circuit, Some(value))?;
|
||||
let unsigned_difference = cv_trait.unsigned_difference(&circuit, None, Some(difference))?;
|
||||
let relative_difference = cv_trait.relative_difference(&circuit, None, Some(difference))?;
|
||||
let unsigned_relative_difference =
|
||||
cv_trait.unsigned_relative_difference(&circuit, None, None, Some(relative_difference))?;
|
||||
let device_count = circuit.device_scale().to_device_count();
|
||||
|
||||
fn new(circuit: Circuit, request: &Request) -> Result<Self, ResponseError> {
|
||||
// YYC MARK:
|
||||
// I can use OnceLock to implement the behavior closing to Python cached_property.
|
||||
// But I didn't do that due to the increased size of this struct, and inviable error handling.
|
||||
// So I decide to calculate all values in there.
|
||||
let circuit_evaluation = CircuitEvaluation::from_circuit(
|
||||
&circuit,
|
||||
request.get_device_kind(),
|
||||
request.get_target_value(),
|
||||
)?;
|
||||
// Build self and return
|
||||
Ok(Self {
|
||||
circuit,
|
||||
device_count,
|
||||
value,
|
||||
difference,
|
||||
unsigned_difference,
|
||||
relative_difference,
|
||||
unsigned_relative_difference,
|
||||
circuit_evaluation,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,12 +143,12 @@ impl ResponseItem {
|
||||
|
||||
/// The device count of this circuit.
|
||||
pub fn device_count(&self) -> usize {
|
||||
self.device_count
|
||||
self.circuit.device_scale().to_device_count()
|
||||
}
|
||||
|
||||
/// The value of this circuit.
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value
|
||||
self.circuit_evaluation.value
|
||||
}
|
||||
|
||||
/// The signed difference between the target value and the value of this circuit.
|
||||
@@ -136,12 +156,12 @@ impl ResponseItem {
|
||||
/// Positive value indicates that the value of this circuit is greater than the target value.
|
||||
/// Negative value indicates that the value of this circuit is less than the target value.
|
||||
pub fn difference(&self) -> f64 {
|
||||
self.difference
|
||||
self.circuit_evaluation.difference
|
||||
}
|
||||
|
||||
/// The unsigned difference between the target value and the value of this circuit.
|
||||
pub fn unsigned_difference(&self) -> f64 {
|
||||
self.unsigned_difference
|
||||
self.circuit_evaluation.unsigned_difference
|
||||
}
|
||||
|
||||
/// The signed relative difference between the target value and the value of this circuit.
|
||||
@@ -149,12 +169,12 @@ impl ResponseItem {
|
||||
/// Positive value indicates that the value of this circuit is greater than the target value.
|
||||
/// Negative value indicates that the value of this circuit is less than the target value.
|
||||
pub fn relative_difference(&self) -> f64 {
|
||||
self.relative_difference
|
||||
self.circuit_evaluation.relative_difference
|
||||
}
|
||||
|
||||
/// The unsigned relative difference between the target value and the value of this circuit.
|
||||
pub fn unsigned_relative_difference(&self) -> f64 {
|
||||
self.unsigned_relative_difference
|
||||
self.circuit_evaluation.unsigned_relative_difference
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,35 +199,29 @@ impl Response {
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`ResponseItem::new`].
|
||||
pub fn new(
|
||||
request: &Request,
|
||||
candidates: impl IntoIterator<Item = Circuit>,
|
||||
) -> Result<Self, LcrConnError> {
|
||||
let cv_trait = CircuitCalculator::new(request.device_kind, request.target_value);
|
||||
|
||||
pub fn new<I>(request: &Request, candidates: I) -> Result<Self, ResponseError>
|
||||
where
|
||||
I: Iterator<Item = Circuit>,
|
||||
{
|
||||
let mut items: Vec<ResponseItem> = candidates
|
||||
.into_iter()
|
||||
.map(|c| ResponseItem::new(c, &cv_trait))
|
||||
.map(|c| ResponseItem::new(c, request))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
// Sort by different strategy
|
||||
match request.response_priority {
|
||||
ResponsePriority::LessDevices => {
|
||||
items.sort_by(|a, b| {
|
||||
a.device_count
|
||||
.cmp(&b.device_count)
|
||||
.then_with(|| {
|
||||
a.unsigned_difference
|
||||
.partial_cmp(&b.unsigned_difference)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
a.device_count().cmp(&b.device_count()).then_with(|| {
|
||||
OrderedFloat(a.unsigned_difference())
|
||||
.cmp(&OrderedFloat(b.unsigned_difference()))
|
||||
})
|
||||
});
|
||||
}
|
||||
ResponsePriority::MoreAccuracy => {
|
||||
items.sort_by(|a, b| {
|
||||
a.unsigned_difference
|
||||
.partial_cmp(&b.unsigned_difference)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
OrderedFloat(a.unsigned_difference())
|
||||
.cmp(&OrderedFloat(b.unsigned_difference()))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -246,11 +260,3 @@ impl Response {
|
||||
self.sorted_items.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for Response {
|
||||
type Output = ResponseItem;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.sorted_items[index]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
pub mod bfs;
|
||||
pub mod lut;
|
||||
|
||||
use crate::query::{Request, Response};
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
/// Aggregated error occurs in every resolvers.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum ResolverError {
|
||||
#[error("{0}")]
|
||||
BfsResolver(#[from] bfs::BfsResolverError),
|
||||
#[error("{0}")]
|
||||
LutResolver(#[from] lut::LutResolverError),
|
||||
}
|
||||
|
||||
/// Abstract base trait for all resolvers.
|
||||
pub trait Resolver {
|
||||
/// Resolve the request and return the response.
|
||||
///
|
||||
/// `request` is the request to resolve.
|
||||
/// The response containing the best matching circuits.
|
||||
fn resolve(&self, request: &Request) -> Result<Response, ResolverError>;
|
||||
}
|
||||
|
||||
pub use bfs::BfsResolver;
|
||||
pub use lut::LutResolver;
|
||||
+240
-351
@@ -1,257 +1,32 @@
|
||||
use super::{Resolver, ResolverError};
|
||||
use crate::common::{Circuit, CircuitError, CircuitEvaluation, DeviceKind, JointKind};
|
||||
use crate::query::{Request, Response, ResponseError};
|
||||
use crate::spec::{SpecCatalog, SpecGroup};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::iter::FusedIterator;
|
||||
use strum::IntoEnumIterator;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
use super::Resolver;
|
||||
use crate::common::{Circuit, CircuitCalculator, DeviceKind, JointKind, LcrConnError};
|
||||
use crate::dataset::{Dataset, DatasetCollection, DatasetItem};
|
||||
use crate::query::{Request, Response};
|
||||
// region: BFS Resolver Kernel
|
||||
|
||||
// ============================================================================
|
||||
// Lazy iterator structs for circuit generation
|
||||
// ============================================================================
|
||||
|
||||
// YYC MARK:
|
||||
// Some circuit are equivalent in topology.
|
||||
// If we deduplicate these equaivalent circuit in building result,
|
||||
// there are too complex works.
|
||||
// So we should deduplicated these equivalent circuit at the beginning,
|
||||
// i.e. when generating them.
|
||||
// So following iterator structs are taking this job.
|
||||
|
||||
/// Iterator over all possible one-device circuits without repeating equivalent topology.
|
||||
pub struct OneDeviceCircuitIter<'a> {
|
||||
items: &'a [DatasetItem],
|
||||
pos: usize,
|
||||
/// Error occurs BFS resolver.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum BfsResolverError {
|
||||
#[error("failed on evaluating circuit: {0}")]
|
||||
EvaluateCircuit(#[from] CircuitError),
|
||||
#[error("fail to build response: {0}")]
|
||||
Response(#[from] ResponseError),
|
||||
}
|
||||
|
||||
impl<'a> OneDeviceCircuitIter<'a> {
|
||||
pub fn new(items: &'a [DatasetItem]) -> Self {
|
||||
Self { items, pos: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for OneDeviceCircuitIter<'_> {
|
||||
type Item = Circuit;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.pos < self.items.len() {
|
||||
// Every single device is unique so we directly output them.
|
||||
// This feature is insured by dataset itself.
|
||||
let circuit = Circuit::from_one_device(self.items[self.pos].value);
|
||||
self.pos += 1;
|
||||
Some(circuit)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for OneDeviceCircuitIter<'_> {}
|
||||
|
||||
/// Iterator over all possible two-device circuits without repeating equivalent topology.
|
||||
pub struct TwoDeviceCircuitIter<'a> {
|
||||
items: &'a [DatasetItem],
|
||||
i: usize,
|
||||
j: usize,
|
||||
joint_idx: usize,
|
||||
}
|
||||
|
||||
impl<'a> TwoDeviceCircuitIter<'a> {
|
||||
pub fn new(items: &'a [DatasetItem]) -> Self {
|
||||
Self {
|
||||
items,
|
||||
i: 0,
|
||||
j: 0,
|
||||
joint_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TwoDeviceCircuitIter<'_> {
|
||||
type Item = Circuit;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let n = self.items.len();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.joint_idx < JointKind::ALL.len() {
|
||||
let jk = JointKind::ALL[self.joint_idx];
|
||||
self.joint_idx += 1;
|
||||
// The two devices in this circuit is always swapable,
|
||||
// so we iterate them without repeating.
|
||||
return Some(Circuit::from_two_devices(
|
||||
self.items[self.i].value,
|
||||
self.items[self.j].value,
|
||||
jk,
|
||||
));
|
||||
}
|
||||
|
||||
// Advance to next combination
|
||||
self.joint_idx = 0;
|
||||
self.j += 1;
|
||||
if self.j >= n {
|
||||
self.i += 1;
|
||||
self.j = self.i;
|
||||
if self.i >= n {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for TwoDeviceCircuitIter<'_> {}
|
||||
|
||||
/// Iterator over three-device circuits where both joints share the same type.
|
||||
///
|
||||
/// In this case, all 3 devices are swapable and are iterated without repeating.
|
||||
pub struct ThreeDeviceSameJointIter<'a> {
|
||||
items: &'a [DatasetItem],
|
||||
i: usize,
|
||||
j: usize,
|
||||
k: usize,
|
||||
joint_idx: usize,
|
||||
}
|
||||
|
||||
impl<'a> ThreeDeviceSameJointIter<'a> {
|
||||
pub fn new(items: &'a [DatasetItem]) -> Self {
|
||||
Self {
|
||||
items,
|
||||
i: 0,
|
||||
j: 0,
|
||||
k: 0,
|
||||
joint_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ThreeDeviceSameJointIter<'_> {
|
||||
type Item = Circuit;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let n = self.items.len();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.joint_idx < JointKind::ALL.len() {
|
||||
let jk = JointKind::ALL[self.joint_idx];
|
||||
self.joint_idx += 1;
|
||||
return Some(Circuit::from_three_devices(
|
||||
self.items[self.i].value,
|
||||
self.items[self.j].value,
|
||||
jk,
|
||||
self.items[self.k].value,
|
||||
jk,
|
||||
));
|
||||
}
|
||||
|
||||
self.joint_idx = 0;
|
||||
self.k += 1;
|
||||
if self.k >= n {
|
||||
self.j += 1;
|
||||
self.k = self.j;
|
||||
if self.j >= n {
|
||||
self.i += 1;
|
||||
self.j = self.i;
|
||||
self.k = self.i;
|
||||
if self.i >= n {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for ThreeDeviceSameJointIter<'_> {}
|
||||
|
||||
/// Iterator over three-device circuits where the two joint types differ.
|
||||
///
|
||||
/// In this case, the first 2 devices are swapable and are iterated without repeating,
|
||||
/// while the third device iterates over all values independently.
|
||||
pub struct ThreeDeviceDiffJointIter<'a> {
|
||||
items: &'a [DatasetItem],
|
||||
i: usize,
|
||||
j: usize,
|
||||
k: usize,
|
||||
joint_idx: usize,
|
||||
}
|
||||
|
||||
impl<'a> ThreeDeviceDiffJointIter<'a> {
|
||||
pub fn new(items: &'a [DatasetItem]) -> Self {
|
||||
Self {
|
||||
items,
|
||||
i: 0,
|
||||
j: 0,
|
||||
k: 0,
|
||||
joint_idx: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ThreeDeviceDiffJointIter<'_> {
|
||||
type Item = Circuit;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let n = self.items.len();
|
||||
if n == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
loop {
|
||||
if self.joint_idx < JointKind::ALL.len() {
|
||||
let j = JointKind::ALL[self.joint_idx];
|
||||
self.joint_idx += 1;
|
||||
return Some(Circuit::from_three_devices(
|
||||
self.items[self.i].value,
|
||||
self.items[self.j].value,
|
||||
j,
|
||||
self.items[self.k].value,
|
||||
j.flip(),
|
||||
));
|
||||
}
|
||||
|
||||
self.joint_idx = 0;
|
||||
self.k += 1;
|
||||
if self.k >= n {
|
||||
self.j += 1;
|
||||
self.k = 0;
|
||||
if self.j >= n {
|
||||
self.i += 1;
|
||||
self.j = self.i;
|
||||
self.k = 0;
|
||||
if self.i >= n {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for ThreeDeviceDiffJointIter<'_> {}
|
||||
|
||||
/// Type alias for the chained three-device circuit iterator.
|
||||
pub type ThreeDeviceCircuitIter<'a> = std::iter::Chain<
|
||||
ThreeDeviceSameJointIter<'a>,
|
||||
ThreeDeviceDiffJointIter<'a>,
|
||||
>;
|
||||
|
||||
// ============================================================================
|
||||
// BfsItem
|
||||
// ============================================================================
|
||||
// region: BFS Item
|
||||
|
||||
/// The entry used in BFS iteration storing circuit and value.
|
||||
pub struct BfsItem {
|
||||
/// The circuit represented by this item.
|
||||
circuit: Circuit,
|
||||
/// The computed value of the circuit.
|
||||
/// The evaluated value of the circuit.
|
||||
value: f64,
|
||||
/// The unsigned difference between the target value and the value of this circuit.
|
||||
unsigned_difference: f64,
|
||||
@@ -259,17 +34,19 @@ pub struct BfsItem {
|
||||
|
||||
impl BfsItem {
|
||||
/// Create a new BFS item by computing values eagerly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`CircuitValueTrait::value`].
|
||||
pub fn new(circuit: Circuit, cv_trait: &CircuitCalculator) -> Result<Self, LcrConnError> {
|
||||
let value = cv_trait.value(&circuit)?;
|
||||
let unsigned_difference = cv_trait.unsigned_difference(&circuit, Some(value))?;
|
||||
pub fn new(circuit: Circuit, request: &Request) -> Result<Self, BfsResolverError> {
|
||||
// YYC MARK:
|
||||
// The same reason for replacing cached_property like I done in `ResponseItem`.
|
||||
let eval = CircuitEvaluation::from_circuit(
|
||||
&circuit,
|
||||
request.get_device_kind(),
|
||||
request.get_target_value(),
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
circuit,
|
||||
value,
|
||||
unsigned_difference,
|
||||
value: eval.value,
|
||||
unsigned_difference: eval.unsigned_difference,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,7 +55,7 @@ impl BfsItem {
|
||||
&self.circuit
|
||||
}
|
||||
|
||||
/// The computed value of the circuit.
|
||||
/// The evaluated value of the circuit.
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value
|
||||
}
|
||||
@@ -294,30 +71,190 @@ impl BfsItem {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ResultBucket
|
||||
// ============================================================================
|
||||
// endregion
|
||||
|
||||
// region: BFS Resolver
|
||||
|
||||
/// A resolver that uses breadth first search to find the best matching circuits.
|
||||
pub struct BfsResolver {
|
||||
/// The specs for all device kinds.
|
||||
specs: SpecCatalog,
|
||||
}
|
||||
|
||||
impl BfsResolver {
|
||||
// YYC MARK:
|
||||
// Some circuit are equivalent in topology.
|
||||
// If we deduplicate these equaivalent circuit in building result, there are too complex works.
|
||||
// So we should deduplicated these equivalent circuit at the beginning, i.e. when generating them.
|
||||
// So following iterator functions are taking this job.
|
||||
//
|
||||
// Additionally, these device values are coming from `spec`.
|
||||
// All values are verified so the building step must success.
|
||||
// So we can safely unwrap them.
|
||||
|
||||
/// Iterate all possible circuits with one device without repeating equivalent topology.
|
||||
pub fn iter_one_device_circuit(specs: &SpecGroup) -> impl Iterator<Item = Circuit> {
|
||||
// Every single device is unique so we directly output them.
|
||||
// This feature is insured by spec itself.
|
||||
specs
|
||||
.iter()
|
||||
.map(|v1| Circuit::from_one_device(v1).expect("unexpected failure on building circuit"))
|
||||
}
|
||||
|
||||
/// Iterate all possible circuits with two devices without repeating equivalent topology.
|
||||
pub fn iter_two_devices_circuit(specs: &SpecGroup) -> impl Iterator<Item = Circuit> {
|
||||
// The two devices in this circuit is always swapable,
|
||||
// so we iterate them without repeating.
|
||||
itertools::iproduct!(
|
||||
specs.iter().array_combinations_with_replacement::<2>(),
|
||||
JointKind::iter()
|
||||
)
|
||||
.map(|([v1, v2], j2)| {
|
||||
Circuit::from_two_devices(v1, v2, j2).expect("unexpected failure on building circuit")
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate all possible circuits with three devices without repeating equivalent topology.
|
||||
pub fn iter_three_devices_circuit(specs: &SpecGroup) -> impl Iterator<Item = Circuit> {
|
||||
// For generating three devices circuit,
|
||||
// it should be consisted by 2 parts.
|
||||
itertools::chain!(
|
||||
// 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!(
|
||||
specs.iter().array_combinations_with_replacement::<3>(),
|
||||
JointKind::iter()
|
||||
)
|
||||
.map(
|
||||
|([v1, v2, v3], j)| Circuit::from_three_devices(v1, v2, j, v3, j)
|
||||
.expect("unexpected failure on building circuit")
|
||||
),
|
||||
// Second, if the joint type is different, then the first 2 devices are swapable.
|
||||
// So we need iterate them without repeating.
|
||||
itertools::iproduct!(
|
||||
specs.iter().array_combinations_with_replacement::<2>(),
|
||||
specs.iter(),
|
||||
JointKind::iter()
|
||||
)
|
||||
.map(|([v1, v2], v3, j)| Circuit::from_three_devices(
|
||||
v1,
|
||||
v2,
|
||||
j,
|
||||
v3,
|
||||
j.flip()
|
||||
)
|
||||
.expect("unexpected failure on building circuit")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl BfsResolver {
|
||||
/// Create a new BFS resolver with the given specs.
|
||||
pub fn new(specs: SpecCatalog) -> Self {
|
||||
Self { specs }
|
||||
}
|
||||
|
||||
fn pick_specs(&self, device_kind: DeviceKind) -> &SpecGroup {
|
||||
match device_kind {
|
||||
DeviceKind::Resistor => self.specs.resistor_specs(),
|
||||
DeviceKind::Capacitor => self.specs.capacitor_specs(),
|
||||
DeviceKind::Inductor => self.specs.inductor_specs(),
|
||||
}
|
||||
}
|
||||
|
||||
fn bfs_iteration(
|
||||
specs: &SpecGroup,
|
||||
request: &Request,
|
||||
) -> impl Iterator<Item = Result<BfsItem, BfsResolverError>> {
|
||||
itertools::chain!(
|
||||
BfsResolver::iter_one_device_circuit(&specs),
|
||||
BfsResolver::iter_two_devices_circuit(&specs),
|
||||
BfsResolver::iter_three_devices_circuit(&specs)
|
||||
)
|
||||
.map(|circuit| BfsItem::new(circuit, request))
|
||||
}
|
||||
|
||||
fn intern_resolve(&self, request: &Request) -> Result<Response, BfsResolverError> {
|
||||
// Pick specs group from catalog
|
||||
let specs = self.pick_specs(request.get_device_kind());
|
||||
|
||||
// Create the result bucket.
|
||||
// The count limit held by request is must be greater than zero, so we can simply unwrap it.
|
||||
let mut bucket =
|
||||
ResultBucket::new(request.get_count_limit()).expect("unexpected blank result bucket");
|
||||
|
||||
// Iterate circuit item one by one
|
||||
for item in BfsResolver::bfs_iteration(specs, request) {
|
||||
let item = item?;
|
||||
// If circuit absolute difference is out of tolerance, skip it directly.
|
||||
if item.unsigned_difference() <= request.get_tolerance() {
|
||||
// Put it into bucket
|
||||
let score = item.unsigned_difference();
|
||||
bucket.insert(item, score);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Return result
|
||||
let circuits = bucket.into_iter().map(|i| i.into_circuit());
|
||||
Ok(Response::new(request, circuits)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for BfsResolver {
|
||||
fn resolve(&self, request: &Request) -> Result<Response, ResolverError> {
|
||||
Ok(self.intern_resolve(request)?)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Result Bucket Helper
|
||||
|
||||
/// The error occurs in [`ResultBucket`] and [`ResultBucketItem`].
|
||||
#[derive(Debug, TeError)]
|
||||
enum ResultBucketError {
|
||||
#[error("the size of binary heap {0} is invalid")]
|
||||
BadBinHeapSize(usize),
|
||||
}
|
||||
|
||||
// region: Result Bucket Item
|
||||
|
||||
/// An item stored in a [`ResultBucket`].
|
||||
struct ResultBucketItem {
|
||||
/// The score associated with this item.
|
||||
score: f64,
|
||||
/// The underlying BfsItem.
|
||||
score: OrderedFloat<f64>,
|
||||
/// The underlying [BfsItem].
|
||||
item: BfsItem,
|
||||
/// Monotonic counter used as a tiebreaker when scores are equal,
|
||||
/// ensuring that BinaryHeap never compares BfsItem directly.
|
||||
/// ensuring that BinaryHeap never compares [BfsItem] directly.
|
||||
seq: usize,
|
||||
}
|
||||
|
||||
impl ResultBucketItem {
|
||||
fn new(score: f64, item: BfsItem, seq: usize) -> Self {
|
||||
Self { score, item, seq }
|
||||
pub fn new(score: f64, item: BfsItem, seq: usize) -> Self {
|
||||
Self {
|
||||
score: OrderedFloat(score),
|
||||
item,
|
||||
seq,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_score(&self) -> f64 {
|
||||
self.score.0
|
||||
}
|
||||
|
||||
pub fn into_bfs_item(self) -> BfsItem {
|
||||
self.item
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ResultBucketItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.score == other.score && self.seq == other.seq
|
||||
self.score.eq(&other.score) && self.seq.eq(&other.seq)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,18 +270,21 @@ impl Ord for ResultBucketItem {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// BinaryHeap is a max-heap: the greatest element is at the top.
|
||||
// We want the entry with the largest score at the top.
|
||||
match self.score.partial_cmp(&other.score) {
|
||||
Some(Ordering::Equal) | None => self.seq.cmp(&other.seq),
|
||||
Some(ord) => ord,
|
||||
}
|
||||
self.score
|
||||
.cmp(&other.score)
|
||||
.then_with(|| self.seq.cmp(&other.seq))
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Result Bucket
|
||||
|
||||
/// A bounded bucket that keeps up to N entries with the smallest scores.
|
||||
///
|
||||
/// When the bucket is full, inserting a new item only succeeds if its score
|
||||
/// is less than the current maximum; the maximum is then evicted.
|
||||
pub struct ResultBucket {
|
||||
struct ResultBucket {
|
||||
/// Maximum number of items the bucket can hold.
|
||||
n: usize,
|
||||
/// Max-heap of [`ResultBucketItem`].
|
||||
@@ -357,24 +297,39 @@ pub struct ResultBucket {
|
||||
|
||||
impl ResultBucket {
|
||||
/// Create a new bucket that holds at most `n` items.
|
||||
pub fn new(n: usize) -> Self {
|
||||
Self {
|
||||
pub fn new(n: usize) -> Result<Self, ResultBucketError> {
|
||||
// Check heap size
|
||||
if n == 0 {
|
||||
Err(ResultBucketError::BadBinHeapSize(n))
|
||||
} else {
|
||||
Ok(Self {
|
||||
n,
|
||||
heap: BinaryHeap::new(),
|
||||
counter: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// YYC MARK:
|
||||
// I want to preserve these 2 functions so I add `allow(dead_code)` to them.
|
||||
|
||||
/// The number of items currently in the bucket.
|
||||
#[allow(dead_code)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.heap.len()
|
||||
}
|
||||
|
||||
/// Whether the bucket is empty.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.heap.is_empty()
|
||||
}
|
||||
|
||||
/// Consume the bucket and return all stored items.
|
||||
pub fn into_iter(self) -> impl Iterator<Item = BfsItem> {
|
||||
self.heap.into_iter().map(|entry| entry.into_bfs_item())
|
||||
}
|
||||
|
||||
/// Insert a [`BfsItem`] with the given score.
|
||||
///
|
||||
/// If the bucket is not yet full the item is always inserted.
|
||||
@@ -382,100 +337,34 @@ impl ResultBucket {
|
||||
/// than the largest score currently in the bucket; the entry
|
||||
/// with the largest score is then evicted.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` if the item was inserted, `false` otherwise.
|
||||
/// Returns `true` if the item was inserted, `false` otherwise.
|
||||
pub fn insert(&mut self, item: BfsItem, score: f64) -> bool {
|
||||
// YYC MARK:
|
||||
// Because this struct stored `n` is must greater than zero,
|
||||
// so after the first `if` branch, the length of this binary heap must be greater than zero.
|
||||
// So there must be at least one item in binary heap.
|
||||
// and we can safely use `expect()` to peek from binary heap.
|
||||
let entry = ResultBucketItem::new(score, item, self.counter);
|
||||
if self.heap.len() < self.n {
|
||||
self.heap.push(entry);
|
||||
self.counter += 1;
|
||||
true
|
||||
} else if score >= self.heap.peek().unwrap().score {
|
||||
} else if score
|
||||
>= self
|
||||
.heap
|
||||
.peek()
|
||||
.expect("unexpected blank binary heap")
|
||||
.get_score()
|
||||
{
|
||||
false
|
||||
} else {
|
||||
*self.heap.peek_mut().unwrap() = entry;
|
||||
*self.heap.peek_mut().expect("unexpected blank binary heap") = entry;
|
||||
self.counter += 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the bucket and return all stored items.
|
||||
pub fn into_items(self) -> Vec<BfsItem> {
|
||||
self.heap.into_iter().map(|entry| entry.item).collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BfsResolver
|
||||
// ============================================================================
|
||||
// endregion
|
||||
|
||||
/// A resolver that uses brute-force search to find the best matching circuits.
|
||||
pub struct BfsResolver {
|
||||
/// The datasets for all device kinds.
|
||||
datasets: DatasetCollection,
|
||||
}
|
||||
|
||||
impl BfsResolver {
|
||||
/// Create a new BFS resolver with the given datasets.
|
||||
pub fn new(datasets: DatasetCollection) -> Self {
|
||||
Self { datasets }
|
||||
}
|
||||
|
||||
/// Iterate all possible circuits with one device without repeating equivalent topology.
|
||||
pub fn iter_one_device_circuit(dataset: &Dataset) -> OneDeviceCircuitIter<'_> {
|
||||
OneDeviceCircuitIter::new(dataset.items())
|
||||
}
|
||||
|
||||
/// Iterate all possible circuits with two devices without repeating equivalent topology.
|
||||
pub fn iter_two_devices_circuit(dataset: &Dataset) -> TwoDeviceCircuitIter<'_> {
|
||||
TwoDeviceCircuitIter::new(dataset.items())
|
||||
}
|
||||
|
||||
/// Iterate all possible circuits with three devices without repeating equivalent topology.
|
||||
pub fn iter_three_devices_circuit(dataset: &Dataset) -> ThreeDeviceCircuitIter<'_> {
|
||||
ThreeDeviceSameJointIter::new(dataset.items())
|
||||
.chain(ThreeDeviceDiffJointIter::new(dataset.items()))
|
||||
}
|
||||
|
||||
fn pick_dataset(&self, device_kind: DeviceKind) -> &Dataset {
|
||||
match device_kind {
|
||||
DeviceKind::Resistor => self.datasets.resistor(),
|
||||
DeviceKind::Capacitor => self.datasets.capacitor(),
|
||||
DeviceKind::Inductor => self.datasets.inductor(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for BfsResolver {
|
||||
fn resolve(&self, request: &Request) -> Result<Response, LcrConnError> {
|
||||
// Pick dataset from collection
|
||||
let dataset = self.pick_dataset(request.device_kind);
|
||||
|
||||
// Iterate circuit item one by one
|
||||
let mut bucket = ResultBucket::new(request.count_limit);
|
||||
let cv_trait = CircuitCalculator::new(request.device_kind, request.target_value);
|
||||
|
||||
let circuits = Self::iter_one_device_circuit(dataset)
|
||||
.chain(Self::iter_two_devices_circuit(dataset))
|
||||
.chain(Self::iter_three_devices_circuit(dataset));
|
||||
|
||||
for circuit in circuits {
|
||||
let item = BfsItem::new(circuit, &cv_trait)?;
|
||||
// If circuit absolute difference is out of tolerance, skip it directly.
|
||||
if item.unsigned_difference() > request.tolerance {
|
||||
continue;
|
||||
}
|
||||
// Put it into bucket
|
||||
bucket.insert(item, item.unsigned_difference());
|
||||
}
|
||||
|
||||
// Return result
|
||||
let circuits: Vec<Circuit> = bucket
|
||||
.into_items()
|
||||
.into_iter()
|
||||
.map(BfsItem::into_circuit)
|
||||
.collect();
|
||||
Response::new(request, circuits)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::bfs::BfsResolver;
|
||||
use super::Resolver;
|
||||
use crate::common::{Circuit, CircuitCalculator, DeviceKind, LcrConnError};
|
||||
use crate::dataset::{Dataset, DatasetCollection};
|
||||
use crate::query::{Request, Response};
|
||||
use super::{Resolver, ResolverError};
|
||||
use crate::common::{Circuit, CircuitError, CircuitEvaluation, DeviceKind};
|
||||
use crate::spec::{SpecGroup, SpecCatalog};
|
||||
use crate::query::{Request, Response, ResponseError};
|
||||
use ordered_float::OrderedFloat;
|
||||
use thiserror::Error as TeError;
|
||||
|
||||
// region: LUT Resolver Kernel
|
||||
|
||||
/// Errors occurs in LUT resolver.
|
||||
#[derive(Debug, TeError)]
|
||||
pub enum LutResolverError {
|
||||
#[error("failed on evaluating circuit: {0}")]
|
||||
CircuitCalculator(#[from] CircuitError),
|
||||
#[error("fail to build response: {0}")]
|
||||
Response(#[from] ResponseError),
|
||||
}
|
||||
|
||||
// region: LUT Item
|
||||
|
||||
/// An item in the lookup table.
|
||||
pub struct LutItem {
|
||||
/// The circuit represented by this item.
|
||||
circuit: Circuit,
|
||||
/// The value of this circuit.
|
||||
value: f64,
|
||||
value: OrderedFloat<f64>,
|
||||
}
|
||||
|
||||
impl LutItem {
|
||||
/// Create a new LUT item by computing the circuit value.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Circuit::compute`].
|
||||
pub fn new(circuit: Circuit, device_kind: DeviceKind) -> Result<Self, LcrConnError> {
|
||||
let value = circuit.compute(device_kind)?;
|
||||
Ok(Self { circuit, value })
|
||||
pub fn new(circuit: Circuit, device_kind: DeviceKind) -> Result<Self, LutResolverError> {
|
||||
let value = circuit.evaluate(device_kind)?;
|
||||
Ok(Self {
|
||||
circuit,
|
||||
value: OrderedFloat(value),
|
||||
})
|
||||
}
|
||||
|
||||
/// The circuit represented by this item.
|
||||
@@ -32,10 +44,14 @@ impl LutItem {
|
||||
|
||||
/// The value of this circuit.
|
||||
pub fn value(&self) -> f64 {
|
||||
self.value
|
||||
self.value.0
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region: LUT Resolver
|
||||
|
||||
/// A resolver that uses a lookup table to find the best matching circuit.
|
||||
pub struct LutResolver {
|
||||
/// The lookup table for resistors.
|
||||
@@ -47,31 +63,29 @@ pub struct LutResolver {
|
||||
}
|
||||
|
||||
impl LutResolver {
|
||||
/// Create a new LUT resolver by building lookup tables from the given datasets.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`LutItem::new`].
|
||||
pub fn new(datasets: &DatasetCollection) -> Result<Self, LcrConnError> {
|
||||
/// Create a new LUT resolver by building lookup tables from the given specs.
|
||||
pub fn new(specs: &SpecCatalog) -> Result<Self, LutResolverError> {
|
||||
Ok(Self {
|
||||
resistor_lut: Self::build_lut(datasets.resistor(), DeviceKind::Resistor)?,
|
||||
capacitor_lut: Self::build_lut(datasets.capacitor(), DeviceKind::Capacitor)?,
|
||||
inductor_lut: Self::build_lut(datasets.inductor(), DeviceKind::Inductor)?,
|
||||
resistor_lut: Self::build_lut(specs.resistor_specs(), DeviceKind::Resistor)?,
|
||||
capacitor_lut: Self::build_lut(specs.capacitor_specs(), DeviceKind::Capacitor)?,
|
||||
inductor_lut: Self::build_lut(specs.inductor_specs(), DeviceKind::Inductor)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_lut(dataset: &Dataset, device_kind: DeviceKind) -> Result<Vec<LutItem>, LcrConnError> {
|
||||
let mut lut: Vec<LutItem> = Vec::new();
|
||||
|
||||
let circuits = BfsResolver::iter_one_device_circuit(dataset)
|
||||
.chain(BfsResolver::iter_two_devices_circuit(dataset))
|
||||
.chain(BfsResolver::iter_three_devices_circuit(dataset));
|
||||
|
||||
for circuit in circuits {
|
||||
lut.push(LutItem::new(circuit, device_kind)?);
|
||||
}
|
||||
|
||||
lut.sort_by(|a, b| a.value.partial_cmp(&b.value).unwrap_or(Ordering::Equal));
|
||||
fn build_lut(
|
||||
specs: &SpecGroup,
|
||||
device_kind: DeviceKind,
|
||||
) -> Result<Vec<LutItem>, LutResolverError> {
|
||||
// Fetch all items
|
||||
let mut lut = itertools::chain!(
|
||||
BfsResolver::iter_one_device_circuit(&specs),
|
||||
BfsResolver::iter_two_devices_circuit(&specs),
|
||||
BfsResolver::iter_three_devices_circuit(&specs)
|
||||
)
|
||||
.map(|circuit| -> Result<LutItem, LutResolverError> { LutItem::new(circuit, device_kind) })
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
// Sort them and return
|
||||
lut.sort_by(|a, b| a.value.cmp(&b.value));
|
||||
Ok(lut)
|
||||
}
|
||||
|
||||
@@ -82,75 +96,156 @@ impl LutResolver {
|
||||
DeviceKind::Inductor => &self.inductor_lut,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for LutResolver {
|
||||
fn resolve(&self, request: &Request) -> Result<Response, LcrConnError> {
|
||||
let lut = self.pick_lut(request.device_kind);
|
||||
let target = request.target_value;
|
||||
let count_limit = request.count_limit;
|
||||
fn intern_resolve(&self, request: &Request) -> Result<Response, LutResolverError> {
|
||||
let lut = self.pick_lut(request.get_device_kind());
|
||||
let target_value = request.get_target_value();
|
||||
let count_limit = request.get_count_limit();
|
||||
let mut bucket: Vec<Circuit> = Vec::new();
|
||||
|
||||
// Locate the insertion point of target in the sorted LUT.
|
||||
// left/right start at the two nearest neighbours and expand outward.
|
||||
let lower_bound = 0;
|
||||
let upper_bound = lut.len() - 1;
|
||||
let target = OrderedFloat(target_value);
|
||||
let idx = lut.partition_point(|item| item.value < target);
|
||||
let mut left = RangedIndex::new(idx, lower_bound, upper_bound);
|
||||
let mut right = left.clone();
|
||||
left.dec();
|
||||
|
||||
// Expand outward non-symmetrically: at each step compare the two
|
||||
// candidates on each side and advance the one that is closer to the
|
||||
// target. This guarantees items are visited in strictly increasing
|
||||
// candidates on each side and advance the one that is closer to the target.
|
||||
// This guarantees items are visited in strictly increasing
|
||||
// difference order, so the first N items within tolerance are exactly
|
||||
// the N best matches.
|
||||
let mut left = idx as isize - 1;
|
||||
let mut right = idx as isize;
|
||||
let lut_len = lut.len() as isize;
|
||||
|
||||
let cv_trait = CircuitCalculator::new(request.device_kind, target);
|
||||
|
||||
while left >= 0 || right < lut_len {
|
||||
loop {
|
||||
// Check result count
|
||||
if bucket.len() >= count_limit {
|
||||
break;
|
||||
}
|
||||
|
||||
let go_left = if left < 0 {
|
||||
false
|
||||
} else if right >= lut_len {
|
||||
true
|
||||
} else {
|
||||
let left_item = &lut[left as usize];
|
||||
let left_diff =
|
||||
cv_trait.unsigned_difference(left_item.circuit(), Some(left_item.value()))?;
|
||||
let right_item = &lut[right as usize];
|
||||
let right_diff = cv_trait
|
||||
.unsigned_difference(right_item.circuit(), Some(right_item.value()))?;
|
||||
let go_left = if left.in_range() {
|
||||
if right.in_range() {
|
||||
let left_item = &lut[left.position()];
|
||||
let left_diff = CircuitEvaluation::from_circuit_value(left_item.value(),target_value)?.unsigned_difference;
|
||||
let right_item = &lut[right.position()];
|
||||
let right_diff = CircuitEvaluation::from_circuit_value(right_item.value(), target_value)?.unsigned_difference;
|
||||
left_diff <= right_diff
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
if right.in_range() {
|
||||
false
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let item = if go_left {
|
||||
let item = &lut[left as usize];
|
||||
left -= 1;
|
||||
let item = &lut[left.position()];
|
||||
left.dec();
|
||||
item
|
||||
} else {
|
||||
let item = &lut[right as usize];
|
||||
right += 1;
|
||||
let item = &lut[right.position()];
|
||||
right.inc();
|
||||
item
|
||||
};
|
||||
|
||||
let diff = cv_trait.unsigned_difference(item.circuit(), Some(item.value()))?;
|
||||
let diff = CircuitEvaluation::from_circuit_value(item.value(), target_value)?.unsigned_difference;
|
||||
// Since the LUT is sorted, values on each side only move further
|
||||
// from target as we advance. Once one side exceeds tolerance,
|
||||
// the rest of that side is guaranteed out of range — disable it.
|
||||
if diff > request.tolerance {
|
||||
if go_left {
|
||||
left = -1;
|
||||
} else {
|
||||
right = lut_len;
|
||||
}
|
||||
continue;
|
||||
// the rest of that side is guaranteed out of range.
|
||||
if diff > request.get_tolerance() {
|
||||
break;
|
||||
}
|
||||
|
||||
bucket.push(item.circuit().clone());
|
||||
}
|
||||
|
||||
Response::new(request, bucket)
|
||||
Ok(Response::new(request, bucket.into_iter())?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolver for LutResolver {
|
||||
fn resolve(&self, request: &Request) -> Result<Response, ResolverError> {
|
||||
Ok(self.intern_resolve(request)?)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// endregion
|
||||
|
||||
// region: Ranged Index Helper
|
||||
|
||||
/// The ranged index for bisect LUT finding in resolver.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RangedIndex {
|
||||
pos: Option<usize>,
|
||||
lower_bound: usize,
|
||||
upper_bound: usize,
|
||||
}
|
||||
|
||||
impl RangedIndex {
|
||||
/// Build ranged index with position, lower and upper bound.
|
||||
pub fn new(pos: usize, lower_bound: usize, upper_bound: usize) -> Self {
|
||||
let pos = if pos < lower_bound || pos > upper_bound {
|
||||
None
|
||||
} else {
|
||||
Some(pos)
|
||||
};
|
||||
|
||||
Self {
|
||||
pos,
|
||||
lower_bound,
|
||||
upper_bound,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the index is in range. True if it is, otherwise false.
|
||||
pub fn in_range(&self) -> bool {
|
||||
self.pos.is_some()
|
||||
}
|
||||
|
||||
/// Get the index as usize.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panic if index is out of range.
|
||||
pub fn position(&self) -> usize {
|
||||
self.pos.expect("unexpected out of range index")
|
||||
}
|
||||
|
||||
/// Increment the index. Return true if the index is advanced.
|
||||
pub fn inc(&mut self) -> bool {
|
||||
match self.pos {
|
||||
Some(pos) => {
|
||||
self.pos = if pos >= self.upper_bound {
|
||||
None
|
||||
} else {
|
||||
Some(pos + 1)
|
||||
};
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement the index. Return true if the index is advanced.
|
||||
pub fn dec(&mut self) -> bool {
|
||||
match self.pos {
|
||||
Some(pos) => {
|
||||
self.pos = if pos <= self.lower_bound {
|
||||
None
|
||||
} else {
|
||||
Some(pos - 1)
|
||||
};
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
pub mod bfs;
|
||||
pub mod lut;
|
||||
|
||||
use crate::common::LcrConnError;
|
||||
use crate::query::{Request, Response};
|
||||
|
||||
/// Abstract base trait for all resolvers.
|
||||
pub trait Resolver {
|
||||
/// Resolve the request and return the response.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The request to resolve.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The response containing the best matching circuits.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// See [`Circuit::compute`](crate::common::Circuit::compute).
|
||||
fn resolve(&self, request: &Request) -> Result<Response, LcrConnError>;
|
||||
}
|
||||
|
||||
pub use bfs::BfsResolver;
|
||||
pub use lut::LutResolver;
|
||||
@@ -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 preset")
|
||||
}
|
||||
|
||||
/// 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 preset")
|
||||
}
|
||||
|
||||
/// 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 preset")
|
||||
}
|
||||
|
||||
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 iter(&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
|
||||
@@ -0,0 +1,11 @@
|
||||
use lcrconn::spec;
|
||||
|
||||
#[test]
|
||||
fn test_spec_preset() {
|
||||
// All individual preset and catalog preset should nit panic
|
||||
let _ = spec::SpecGroup::resistor_preset();
|
||||
let _ = spec::SpecGroup::capacitor_preset();
|
||||
let _ = spec::SpecGroup::inductor_preset();
|
||||
|
||||
let _ = spec::SpecCatalog::devices_preset();
|
||||
}
|
||||
@@ -321,6 +321,7 @@ class App:
|
||||
def __get_device_unit(self, device_kind: DeviceKind) -> str:
|
||||
match device_kind:
|
||||
case DeviceKind.RESISTOR:
|
||||
# YYC MARK: This is ohm char.
|
||||
return "\u2126"
|
||||
case DeviceKind.CAPACITOR:
|
||||
return "F"
|
||||
|
||||
@@ -15,7 +15,7 @@ class DatasetItem:
|
||||
def __post_init__(self):
|
||||
value = self.value
|
||||
if value <= 0:
|
||||
raise ValueError(f"Invalid value {value} in dataset")
|
||||
raise ValueError(f"Invalid value {value} in dataset item")
|
||||
str_value = self.str_value
|
||||
if len(str_value) == 0:
|
||||
raise ValueError(f"Unexpected empty string in dataset item")
|
||||
@@ -286,7 +286,7 @@ class DatasetCollection:
|
||||
self.__inductor.save_file(inductor)
|
||||
|
||||
@property
|
||||
def resistor_values(self) -> Dataset:
|
||||
def resistor_dataset(self) -> Dataset:
|
||||
"""
|
||||
Get the available standard values for resistor
|
||||
|
||||
@@ -295,7 +295,7 @@ class DatasetCollection:
|
||||
return self.__resistor
|
||||
|
||||
@property
|
||||
def capacitor_values(self) -> Dataset:
|
||||
def capacitor_dataset(self) -> Dataset:
|
||||
"""
|
||||
Get the available standard values for capacitor
|
||||
|
||||
@@ -304,7 +304,7 @@ class DatasetCollection:
|
||||
return self.__capacitor
|
||||
|
||||
@property
|
||||
def inductor_values(self) -> Dataset:
|
||||
def inductor_dataset(self) -> Dataset:
|
||||
"""
|
||||
Get the available standard values for inductor
|
||||
|
||||
|
||||
@@ -239,11 +239,11 @@ class BfsResolver(Resolver):
|
||||
dataset: Dataset
|
||||
match request.device_kind:
|
||||
case DeviceKind.RESISTOR:
|
||||
dataset = self.__datasets.resistor_values
|
||||
dataset = self.__datasets.resistor_dataset
|
||||
case DeviceKind.CAPACITOR:
|
||||
dataset = self.__datasets.capacitor_values
|
||||
dataset = self.__datasets.capacitor_dataset
|
||||
case DeviceKind.INDUCTOR:
|
||||
dataset = self.__datasets.inductor_values
|
||||
dataset = self.__datasets.inductor_dataset
|
||||
|
||||
# Iterate circuit item one by one
|
||||
bucket = ResultBucket(request.count_limit)
|
||||
|
||||
@@ -44,13 +44,13 @@ class LutResolver(Resolver):
|
||||
|
||||
def __init__(self, datasets: DatasetCollection):
|
||||
self.__resistor_lut = LutResolver.__build_lut(
|
||||
datasets.resistor_values, DeviceKind.RESISTOR
|
||||
datasets.resistor_dataset, DeviceKind.RESISTOR
|
||||
)
|
||||
self.__capacitor_lut = LutResolver.__build_lut(
|
||||
datasets.capacitor_values, DeviceKind.CAPACITOR
|
||||
datasets.capacitor_dataset, DeviceKind.CAPACITOR
|
||||
)
|
||||
self.__inductor_lut = LutResolver.__build_lut(
|
||||
datasets.inductor_values, DeviceKind.INDUCTOR
|
||||
datasets.inductor_dataset, DeviceKind.INDUCTOR
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -121,13 +121,9 @@ class LutResolver(Resolver):
|
||||
diff = ccalc.unsigned_difference(item.circuit, value=item.value)
|
||||
# Since the LUT is sorted, values on each side only move further
|
||||
# from target as we advance. Once one side exceeds tolerance,
|
||||
# the rest of that side is guaranteed out of range — disable it.
|
||||
# the rest of that side is guaranteed out of range.
|
||||
if diff > request.tolerance:
|
||||
if go_left:
|
||||
left = -1
|
||||
else:
|
||||
right = len(lut)
|
||||
continue
|
||||
break
|
||||
|
||||
bucket.append(item.circuit)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user