Compare commits

..
28 Commits
Author SHA1 Message Date
yyc12345 782bd86407 refactor: finish refactor of kernel 2026-07-22 20:27:34 +08:00
yyc12345 5a41c6266c refactor: continue refactoring kernel 2026-07-22 16:59:55 +08:00
yyc12345 1514d25da1 refactor: refactor kernel dataset into spec 2026-07-22 14:30:29 +08:00
yyc12345 3dd8275787 refactor: refactor kernel common to have better code 2026-07-22 11:57:56 +08:00
yyc12345 5bfdfb71c2 fix: fix all build issue of lcrconn-cli 2026-06-29 21:52:39 +08:00
yyc12345 bbd47006d4 fix: fix kernel lut build issue 2026-06-29 20:10:23 +08:00
yyc12345 4af8dd97a2 fix: fix kernel bfs resolver issue 2026-06-29 16:56:13 +08:00
yyc12345 a62ed05d50 fix: fix request error 2026-06-29 14:35:19 +08:00
yyc12345 24e4c595bb refactor: rename names in legacy according to the change in kernel 2026-06-29 14:06:20 +08:00
yyc12345 1d9322f300 fix: fix dataset build issue 2026-06-29 14:05:56 +08:00
yyc12345 9d9c0473e8 fix: update kernel common 2026-06-29 10:06:00 +08:00
yyc12345 ccd96e6801 refactor: rename the trait for comouting circuit values in legacy to match the same modification in kernel 2026-06-28 22:31:30 +08:00
yyc12345 cca66b0cac fix: fix kernel common module build issue 2026-06-28 22:30:32 +08:00
yyc12345 aa6c4f72bd feat: use AI to migrate project (no fix now) 2026-06-28 20:47:00 +08:00
yyc12345 7665de0889 fix: fix bugs pointed by AI 2026-06-28 17:23:40 +08:00
yyc12345 e587d3e5ff feat: initialize rust project 2026-06-28 16:24:46 +08:00
yyc12345 73e539933a feat: add preset for dataset 2026-06-28 16:17:23 +08:00
yyc12345 215146bf59 feat: support save feature for dataset 2026-06-28 13:55:03 +08:00
yyc12345 5ad3e11836 feat: improve response plan display 2026-06-28 11:19:25 +08:00
yyc12345 5b3a2c030d feat: update circuit display in cli 2026-06-27 23:28:30 +08:00
yyc12345 3bec73b718 doc: add resolver hint in doc 2026-06-25 13:53:22 +08:00
yyc12345 21a625b054 fix: use circuit compute trait in lut resolver 2026-06-25 12:40:29 +08:00
yyc12345 826dda9841 doc: update progress 2026-06-25 12:32:35 +08:00
yyc12345 7fa0f56495 feat: remove astar, use bfs instead 2026-06-25 12:31:09 +08:00
yyc12345 625628cc1a doc: add history about this project 2026-06-17 20:30:48 +08:00
yyc12345 181c483923 fix: fix human readable value display 2026-06-17 20:25:02 +08:00
yyc12345 d42885f1ab feat: update legacy
- use sorted lut and bisect to optimize lut resolver
- add circuit decuper
- add signed diff for response item
2026-06-17 19:49:54 +08:00
yyc12345 96fa6263a8 doc: add some infos
- add some infos in doc
- add count limit prompt in query step
2026-06-17 12:39:12 +08:00
27 changed files with 3817 additions and 276 deletions
+15 -1
View File
@@ -1,3 +1,17 @@
# LCR Connector # LCR Connector
TODO Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices.
This project is coming from HFUT experiment class where I need to build an active low-pass filter with limited laboratory components.
So I create this project to help me to find the closest circuit for my given value.
After a couple of years, I still find it useful, so I decide to refactor it.
And make it available for everyone in a more convenient way, like GUI and mobile app.
# Todos
- [x] Refactor the legacy version.
- [x] Add BFS resolver in legacy version to replace LUT resolver.
- [ ] Use Rust to fully rewrite the legacy version as a library.
- [ ] Use Rust to create a CLI based on the library created at previous step.
- [ ] Utilize FLTK to create a GUI in desktop operating system.
- [ ] Utilize Flutter to create a GUI in mobile operating system.
+1
View File
@@ -0,0 +1 @@
/target
+284
View File
@@ -0,0 +1,284 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
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",
]
[[package]]
name = "lcrconn-cli"
version = "1.0.0"
dependencies = [
"anyhow",
"clap",
"lcrconn",
"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]]
name = "once_cell_polyfill"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
+6
View File
@@ -0,0 +1,6 @@
[workspace]
resolver = "3"
members = ["lcrconn", "lcrconn-cli"]
[workspace.dependencies]
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "lcrconn-cli"
version = "1.0.0"
edition = "2024"
[dependencies]
anyhow = "1.0.103"
lcrconn = { path="../lcrconn" }
clap = { version="4.5.48", features=["derive"]}
strum = "=0.28.0"
strum_macros = "=0.28.0"
+541
View File
@@ -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
}
+103
View File
@@ -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
}
+15
View File
@@ -0,0 +1,15 @@
mod app;
mod cli;
fn main() {
let config = cli::parse_args();
let app = app::App::new(config).unwrap_or_else(|err| {
eprintln!("Fail to initialize application: {}", err);
std::process::exit(1);
});
app.run().unwrap_or_else(|err| {
eprintln!("Runtime error: {}", err);
std::process::exit(1);
});
}
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "lcrconn"
version = "1.0.0"
edition = "2024"
[dependencies]
thiserror = "2.0.12"
ordered-float = "=5.3.0"
itertools = "0.15.0"
strum = "=0.28.0"
strum_macros = "=0.28.0"
+392
View File
@@ -0,0 +1,392 @@
use strum_macros::EnumIter;
use thiserror::Error as TeError;
// region: Validator
/// Error occurs when validating floating point value.
#[derive(Debug, TeError)]
#[error("given floating point value {0} is invalid")]
pub struct FloatingPointError(f64);
/// 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 {
Err(FloatingPointError(f))
}
}
/// Error occurs when validating device value.
#[derive(Debug, TeError)]
pub enum DeviceValueError {
#[error("given device value is bad floating point: {0}")]
BadFloatingPoint(#[from] FloatingPointError),
#[error("given device value {0} is out of range")]
OutOfRange(f64),
}
/// 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 {
Err(DeviceValueError::OutOfRange(f))
}
}
// endregion
// region: Circuit Utilities
/// The kind of device.
#[derive(Debug, Clone, Copy)]
pub enum DeviceKind {
/// Resistor device.
Resistor,
/// Capacitor device.
Capacitor,
/// Inductor device.
Inductor,
}
/// The joint type between 2 devices.
#[derive(Debug, Clone, Copy, EnumIter)]
pub enum JointKind {
/// Series connection.
Series,
/// Parallel connection.
Parallel,
}
impl JointKind {
/// Flip the joint kind from series to parallel or vice versa.
///
/// # Returns
///
/// The flipped joint kind.
pub fn flip(self) -> Self {
match self {
JointKind::Series => JointKind::Parallel,
JointKind::Parallel => JointKind::Series,
}
}
}
/// The scale of devices in the circuit.
#[derive(Debug, Clone, Copy)]
pub enum CircuitDeviceScale {
/// One device.
One,
/// Two devices.
Two,
/// Three devices.
Three,
}
impl CircuitDeviceScale {
/// Convert circuit device scale to device count.
/// The return value only can be 1, 2, and 3.
pub fn to_device_count(self) -> usize {
match self {
CircuitDeviceScale::One => 1,
CircuitDeviceScale::Two => 2,
CircuitDeviceScale::Three => 3,
}
}
}
// endregion
// region: Circuit Stuff
/// Error occurs when manipulating [Circuit] and [SubCircuit].
#[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")]
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("floating point is invalid after arithmetic operation: {0}")]
BadArithmetic(FloatingPointError),
}
/// The part of circuit composed of two devices and the joint kind.
#[derive(Debug, Clone)]
pub struct SubCircuit {
/// The value of the device.
device_value: f64,
/// The joint kind between this device and the next device.
joint_kind: JointKind,
}
impl SubCircuit {
/// Initialize subcircuit with given device value and joint kind.
///
/// The input device value should greater than zero,
/// otherwise an error will return.
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,
})
}
/// Evaluate the joint value with given previous joint evaluated value and device kind.
///
/// 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 evaluate(&self, value: f64, device_kind: DeviceKind) -> Result<f64, CircuitError> {
// Check the range of provided value for computing
let value =
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.
let joint_kind = match device_kind {
DeviceKind::Capacitor => self.joint_kind.flip(),
_ => self.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| CircuitError::BadArithmetic(err))
}
/// Get the device value.
pub fn device_value(&self) -> f64 {
self.device_value
}
/// Get the joint kind.
pub fn joint_kind(&self) -> JointKind {
self.joint_kind
}
}
/// The circuit composed of multiple joints.
#[derive(Clone, Debug)]
pub struct Circuit {
/// The value of the first device.
first_device_value: f64,
/// The second device and its joint property.
second_device_subckt: Option<SubCircuit>,
/// The third device and its joint property.
third_device_subckt: Option<SubCircuit>,
}
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.
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 = 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::InterleavedSubCircuit);
}
// Everything is okey
Ok(Self {
first_device_value,
second_device_subckt,
third_device_subckt,
})
}
/// Create a circuit from a single device.
pub fn from_one_device(device1_value: f64) -> Result<Self, CircuitError> {
Self::new(device1_value, None, None)
}
/// Create a circuit from two devices.
pub fn from_two_devices(
device1_value: f64,
device2_value: f64,
device2_joint: JointKind,
) -> Result<Self, CircuitError> {
Self::new(
device1_value,
Some(SubCircuit::new(device2_value, device2_joint)?),
None,
)
}
/// Create a circuit from three devices.
pub fn from_three_devices(
device1_value: f64,
device2_value: f64,
device2_joint: JointKind,
device3_value: f64,
device3_joint: JointKind,
) -> Result<Self, CircuitError> {
Self::new(
device1_value,
Some(SubCircuit::new(device2_value, device2_joint)?),
Some(SubCircuit::new(device3_value, device3_joint)?),
)
}
/// 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.evaluate(value, device_kind)?,
None => return Ok(value),
}
match &self.third_device_subckt {
Some(subckt) => value = subckt.evaluate(value, device_kind)?,
None => return Ok(value),
}
Ok(value)
}
/// Get the device scale.
///
/// # Returns
///
/// The device scale.
pub fn device_scale(&self) -> CircuitDeviceScale {
if self.third_device_subckt.is_some() {
CircuitDeviceScale::Three
} else if self.second_device_subckt.is_some() {
CircuitDeviceScale::Two
} else {
CircuitDeviceScale::One
}
}
/// Get the value of the first device.
pub fn first_device_value(&self) -> f64 {
self.first_device_value
}
/// Get the joint kind of the second device.
pub fn second_device_joint(&self) -> Result<JointKind, CircuitError> {
self.second_device_subckt
.as_ref()
.map(|s| s.joint_kind())
.ok_or(CircuitError::NoSuchDevice)
}
/// Get the value of the second device.
pub fn second_device_value(&self) -> Result<f64, CircuitError> {
self.second_device_subckt
.as_ref()
.map(|s| s.device_value())
.ok_or(CircuitError::NoSuchDevice)
}
/// Get the joint kind of the third device.
pub fn third_device_joint(&self) -> Result<JointKind, CircuitError> {
self.third_device_subckt
.as_ref()
.map(|s| s.joint_kind())
.ok_or(CircuitError::NoSuchDevice)
}
/// Get the value of the third device.
pub fn third_device_value(&self) -> Result<f64, CircuitError> {
self.third_device_subckt
.as_ref()
.map(|s| s.device_value())
.ok_or(CircuitError::NoSuchDevice)
}
}
/// The evaluation result of circuit with target value.
#[derive(Debug, Clone)]
pub struct CircuitEvaluation {
/// The value of this circuit.
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.
pub difference: f64,
/// The unsigned difference between the target value and the value of this circuit.
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.
pub relative_difference: f64,
/// The unsigned relative difference between the target value and the value of this circuit.
pub unsigned_relative_difference: f64,
}
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,
})
}
/// 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)
}
}
// endregion
+8
View File
@@ -0,0 +1,8 @@
pub mod common;
pub mod spec;
pub mod query;
pub mod resolver;
pub use common::DeviceKind;
pub use query::{Request, Response, ResponsePriority};
pub use resolver::{Resolver, BfsResolver, LutResolver};
+262
View File
@@ -0,0 +1,262 @@
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(Debug, Clone, Copy)]
pub enum ResponsePriority {
/// Less devices is the first priority.
LessDevices,
/// More accuracy is the first priority.
MoreAccuracy,
}
/// 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.
device_kind: DeviceKind,
/// The target value of the device.
target_value: f64,
/// The tolerance of the device in absolute value.
tolerance: f64,
/// The priority principle when sorting response items.
response_priority: ResponsePriority,
/// The limited count of results.
count_limit: usize,
}
impl Request {
/// Create a new request with validation.
pub fn new(
device_kind: DeviceKind,
target_value: f64,
tolerance: f64,
response_priority: ResponsePriority,
count_limit: usize,
) -> 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(RequestError::BadCountLimit(count_limit));
}
// Everything is okey.
Ok(Self {
device_kind,
target_value,
tolerance,
response_priority,
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.
#[derive(Clone, Debug)]
pub struct ResponseItem {
/// The circuit of this response item.
circuit: Circuit,
/// The evaluation result of this circuit.
circuit_evaluation: CircuitEvaluation,
}
impl ResponseItem {
/// Create a new response item by computing all values eagerly.
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,
circuit_evaluation,
})
}
/// The circuit of this response item.
pub fn circuit(&self) -> &Circuit {
&self.circuit
}
/// The device count of this circuit.
pub fn device_count(&self) -> usize {
self.circuit.device_scale().to_device_count()
}
/// The value of this circuit.
pub fn value(&self) -> f64 {
self.circuit_evaluation.value
}
/// 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.
pub fn difference(&self) -> f64 {
self.circuit_evaluation.difference
}
/// The unsigned difference between the target value and the value of this circuit.
pub fn unsigned_difference(&self) -> f64 {
self.circuit_evaluation.unsigned_difference
}
/// 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.
pub fn relative_difference(&self) -> f64 {
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.circuit_evaluation.unsigned_relative_difference
}
}
/// The collection of possible solutions given by the resolver.
///
/// For getting the response items, please use `response[index]` or `response.get(index)`.
/// For iterating the response items, please use the `into_iter()` method.
/// For getting the count of response items, please use the `len()` method.
pub struct Response {
/// The kind of device of this response.
device_kind: DeviceKind,
/// The sorted items by priority and difference.
sorted_items: Vec<ResponseItem>,
}
impl Response {
/// Create a new response from request and candidate circuits.
///
/// The candidates are sorted by the priority specified in the request and then truncated
/// to the count limit.
///
/// # Errors
///
/// See [`ResponseItem::new`].
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, 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(|| {
OrderedFloat(a.unsigned_difference())
.cmp(&OrderedFloat(b.unsigned_difference()))
})
});
}
ResponsePriority::MoreAccuracy => {
items.sort_by(|a, b| {
OrderedFloat(a.unsigned_difference())
.cmp(&OrderedFloat(b.unsigned_difference()))
});
}
}
// Cut item by limit
items.truncate(request.count_limit);
Ok(Self {
device_kind: request.device_kind,
sorted_items: items,
})
}
/// The kind of device of this response.
pub fn device_kind(&self) -> DeviceKind {
self.device_kind
}
/// The number of response items.
pub fn len(&self) -> usize {
self.sorted_items.len()
}
/// Whether the response is empty.
pub fn is_empty(&self) -> bool {
self.sorted_items.is_empty()
}
/// Get a response item by index.
pub fn get(&self, index: usize) -> Option<&ResponseItem> {
self.sorted_items.get(index)
}
/// Iterate over response items by reference.
pub fn iter(&self) -> impl Iterator<Item = &ResponseItem> {
self.sorted_items.iter()
}
}
+26
View File
@@ -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;
+370
View File
@@ -0,0 +1,370 @@
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 strum::IntoEnumIterator;
use thiserror::Error as TeError;
// region: BFS Resolver Kernel
/// 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),
}
// 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 evaluated value of the circuit.
value: f64,
/// The unsigned difference between the target value and the value of this circuit.
unsigned_difference: f64,
}
impl BfsItem {
/// Create a new BFS item by computing values eagerly.
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: eval.value,
unsigned_difference: eval.unsigned_difference,
})
}
/// The circuit represented by this item.
pub fn circuit(&self) -> &Circuit {
&self.circuit
}
/// The evaluated value of the circuit.
pub fn value(&self) -> f64 {
self.value
}
/// The unsigned difference between the target value and the value of this circuit.
pub fn unsigned_difference(&self) -> f64 {
self.unsigned_difference
}
/// Consume this item and return the inner circuit.
pub fn into_circuit(self) -> Circuit {
self.circuit
}
}
// 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: OrderedFloat<f64>,
/// The underlying [BfsItem].
item: BfsItem,
/// Monotonic counter used as a tiebreaker when scores are equal,
/// ensuring that BinaryHeap never compares [BfsItem] directly.
seq: usize,
}
impl ResultBucketItem {
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.eq(&other.score) && self.seq.eq(&other.seq)
}
}
impl Eq for ResultBucketItem {}
impl PartialOrd for ResultBucketItem {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
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.
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.
struct ResultBucket {
/// Maximum number of items the bucket can hold.
n: usize,
/// Max-heap of [`ResultBucketItem`].
/// The entry with the largest score sits at index 0.
heap: BinaryHeap<ResultBucketItem>,
/// Monotonic counter fed to each [`ResultBucketItem`] as a tiebreaker,
/// preventing BinaryHeap from comparing BfsItem on score collisions.
counter: usize,
}
impl ResultBucket {
/// Create a new bucket that holds at most `n` items.
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.
/// Otherwise the item is only inserted when `score` is smaller
/// 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.
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()
.expect("unexpected blank binary heap")
.get_score()
{
false
} else {
*self.heap.peek_mut().expect("unexpected blank binary heap") = entry;
self.counter += 1;
true
}
}
}
// endregion
// endregion
+251
View File
@@ -0,0 +1,251 @@
use super::bfs::BfsResolver;
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: OrderedFloat<f64>,
}
impl LutItem {
/// Create a new LUT item by computing the 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.
pub fn circuit(&self) -> &Circuit {
&self.circuit
}
/// The value of this circuit.
pub fn value(&self) -> f64 {
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.
resistor_lut: Vec<LutItem>,
/// The lookup table for capacitors.
capacitor_lut: Vec<LutItem>,
/// The lookup table for inductors.
inductor_lut: Vec<LutItem>,
}
impl LutResolver {
/// 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(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(
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)
}
fn pick_lut(&self, device_kind: DeviceKind) -> &[LutItem] {
match device_kind {
DeviceKind::Resistor => &self.resistor_lut,
DeviceKind::Capacitor => &self.capacitor_lut,
DeviceKind::Inductor => &self.inductor_lut,
}
}
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
// difference order, so the first N items within tolerance are exactly
// the N best matches.
loop {
// Check result count
if bucket.len() >= count_limit {
break;
}
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.position()];
left.dec();
item
} else {
let item = &lut[right.position()];
right.inc();
item
};
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.
if diff > request.get_tolerance() {
break;
}
bucket.push(item.circuit().clone());
}
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
+496
View File
@@ -0,0 +1,496 @@
//! Types for managing the rated values of components available in your lab.
//!
//! In this module, a "spec" means a single rated parameter value of a component,
//! such as `100` Ohms, `4.7k` Ohms, or `10u` Farads. It is **not** a general
//! technical specification document — it is simply the nominal value printed on
//! the component's body.
//!
//! - [`Spec`] — one rated value (e.g., 4.7k).
//! - [`SpecGroup`] — all rated values of a given component type that your lab
//! actually has in stock (e.g., all resistor values available in your drawer).
//! - [`SpecCatalog`] — the complete collection of rated values for resistors,
//! capacitors, and inductors.
//!
//! In short: these types answer the question "which exact component values can
//! I pick from the shelf?".
use crate::common::{
DeviceValueError, FloatingPointError, validate_device_value, validate_floating_point,
};
use ordered_float::OrderedFloat;
use std::collections::HashSet;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Error as IoError, Write};
use std::num::ParseFloatError;
use std::path::Path;
use thiserror::Error as TeError;
/// Errors that can occur when working with rated component values.
#[derive(Debug, TeError)]
pub enum SpecError {
#[error("invalid device value: {0}")]
BadDeviceValue(#[from] DeviceValueError),
#[error("bad string form of device value: {0}")]
ParseHumanReadableValue(#[from] ParseHumanReadableValueError),
#[error("duplicate rated value: {0}")]
DupSpecItem(String),
#[error("empty rated value group")]
EmptySpecGroup,
#[error("fail to open rated values file: {0}")]
OpenSpecFile(IoError),
#[error("fail to read rated values file: {0}")]
ReadSpecFile(IoError),
#[error("fail to write rated values file: {0}")]
WriteSpecFile(IoError),
}
/// One rated value of a component (e.g., `4.7k` standing for 4700Ohms).
///
/// A `Spec` stores both the parsed numeric value and the original human-readable
/// string so that the value can be re-serialized exactly as it was entered.
#[derive(Debug, Clone)]
struct Spec {
/// The numeric rated value (e.g., `4700.0` for `"4.7k"`).
value: f64,
/// The original human-readable form (e.g., `"4.7k"`), kept for faithful
/// round-trip serialization.
str_value: String,
}
impl Spec {
/// Create a new rated value from its human-readable representations.
pub fn new(str_value: String) -> Result<Self, SpecError> {
// Try parsing value and check its range
let value = from_human_readable_value(&str_value)?;
let value = validate_device_value(value)?;
Ok(Self { value, str_value })
}
/// Get the numeric rated value (e.g., `4700.0` for `"4.7k"`).
pub fn get_value(&self) -> f64 {
self.value
}
/// Get the original human-readable value form (e.g., `"4.7k"`).
pub fn get_str_value(&self) -> &str {
&self.str_value
}
}
/// All rated values that your lab stocks for a single component type.
///
/// For example, a `SpecGroup` for resistors might hold `{100, 220, 470, 1k, 4.7k, 10k}`
/// — these are the actual resistor values you have on hand. The same concept applies
/// to capacitors and inductors.
pub struct SpecGroup {
/// The rated values belonging to this group.
specs: Vec<Spec>,
}
impl SpecGroup {
/// Internal constructor: parse and deduplicate a sequence of human-readable rated values.
fn new<I>(str_values: I) -> Result<Self, SpecError>
where
I: IntoIterator<Item = String>,
{
// Check string form value one by one
let mut specs: Vec<Spec> = Vec::new();
let mut seen: HashSet<OrderedFloat<f64>> = HashSet::new();
for str_value in str_values {
// Build spec instance
let spec = Spec::new(str_value)?;
// Check and update set
if !seen.insert(OrderedFloat(spec.get_value())) {
return Err(SpecError::DupSpecItem(spec.get_str_value().to_string()));
}
// Add into result
specs.push(spec);
}
// Check empty case
if specs.is_empty() {
return Err(SpecError::EmptySpecGroup);
}
// Ok, assign it
Ok(Self { specs })
}
/// Build a spec group from any iterable of human-readable rated values (e.g., `"4.7k"`, `"100"`).
pub fn from_iterator<I, S>(str_values: I) -> Result<Self, SpecError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self::new(str_values.into_iter().map(|i| i.into()))
}
/// Read rated values from a text block, one value per non-empty line.
pub fn from_text(text: &str) -> Result<Self, SpecError> {
let lines = text
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty());
Self::from_iterator(lines)
}
/// Read rated values from a file, one value per non-empty line.
pub fn from_file<P>(path: P) -> Result<Self, SpecError>
where
P: AsRef<Path>,
{
let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?;
let reader = BufReader::new(file);
let lines = reader
.lines()
.map(|line| line.map(|line| line.trim().to_string()))
.filter(|line| !matches!(line, Ok(line) if line.is_empty()))
.collect::<Result<Vec<_>, _>>()
.map_err(|err| SpecError::ReadSpecFile(err))?;
Self::from_iterator(lines.into_iter())
}
/// A commonly used set of resistor rated values (E12derived).
pub fn resistor_preset() -> Self {
Self::from_iterator([
"100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k",
"6.8k", "10k", "47k", "100k", "1M",
]).expect("unexpected bad rated values 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 devicetype spec groups.
pub fn new(resistor: SpecGroup, capacitor: SpecGroup, inductor: SpecGroup) -> Self {
Self {
resistor,
capacitor,
inductor,
}
}
/// Build a catalogue from three iterables of humanreadable rated values.
///
/// * `resistor` — values such as `"100"`, `"4.7k"`, etc.
/// * `capacitor` — values such as `"10p"`, `"4.7u"`, etc.
/// * `inductor` — values such as `"1u"`, `"10u"`, etc.
pub fn from_iterable<I1, S1, I2, S2, I3, S3>(
resistor: I1,
capacitor: I2,
inductor: I3,
) -> Result<Self, SpecError>
where
I1: IntoIterator<Item = S1>,
S1: Into<String>,
I2: IntoIterator<Item = S2>,
S2: Into<String>,
I3: IntoIterator<Item = S3>,
S3: Into<String>,
{
Ok(Self {
resistor: SpecGroup::from_iterator(resistor)?,
capacitor: SpecGroup::from_iterator(capacitor)?,
inductor: SpecGroup::from_iterator(inductor)?,
})
}
/// Build a catalogue from three text blocks, one value per line.
///
/// * `resistor` — the resistor ratedvalues text.
/// * `capacitor` — the capacitor ratedvalues text.
/// * `inductor` — the inductor ratedvalues text.
pub fn from_text(resistor: &str, capacitor: &str, inductor: &str) -> Result<Self, SpecError> {
Ok(Self {
resistor: SpecGroup::from_text(resistor)?,
capacitor: SpecGroup::from_text(capacitor)?,
inductor: SpecGroup::from_text(inductor)?,
})
}
/// Build a catalogue from three files, one value per line.
///
/// * `resistor` — path to the resistor ratedvalues file.
/// * `capacitor` — path to the capacitor ratedvalues file.
/// * `inductor` — path to the inductor ratedvalues file.
pub fn from_file<P1, P2, P3>(
resistor: P1,
capacitor: P2,
inductor: P3,
) -> Result<Self, SpecError>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
P3: AsRef<Path>,
{
Ok(Self {
resistor: SpecGroup::from_file(resistor)?,
capacitor: SpecGroup::from_file(capacitor)?,
inductor: SpecGroup::from_file(inductor)?,
})
}
/// A readytouse catalogue with common resistor, capacitor and inductor rated values.
pub fn devices_preset() -> Self {
Self {
resistor: SpecGroup::resistor_preset(),
capacitor: SpecGroup::capacitor_preset(),
inductor: SpecGroup::inductor_preset(),
}
}
/// Return one saveiterator for each of the three device types.
pub fn save_iterator(
&self,
) -> (
impl Iterator<Item = &str>,
impl Iterator<Item = &str>,
impl Iterator<Item = &str>,
) {
(
self.resistor.save_iterator(),
self.capacitor.save_iterator(),
self.inductor.save_iterator(),
)
}
/// Return the text representation of all three devicetype value sets.
pub fn save_text(&self) -> (String, String, String) {
(
self.resistor.save_text(),
self.capacitor.save_text(),
self.inductor.save_text(),
)
}
/// Save all three devicetype value sets to files, one value per line.
///
/// * `resistor` — file path for the resistor values.
/// * `capacitor` — file path for the capacitor values.
/// * `inductor` — file path for the inductor values.
pub fn save_file<P1, P2, P3>(
&self,
resistor: P1,
capacitor: P2,
inductor: P3,
) -> Result<(), SpecError>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
P3: AsRef<Path>,
{
self.resistor.save_file(resistor)?;
self.capacitor.save_file(capacitor)?;
self.inductor.save_file(inductor)?;
Ok(())
}
/// Access the resistor ratedvalue set.
pub fn resistor_specs(&self) -> &SpecGroup {
&self.resistor
}
/// Access the capacitor ratedvalue set.
pub fn capacitor_specs(&self) -> &SpecGroup {
&self.capacitor
}
/// Access the inductor ratedvalue set.
pub fn inductor_specs(&self) -> &SpecGroup {
&self.inductor
}
}
// region: Human Readable Value
#[derive(Debug, TeError)]
pub enum ParseHumanReadableValueError {
#[error("fail to parse floating point part of given human readable value: {0}")]
ParseFloat(#[from] ParseFloatError),
#[error("arithmetic error: {0}")]
BadArithmetic(#[from] FloatingPointError),
}
/// Convert human readable value to float.
///
/// `strl` is the human readable value.
/// The return value is the parsed float value. or error occurs when parsing.
///
/// This function guarantee that return value must be a valid floating value.
/// But do not guarantee that it can be used as device value.
/// It is possible that it is negative or zero floating point value.
pub fn from_human_readable_value(strl: &str) -> Result<f64, ParseHumanReadableValueError> {
let strl = strl.trim();
let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') {
(stripped, 1e-12)
} else if let Some(stripped) = strl.strip_suffix('p') {
(stripped, 1e-9)
} else if let Some(stripped) = strl.strip_suffix('u') {
(stripped, 1e-6)
} else if let Some(stripped) = strl.strip_suffix('m') {
(stripped, 1e-3)
} else if let Some(stripped) = strl.strip_suffix('k') {
(stripped, 1e3)
} else if let Some(stripped) = strl.strip_suffix('M') {
(stripped, 1e6)
} else if let Some(stripped) = strl.strip_suffix('G') {
(stripped, 1e9)
} else {
(strl, 1.0)
};
let num = num_part.parse::<f64>()?;
Ok(validate_floating_point(num * multiplier)?)
}
/// The unit scale for human readable value.
#[derive(Debug, Clone, Copy)]
pub enum UnitScale {
NanoLower,
Nano,
Micro,
Milli,
None,
Kilo,
Mega,
Giga,
GigaHigher,
}
/// Get the unit scale of human readable value.
///
/// `v` is the value for analyzing scale.
/// It must be a valid floating point value.
///
/// # Panics
///
/// This function panics when given floating point value is bad.
pub fn get_human_readable_value_scale(v: f64) -> UnitScale {
let v = validate_floating_point(v).expect("unexpected bad floating point value");
let v = v.abs();
if v < 1e-12 {
UnitScale::NanoLower
} else if v < 1e-9 {
UnitScale::Nano
} else if v < 1e-6 {
UnitScale::Micro
} else if v < 1e-3 {
UnitScale::Milli
} else if v < 1e3 {
UnitScale::None
} else if v < 1e6 {
UnitScale::Kilo
} else if v < 1e9 {
UnitScale::Mega
} else if v < 1e12 {
UnitScale::Giga
} else {
UnitScale::GigaHigher
}
}
/// Convert float value to human readable value.
///
/// `v`is the float value for formatting as human readable value.
/// It must be a valid floating point value.
///
/// # Panics
///
/// This function panics when given floating point value is bad.
pub fn to_human_readable_value(v: f64) -> String {
let scale = get_human_readable_value_scale(v);
match scale {
UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12),
UnitScale::Nano => format!("{:+.4} p", v / 1e-9),
UnitScale::Micro => format!("{:+.4} u", v / 1e-6),
UnitScale::Milli => format!("{:+.4} m", v / 1e-3),
// YYC MARK:
// The space of this format string is by design
// for keeping the same style with other format strings.
UnitScale::None => format!("{:+.4} ", v),
UnitScale::Kilo => format!("{:+.4} k", v / 1e3),
UnitScale::Mega => format!("{:+.4} M", v / 1e6),
UnitScale::Giga => format!("{:+.4} G", v / 1e9),
UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9),
}
}
// endregion
+11
View File
@@ -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();
}
+51 -2
View File
@@ -1,5 +1,54 @@
# LCR Connector (Legacy) # LCR Connector (Legacy)
在3个元器件内,使用给定元器件数值列表快速找到目标数值元器件的最好拼接方式,支持电阻,电容,电感 Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices.
执行`uv run lcr-connector --help`来查阅参数手册。 This is the legacy version of LCR Connector, although this is also refactored from true legacy version in modern Python.
## Usage
- Execute `uv sync` to configure the environment.
- Execute `uv run lcr-connector --help` for the usage of LCR Connector.
- After launch LCR Connector, you can see the help message in interactive console, or type `help` to see the help message.
A small hint for you: `lut` resolver is good and quick on most devices.
However, if you are running on memory-limited device, `bfs` resolver may be the substitute for you.
Additionaly, for using LCR Connector, you need 3 list files holding all possible device standard values which are available in your laboratory.
Each of them represents a type of device respectively, resistor, capacitor, or inductor.
These list files are basically like this:
```
100
220
270
390
470
680
1k
1.2k
1.5k
2.2k
3.3k
4.7k
6.8k
10k
47k
100k
1M
```
Supported units are:
- n: Nano
- p: Pico
- u: Micro
- m: Milli
- k: Kilo
- M: Mega
- G: Giga
There is no physical unit for the values in the list files.
Unit is optional. If you don't specify a unit, the value is considered as a plain floating value.
Unit is **case sensitive** to distinguish between milli and mega (e.g. 1m is milli, 1M is mega).
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "lcr-connector" name = "lcr-connector"
version = "1.0.0" version = "1.0.0"
description = "Use as much 3 devices to reach target value for resistor, capacitor and inductor." description = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices."
readme = "README.md" readme = "README.md"
authors = [ authors = [
{ name = "yyc12345", email = "yyc12321@outlook.com" } { name = "yyc12345", email = "yyc12321@outlook.com" }
+179 -42
View File
@@ -9,8 +9,8 @@ from .dataset import (
to_human_readable_value, to_human_readable_value,
from_human_readable_value, from_human_readable_value,
) )
from .query import Request, ResponsePriority, Response from .query import Request, ResponsePriority, Response, MAX_RESPONSE_CNT
from .resolver import Resolver, LutResolver, AStarResolver from .resolver import Resolver, LutResolver, BfsResolver
_TStrEnum = TypeVar("_TStrEnum", bound=enum.StrEnum) _TStrEnum = TypeVar("_TStrEnum", bound=enum.StrEnum)
@@ -22,11 +22,11 @@ class AppResolver(enum.StrEnum):
LUT = "lut" LUT = "lut"
"""The look-up table resolver.""" """The look-up table resolver."""
ASTAR = "astar" BFS = "bfs"
"""The A* resolver.""" """The BFS resolver."""
@dataclass @dataclass(frozen=True)
class AppConfig: class AppConfig:
""" """
The configuration for the app. The configuration for the app.
@@ -65,8 +65,8 @@ class App:
match self.__config.resolver: match self.__config.resolver:
case AppResolver.LUT: case AppResolver.LUT:
self.__resolver = LutResolver(self.__dataset) self.__resolver = LutResolver(self.__dataset)
case AppResolver.ASTAR: case AppResolver.BFS:
self.__resolver = AStarResolver(self.__dataset) self.__resolver = BfsResolver(self.__dataset)
def run(self) -> None: def run(self) -> None:
""" """
@@ -76,6 +76,8 @@ class App:
print('Type "help" for more info. Type "exit" to quit.') print('Type "help" for more info. Type "exit" to quit.')
self.__op_main() self.__op_main()
# region: Subcommand Processors
class MainCmd(enum.StrEnum): class MainCmd(enum.StrEnum):
QUERY = "query" QUERY = "query"
HELP = "help" HELP = "help"
@@ -143,14 +145,19 @@ class App:
tolerance = self.__accept_device_value_tolerance(target_value) tolerance = self.__accept_device_value_tolerance(target_value)
print("How to sort result?") print("How to sort result?")
print("l: less component")
print("a: more accuracy") print("a: more accuracy")
print("l: less component")
response_priority = self.__accept_command( response_priority = self.__accept_command(
App.QuerySortPriority App.QuerySortPriority
).to_response_priority() ).to_response_priority()
print("How may result are you expected?")
count_limit = self.__accept_count_value()
# build request and ask resolver # build request and ask resolver
request = Request(device_kind, target_value, tolerance, response_priority, 100) request = Request(
device_kind, target_value, tolerance, response_priority, count_limit
)
response = self.__resolver.resolve(request) response = self.__resolver.resolve(request)
# use page viewer to show result # use page viewer to show result
@@ -174,17 +181,8 @@ class App:
index = current_page * (ITEMS_PER_PAGE - 1) + i index = current_page * (ITEMS_PER_PAGE - 1) + i
if index >= cnt: if index >= cnt:
continue continue
# fetch item and print it # and print it
item = response[index] self.__illustrate_response(response, index)
print(
"Plan {0}\tValue: {1}\tDiff: {2} ({3:.2%})".format(
index + 1,
to_human_readable_value(item.value),
to_human_readable_value(item.difference),
item.relative_difference,
)
)
self.__illustrate_circuit(item.circuit)
# print page footer # print page footer
print("") print("")
@@ -199,6 +197,10 @@ class App:
case App.PageViewerCmd.QUIT: case App.PageViewerCmd.QUIT:
break break
# endregion
# region: Command Utilities
def __accept_command(self, cmd_enum: type[_TStrEnum]) -> _TStrEnum: def __accept_command(self, cmd_enum: type[_TStrEnum]) -> _TStrEnum:
""" """
Accept a command from the user. Accept a command from the user.
@@ -217,10 +219,31 @@ class App:
except ValueError: except ValueError:
print("Unknown command, please try again.") print("Unknown command, please try again.")
def __accept_count_value(self) -> int:
while True:
self.__show_prompt_arrow()
words = input()
if words == "":
continue
try:
value = int(words)
except ValueError:
print("Wrong value, please try again.")
continue
if value > MAX_RESPONSE_CNT or value <= 0:
print("Wrong value, please try again.")
else:
return value
def __accept_device_value(self) -> float: def __accept_device_value(self) -> float:
while True: while True:
self.__show_prompt_arrow() self.__show_prompt_arrow()
words = input() words = input()
if words == "":
continue
value = self.__parse_human_readable_value(words) value = self.__parse_human_readable_value(words)
if value is None: if value is None:
print("Wrong value, please try again.") print("Wrong value, please try again.")
@@ -231,6 +254,8 @@ class App:
while True: while True:
self.__show_prompt_arrow() self.__show_prompt_arrow()
words = input() words = input()
if words == "":
continue
if words.endswith("%"): if words.endswith("%"):
value = self.__parse_plain_float( value = self.__parse_plain_float(
@@ -289,33 +314,145 @@ class App:
else: else:
return None return None
def __get_joint_kind_symbol(self, joint_kind: JointKind) -> str: # endregion
match joint_kind:
case JointKind.SERIES:
return "S"
case JointKind.PARALLEL:
return "P"
def __illustrate_circuit(self, circuit: Circuit) -> None: # region: Response Display
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"
case DeviceKind.INDUCTOR:
return "H"
def __to_circult_graph_value(self, value: float, device_kind: DeviceKind) -> str:
# Remove sign and append device unit
return to_human_readable_value(value)[1:] + self.__get_device_unit(device_kind)
def __to_plan_head_value(self, value: float, device_kind: DeviceKind) -> str:
# Remove sign and append device unit
return to_human_readable_value(value)[1:] + self.__get_device_unit(device_kind)
def __to_plan_head_diff(self, value: float, device_kind: DeviceKind) -> str:
# Keep the sign and append device unit
return to_human_readable_value(value) + self.__get_device_unit(device_kind)
def __to_plan_head_diff_pct(self, value: float) -> str:
# Keep the sign and format it as percentage style without trailing device unit
return "{:.2%}".format(value)
# 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.
def __illustrate_response(self, response: Response, index: int) -> None:
"""
Illustrate response item with given response and item.
:param response: The response to illustrate.
:param index: The zero-based index of the item to illustrate.
"""
# fetch item and device kind from response
item = response[index]
device_kind = response.device_kind
# print header
print(
"Plan {0:<4} Value: {1:<16} Diff: {2} ({3})".format(
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)
def __illustrate_circuit(self, circuit: Circuit, device_kind: DeviceKind) -> None:
match circuit.device_scale: match circuit.device_scale:
case CircuitDeviceScale.ONE: case CircuitDeviceScale.ONE:
dev1 = to_human_readable_value(circuit.first_device_value) self.__illustrate_one_device_circuit(circuit, device_kind)
print(f"{dev1}")
case CircuitDeviceScale.TWO: case CircuitDeviceScale.TWO:
dev1 = to_human_readable_value(circuit.first_device_value) self.__illustrate_two_device_circuit(circuit, device_kind)
j2 = self.__get_joint_kind_symbol(circuit.second_device_joint)
dev2 = to_human_readable_value(circuit.second_device_value)
print(f"[{j2}] ┬ {dev1}")
print(f"{dev2}")
case CircuitDeviceScale.THREE: case CircuitDeviceScale.THREE:
dev1 = to_human_readable_value(circuit.first_device_value) self.__illustrate_three_device_circuit(circuit, device_kind)
j2 = self.__get_joint_kind_symbol(circuit.second_device_joint)
dev2 = to_human_readable_value(circuit.second_device_value) def __illustrate_one_device_circuit(
j3 = self.__get_joint_kind_symbol(circuit.third_device_joint) self, circuit: Circuit, device_kind: DeviceKind
dev3 = to_human_readable_value(circuit.third_device_value) ) -> None:
print(f"[{j3}] ┬ [{j2}] ┬ {dev1}") dev1 = self.__to_circult_graph_value(circuit.first_device_value, device_kind)
print(f" │ └ {dev2}") print(f"──[{dev1:^16}]──")
print(f"{dev3}")
def __illustrate_two_device_circuit(
self, circuit: Circuit, device_kind: DeviceKind
) -> None:
dev1 = self.__to_circult_graph_value(circuit.first_device_value, device_kind)
j2 = circuit.second_device_joint
dev2 = self.__to_circult_graph_value(circuit.second_device_value, device_kind)
match j2:
case JointKind.SERIES:
print(f"──[{dev1:^16}]──[{dev2:^16}]──")
case JointKind.PARALLEL:
SEP0: str = " " * (6 + (16 - 10))
print(f" ┌──[{dev1:^16}]──┐ ")
print(f"──┤ {SEP0} ├──")
print(f" └──[{dev2:^16}]──┘ ")
def __illustrate_three_device_circuit(
self, circuit: Circuit, device_kind: DeviceKind
) -> None:
dev1 = self.__to_circult_graph_value(circuit.first_device_value, device_kind)
j2 = circuit.second_device_joint
dev2 = self.__to_circult_graph_value(circuit.second_device_value, device_kind)
j3 = circuit.third_device_joint
dev3 = self.__to_circult_graph_value(circuit.third_device_value, device_kind)
match j2:
case JointKind.SERIES:
match j3:
case JointKind.SERIES:
# All in series
print(f"──[{dev1:^16}]──[{dev2:^16}]──[{dev3:^16}]──")
case JointKind.PARALLEL:
# First series then parallel
SEP0: str = "" * (6 + ((16 - 10) // 2))
SEP1: str = " " * (6 + 2 * (16 - 10))
print(f" ┌──[{dev1:^16}]──[{dev2:^16}]──┐ ")
print(f"──┤ {SEP1} ├──")
print(f" └───{SEP0}[{dev3:^16}]{SEP0}───┘ ")
case JointKind.PARALLEL:
match j3:
case JointKind.SERIES:
# First parallel then series
SEP0: str = " " * (6 + (16 - 10))
print(f" {SEP0} ┌──[{dev1:^16}]──┐ ")
print(f"──[{dev3:^16}]──┤ {SEP0} ├──")
print(f" {SEP0} └──[{dev2:^16}]──┘ ")
case JointKind.PARALLEL:
# All in parallel
print(f" ┌──[{dev1:^16}]──┐ ")
print(f"──┼──[{dev2:^16}]──┼──")
print(f" └──[{dev3:^16}]──┘ ")
# endregion
def main() -> None: def main() -> None:
+105
View File
@@ -1,4 +1,5 @@
import enum import enum
from typing import Optional
class LcrConnException(Exception): class LcrConnException(Exception):
@@ -267,3 +268,107 @@ class Circuit:
return self.__third_device_subckt.device_value return self.__third_device_subckt.device_value
else: else:
raise LcrConnException("No third device") raise LcrConnException("No third device")
class CircuitCalculator:
"""The bunch of functions for handful circuit computation"""
__device_kind: DeviceKind
"""The kind of the device"""
__target_value: float
"""The target value"""
def __init__(self, device_kind: DeviceKind, target_value: float) -> None:
self.__device_kind = device_kind
self.__target_value = target_value
def value(self, circuit: Circuit) -> float:
"""
The value of this circuit.
:param circuit: The circuit for computation.
:return: The value.
"""
return circuit.compute(self.__device_kind)
def difference(self, circuit: Circuit, value: Optional[float] = None) -> float:
"""
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.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:return: The signed difference.
"""
if value is None:
value = self.value(circuit)
return value - self.__target_value
def unsigned_difference(
self,
circuit: Circuit,
value: Optional[float] = None,
difference: Optional[float] = None,
) -> float:
"""
The unsigned difference between the target value and the value of this circuit.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:param difference: The difference of the circuit computed by the `difference` method
for reducing computation steps, or None if you request this method to compute the difference.
:return: The unsigned difference.
"""
if difference is None:
difference = self.difference(circuit, value)
return abs(difference)
def relative_difference(
self,
circuit: Circuit,
value: Optional[float] = None,
difference: Optional[float] = None,
) -> float:
"""
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.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:param difference: The difference of the circuit computed by the `difference` method
for reducing computation steps, or None if you request this method to compute the difference.
:return: The signed relative difference.
"""
if difference is None:
difference = self.difference(circuit, value)
return difference / self.__target_value
def unsigned_relative_difference(
self,
circuit: Circuit,
value: Optional[float] = None,
difference: Optional[float] = None,
relative_difference: Optional[float] = None,
) -> float:
"""
The unsigned relative difference between the target value and the value of this circuit.
:param circuit: The circuit for computation.
:param value: The value of the circuit computed by the `value` method
for reducing computation steps, or None if you request this method to compute the value.
:param difference: The difference of the circuit computed by the `difference` method
for reducing computation steps, or None if you request this method to compute the difference.
:param relative_difference: The relative difference of the circuit computed by the `relative_difference` method
for reducing computation steps, or None if you request this method to compute the relative difference.
:return: The unsigned relative difference.
"""
if relative_difference is None:
relative_difference = self.relative_difference(circuit, value, difference)
return abs(relative_difference)
+269 -38
View File
@@ -1,8 +1,26 @@
from typing import Iterable import enum
from dataclasses import dataclass
from typing import Iterable, Iterator
from pathlib import Path from pathlib import Path
from .common import LcrConnException from .common import LcrConnException
@dataclass(frozen=True)
class DatasetItem:
value: float
"""The actual value of this item."""
str_value: str
"""The string form of this value given from original input for re-saving."""
def __post_init__(self):
value = self.value
if value <= 0:
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")
class Dataset: class Dataset:
""" """
A list holding available standard values for resistor, capacitor or inductor. A list holding available standard values for resistor, capacitor or inductor.
@@ -13,27 +31,34 @@ class Dataset:
This list will only contain 100 and 4.7k. This list will only contain 100 and 4.7k.
""" """
__values: tuple[float, ...] __values: tuple[DatasetItem, ...]
"""A list of available device gauge values""" """A list of available device gauge values"""
def __init__(self, values: tuple[float, ...]): def __init__(self, str_values: Iterable[str]):
# Check redundant parts # Check string form value one by one
valueset = set(values) value_items: list[DatasetItem] = []
if len(valueset) != len(values): value_set: set[float] = set()
raise LcrConnException(f"Duplicate item in standard value list") for str_value in str_values:
if len(valueset) == 0: # Try parsing value
value = from_human_readable_value(str_value)
# Check and update set
if value in value_set:
raise LcrConnException(
f"Duplicate item {str_value} in standard value list"
)
else:
value_set.add(value)
# Add into result
value_items.append(DatasetItem(value, str_value))
# Check empty case
if len(value_items) == 0:
raise LcrConnException(f"Empty standard value list is not allowed") raise LcrConnException(f"Empty standard value list is not allowed")
# Ok, assign it # Ok, assign it
self.__values = values self.__values = tuple(value_items)
@staticmethod @staticmethod
def from_iterable(stringfied_values: Iterable[str]) -> "Dataset": def from_iterable(stringfied_values: Iterable[str]) -> "Dataset":
return Dataset( return Dataset(stringfied_values)
tuple(
from_human_readable_value(stringfied_value)
for stringfied_value in stringfied_values
)
)
@staticmethod @staticmethod
def from_text(text: str) -> "Dataset": def from_text(text: str) -> "Dataset":
@@ -47,14 +72,104 @@ class Dataset:
legal_lines = filter(lambda line: line != "", (line.strip() for line in f)) legal_lines = filter(lambda line: line != "", (line.strip() for line in f))
return Dataset.from_iterable(legal_lines) return Dataset.from_iterable(legal_lines)
@staticmethod
def resistor_preset() -> "Dataset":
return Dataset.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",
)
)
@staticmethod
def capacitor_preset() -> "Dataset":
return Dataset.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",
)
)
@staticmethod
def inductor_preset() -> "Dataset":
return Dataset.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",
)
)
def __save(self) -> Iterator[str]:
return map(lambda i: i.str_value, self.__values)
def save_iterator(self) -> Iterator[str]:
return self.__save()
def save_text(self) -> str:
return "\n".join(self.__save())
def save_file(self, filename: Path) -> None:
with open(filename, "w", encoding="utf-8") as f:
f.write(self.save_text())
@property @property
def values(self) -> tuple[float, ...]: def values(self) -> Iterator[float]:
""" """
Get the available standard values Get the available standard values
:return: A tuple of available standard values :return: A tuple of available standard values
""" """
return self.__values return map(lambda i: i.value, self.__values)
class DatasetCollection: class DatasetCollection:
@@ -78,6 +193,14 @@ class DatasetCollection:
def from_iterable( def from_iterable(
resistor: Iterable[str], capacitor: Iterable[str], inductor: Iterable[str] resistor: Iterable[str], capacitor: Iterable[str], inductor: Iterable[str]
) -> "DatasetCollection": ) -> "DatasetCollection":
"""
Load the standard values for resistor, capacitor and inductor respectively from iterables.
:param resistor: The iterable to load available standard values for resistor
:param capacitor: The iterable to load available standard values for capacitor
:param inductor: The iterable to load available standard values for inductor
:return: The built dataset collection
"""
return DatasetCollection( return DatasetCollection(
Dataset.from_iterable(resistor), Dataset.from_iterable(resistor),
Dataset.from_iterable(capacitor), Dataset.from_iterable(capacitor),
@@ -86,6 +209,14 @@ class DatasetCollection:
@staticmethod @staticmethod
def from_text(resistor: str, capacitor: str, inductor: str) -> "DatasetCollection": def from_text(resistor: str, capacitor: str, inductor: str) -> "DatasetCollection":
"""
Load the standard values for resistor, capacitor and inductor respectively from strings.
:param resistor: The string to load available standard values for resistor
:param capacitor: The string to load available standard values for capacitor
:param inductor: The string to load available standard values for inductor
:return: The built dataset collection
"""
return DatasetCollection( return DatasetCollection(
Dataset.from_text(resistor), Dataset.from_text(resistor),
Dataset.from_text(capacitor), Dataset.from_text(capacitor),
@@ -96,14 +227,66 @@ class DatasetCollection:
def from_file( def from_file(
resistor: Path, capacitor: Path, inductor: Path resistor: Path, capacitor: Path, inductor: Path
) -> "DatasetCollection": ) -> "DatasetCollection":
"""
Load the standard values for resistor, capacitor and inductor respectively from files.
:param resistor: The file to load available standard values for resistor
:param capacitor: The file to load available standard values for capacitor
:param inductor: The file to load available standard values for inductor
:return: The built dataset collection
"""
return DatasetCollection( return DatasetCollection(
Dataset.from_file(resistor), Dataset.from_file(resistor),
Dataset.from_file(capacitor), Dataset.from_file(capacitor),
Dataset.from_file(inductor), Dataset.from_file(inductor),
) )
@staticmethod
def devices_preset() -> "DatasetCollection":
return DatasetCollection(
Dataset.resistor_preset(),
Dataset.capacitor_preset(),
Dataset.inductor_preset(),
)
def save_iterator(self) -> tuple[Iterator[str], Iterator[str], Iterator[str]]:
"""
Get the iterator of available standard values for resistor, capacitor and inductor respectively.
:return: A tuple of iterators of available standard values for resistor, capacitor and inductor respectively.
"""
return (
self.__resistor.save_iterator(),
self.__capacitor.save_iterator(),
self.__inductor.save_iterator(),
)
def save_text(self) -> tuple[str, str, str]:
"""
Get the string form of available standard values for resistor, capacitor and inductor respectively.
:return: A tuple of strings of available standard values for resistor, capacitor and inductor respectively.
"""
return (
self.__resistor.save_text(),
self.__capacitor.save_text(),
self.__inductor.save_text(),
)
def save_file(self, resistor: Path, capacitor: Path, inductor: Path) -> None:
"""
Save the available standard values for resistor, capacitor and inductor respectively to files.
:param resistor: The file to save available standard values for resistor
:param capacitor: The file to save available standard values for capacitor
:param inductor: The file to save available standard values for inductor
"""
self.__resistor.save_file(resistor)
self.__capacitor.save_file(capacitor)
self.__inductor.save_file(inductor)
@property @property
def resistor_values(self) -> Dataset: def resistor_dataset(self) -> Dataset:
""" """
Get the available standard values for resistor Get the available standard values for resistor
@@ -112,7 +295,7 @@ class DatasetCollection:
return self.__resistor return self.__resistor
@property @property
def capacitor_values(self) -> Dataset: def capacitor_dataset(self) -> Dataset:
""" """
Get the available standard values for capacitor Get the available standard values for capacitor
@@ -121,7 +304,7 @@ class DatasetCollection:
return self.__capacitor return self.__capacitor
@property @property
def inductor_values(self) -> Dataset: def inductor_dataset(self) -> Dataset:
""" """
Get the available standard values for inductor Get the available standard values for inductor
@@ -157,6 +340,49 @@ def from_human_readable_value(strl: str) -> float:
return float(strl) return float(strl)
class UnitScale(enum.IntEnum):
"""
The unit scale for human readable value
"""
NANO_LOWER = enum.auto()
NANO = enum.auto()
MICRO = enum.auto()
MILLI = enum.auto()
NONE = enum.auto()
KILO = enum.auto()
MEGA = enum.auto()
GIGA = enum.auto()
GIGA_HIGHER = enum.auto()
def get_human_readable_value_scale(v: float) -> UnitScale:
"""
Get the unit scale of human readable value
:param v: The value
:return: The unit scale
"""
v = abs(v)
if v < 1e-12:
return UnitScale.NANO_LOWER
if v < 1e-9:
return UnitScale.NANO
if v < 1e-6:
return UnitScale.MICRO
if v < 1e-3:
return UnitScale.MILLI
if v < 1e3:
return UnitScale.NONE
if v < 1e6:
return UnitScale.KILO
if v < 1e9:
return UnitScale.MEGA
if v < 1e12:
return UnitScale.GIGA
return UnitScale.GIGA_HIGHER
def to_human_readable_value(v: float) -> str: def to_human_readable_value(v: float) -> str:
""" """
Convert float value to human readable value Convert float value to human readable value
@@ -164,21 +390,26 @@ def to_human_readable_value(v: float) -> str:
:param value: The float value :param value: The float value
:return: The human readable value :return: The human readable value
""" """
if v / 1e-12 < 1e3: scale = get_human_readable_value_scale(v)
return "{:e} n".format(v / 1e-12) match scale:
if v / 1e-9 < 1e3: case UnitScale.NANO_LOWER:
return "{:.4f} p".format(v / 1e-9) return "{:+.4e} n".format(v / 1e-12)
if v / 1e-6 < 1e3: case UnitScale.NANO:
return "{:.4f} u".format(v / 1e-6) return "{:+.4f} p".format(v / 1e-9)
if v / 1e-3 < 1e3: case UnitScale.MICRO:
return "{:.4f} m".format(v / 1e-3) return "{:+.4f} u".format(v / 1e-6)
if v < 1e3: case UnitScale.MILLI:
return "{:.4f}".format(v) return "{:+.4f} m".format(v / 1e-3)
if v / 1e3 < 1e3: case UnitScale.NONE:
return "{:.4f} k".format(v / 1e3) # YYC MARK:
if v / 1e6 < 1e3: # The space of this format string is by design
return "{:.4f} M".format(v / 1e6) # for keeping the same style with other format strings.
if v / 1e9 < 1e3: return "{:+.4f} ".format(v)
return "{:.4f} G".format(v / 1e9) case UnitScale.KILO:
return "{:+.4f} k".format(v / 1e3)
return "{:e}".format(v) case UnitScale.MEGA:
return "{:+.4f} M".format(v / 1e6)
case UnitScale.GIGA:
return "{:+.4f} G".format(v / 1e9)
case UnitScale.GIGA_HIGHER:
return "{:+.4e} G".format(v / 1e9)
+78 -27
View File
@@ -1,8 +1,8 @@
import enum import enum
from functools import cached_property from functools import cached_property
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator from typing import Iterable, Iterator
from .common import DeviceKind, Circuit from .common import DeviceKind, Circuit, CircuitCalculator
class ResponsePriority(enum.Enum): class ResponsePriority(enum.Enum):
@@ -16,7 +16,11 @@ class ResponsePriority(enum.Enum):
"""More accuracy is the first priority.""" """More accuracy is the first priority."""
@dataclass MAX_RESPONSE_CNT: int = 50
"""The maximum count for the response item count passed in request."""
@dataclass(frozen=True)
class Request: class Request:
""" """
All request infomation for the resolver. All request infomation for the resolver.
@@ -33,6 +37,21 @@ class Request:
count_limit: int count_limit: int
"""The limited count of results.""" """The limited count of results."""
def __post_init__(self):
target_value = self.target_value
if target_value <= 0:
raise ValueError(
f"Invalid value {target_value} for target value in request."
)
tolerance = self.tolerance
if tolerance < 0:
raise ValueError(f"Invalid value {tolerance} for tolerance in request.")
count_limit = self.count_limit
if count_limit <= 0 or count_limit > MAX_RESPONSE_CNT:
raise ValueError(
f"Too large or too less value {count_limit} for response count limit in request."
)
class ResponseItem: class ResponseItem:
""" """
@@ -41,17 +60,12 @@ class ResponseItem:
__circuit: Circuit __circuit: Circuit
"""The circuit of the response item.""" """The circuit of the response item."""
__device_kind: DeviceKind __ccalc: CircuitCalculator
"""The kind of device of this circuit.""" """The trait for computing circuit values."""
__target_value: float
"""The target value of this circuit."""
def __init__( def __init__(self, circuit: Circuit, ccalc: CircuitCalculator) -> None:
self, circuit: Circuit, device_kind: DeviceKind, target_value: float
) -> None:
self.__circuit = circuit self.__circuit = circuit
self.__device_kind = device_kind self.__ccalc = ccalc
self.__target_value = target_value
@property @property
def circuit(self) -> Circuit: def circuit(self) -> Circuit:
@@ -62,7 +76,7 @@ class ResponseItem:
""" """
return self.__circuit return self.__circuit
@cached_property @property
def device_count(self) -> int: def device_count(self) -> int:
""" """
The device count of this circuit. The device count of this circuit.
@@ -78,25 +92,55 @@ class ResponseItem:
:return: The value. :return: The value.
""" """
return self.__circuit.compute(self.__device_kind) return self.__ccalc.value(self.__circuit)
@cached_property @cached_property
def difference(self) -> float: def difference(self) -> float:
""" """
The absolute difference between the target value and the value of this circuit. The signed difference between the target value and the value of this circuit.
:return: The absolute difference. 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.
:return: The signed difference.
""" """
return abs(self.__target_value - self.value) return self.__ccalc.difference(self.__circuit, value=self.value)
@cached_property
def unsigned_difference(self) -> float:
"""
The unsigned difference between the target value and the value of this circuit.
:return: The unsigned difference.
"""
return self.__ccalc.unsigned_difference(
self.__circuit, difference=self.difference
)
@cached_property @cached_property
def relative_difference(self) -> float: def relative_difference(self) -> float:
""" """
The relative difference between the target value and the value of this circuit. The signed relative difference between the target value and the value of this circuit.
:return: The relative difference. 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.
:return: The signed relative difference.
""" """
return self.difference / self.__target_value return self.__ccalc.relative_difference(
self.__circuit, difference=self.difference
)
@cached_property
def unsigned_relative_difference(self) -> float:
"""
The unsigned relative difference between the target value and the value of this circuit.
:return: The unsigned relative difference.
"""
return self.__ccalc.unsigned_relative_difference(
self.__circuit, relative_difference=self.relative_difference
)
class Response: class Response:
@@ -108,25 +152,32 @@ class Response:
For getting the count of response items, please use the ``len`` function. For getting the count of response items, please use the ``len`` function.
""" """
__device_kind: DeviceKind
"""The kind of device of this response."""
__sorted_items: list[ResponseItem] __sorted_items: list[ResponseItem]
"""The sorted items by priority and difference.""" """The sorted items by priority and difference."""
def __init__(self, request: Request, candidates: Iterator[Circuit]) -> None: def __init__(self, request: Request, candidates: Iterable[Circuit]) -> None:
self.__sorted_items = list( ccalc = CircuitCalculator(request.device_kind, request.target_value)
ResponseItem(item, request.device_kind, request.target_value) self.__device_kind = request.device_kind
for item in candidates self.__sorted_items = list(ResponseItem(item, ccalc) for item in candidates)
)
# Sort by different strategy # Sort by different strategy
match request.response_priority: match request.response_priority:
case ResponsePriority.LESS_DEVICES: case ResponsePriority.LESS_DEVICES:
self.__sorted_items.sort(key=lambda x: (x.device_count, x.difference)) self.__sorted_items.sort(
key=lambda x: (x.device_count, x.unsigned_difference)
)
case ResponsePriority.MORE_ACCURACY: case ResponsePriority.MORE_ACCURACY:
self.__sorted_items.sort(key=lambda x: x.difference) self.__sorted_items.sort(key=lambda x: x.unsigned_difference)
# Cut item by limit # Cut item by limit
self.__sorted_items = self.__sorted_items[: request.count_limit] self.__sorted_items = self.__sorted_items[: request.count_limit]
@property
def device_kind(self) -> DeviceKind:
return self.__device_kind
def __getitem__(self, index: int) -> ResponseItem: def __getitem__(self, index: int) -> ResponseItem:
return self.__sorted_items[index] return self.__sorted_items[index]
@@ -1,9 +1,9 @@
from .common import Resolver from .common import Resolver
from .lut import LutResolver from .lut import LutResolver
from .astar import AStarResolver from .bfs import BfsResolver
__all__ = [ __all__ = [
'Resolver', 'Resolver',
'LutResolver', 'LutResolver',
'AStarResolver' 'BfsResolver'
] ]
@@ -1,17 +0,0 @@
from typing import Iterator
from .common import Resolver
from ..dataset import DatasetCollection
from ..common import Circuit
from ..query import Request, Response
class AStarResolver(Resolver):
"""
A resolver that uses A* algorithm to find the best matching circuit.
"""
def __init__(self, dataset: DatasetCollection):
pass
def resolve(self, request: Request) -> Iterator[Circuit]:
pass
+259
View File
@@ -0,0 +1,259 @@
import heapq
from itertools import chain, combinations_with_replacement, product
from typing import Iterable, Iterator
from functools import cached_property
from .common import Resolver
from ..dataset import DatasetCollection, Dataset
from ..common import Circuit, DeviceKind, JointKind, CircuitCalculator
from ..query import Request, Response
class BfsItem:
"""
The entry used in BFS iteration storing circuit and value.
"""
__circuit: Circuit
"""The circuit represented by this item."""
__ccalc: CircuitCalculator
"""The trait for computing circuit values."""
def __init__(self, circuit: Circuit, ccalc: CircuitCalculator):
self.__circuit = circuit
self.__ccalc = ccalc
@property
def circuit(self) -> Circuit:
return self.__circuit
@cached_property
def value(self) -> float:
"""
The computed value of the circuit.
:return: The computed value.
"""
return self.__ccalc.value(self.__circuit)
@cached_property
def unsigned_difference(self) -> float:
"""
The unsigned difference between the target value and the value of this circuit.
:return: The unsigned difference.
"""
return self.__ccalc.unsigned_difference(self.__circuit, value=self.value)
class ResultBucket(Iterable[BfsItem]):
"""
A bounded bucket that keeps up to `N` LutItem entries with the smallest floats.
When the bucket is full, inserting a new item only succeeds if its float
is less than the current maximum; the maximum is then evicted.
"""
class ResultBucketItem:
"""
An item stored in a :class:`ResultBucket`.
"""
__score: float
"""The score associated with this item."""
__item: BfsItem
"""The underlying LutItem."""
__seq: int
"""
Monotonic counter used as a tiebreaker when scores are equal,
ensuring that heapq never compares :class:`LutItem` directly.
"""
def __init__(self, score: float, item: BfsItem, seq: int):
self.__score = score
self.__item = item
self.__seq = seq
@property
def score(self) -> float:
"""The score associated with this item."""
return self.__score
@property
def item(self) -> BfsItem:
"""The underlying LutItem."""
return self.__item
def __lt__(self, other: "ResultBucket.ResultBucketItem") -> bool:
# heapq is a min-heap: it always pops the smallest element.
# We invert the comparison so that an item with a larger score
# is considered "smaller", effectively turning the min-heap
# into a max-heap (largest-score item at the top).
if self.__score != other.__score:
return self.__score > other.__score
# Counter tiebreaker: when scores are equal the later-inserted
# item (higher seq) is considered "smaller" and gets evicted first.
return self.__seq > other.__seq
__n: int
"""Maximum number of items the bucket can hold."""
__heap: list[ResultBucketItem]
"""
Min-heap of :class:`ResultBucketItem`. The heap invariant is inverted
via :meth:`ResultBucketItem.__lt__` so the entry with the largest score
sits at index 0.
"""
__counter: int
"""
Monotonic counter fed to each :class:`ResultBucketItem` as a tiebreaker,
preventing heapq from comparing :class:`LutItem` on score collisions.
"""
def __init__(self, n: int):
self.__n = n
self.__heap = []
self.__counter = 0
def __len__(self) -> int:
return len(self.__heap)
def __iter__(self) -> Iterator[BfsItem]:
for entry in self.__heap:
yield entry.item
def insert(self, item: BfsItem, score: float) -> bool:
"""
Insert a :class:`LutItem` with the given score.
If the bucket is not yet full the item is always inserted.
Otherwise the item is only inserted when *score* is smaller
than the largest score currently in the bucket; the entry
with the largest score is then evicted.
:param item: The LutItem to insert.
:param score: The score associated with the item.
:return: ``True`` if the item was inserted, ``False`` otherwise.
"""
entry = ResultBucket.ResultBucketItem(score, item, self.__counter)
if len(self.__heap) < self.__n:
heapq.heappush(self.__heap, entry)
self.__counter += 1
return True
if score >= self.__heap[0].score:
return False
heapq.heapreplace(self.__heap, entry)
self.__counter += 1
return True
class BfsResolver(Resolver):
__datasets: DatasetCollection
def __init__(self, datasets: DatasetCollection):
self.__datasets = datasets
# 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 3 function are taking this job.
@staticmethod
def iter_one_device_circuit(dataset: Dataset) -> Iterator[Circuit]:
"""
Iterate all possible circuits with one device without repeating equivalent topology.
:param dataset: The dataset to iterate.
:return: The iterator of circuits with one device.
"""
# Every single device is unique so we directly output them.
# This feature is insured by dataset itself.
return (Circuit.from_one_device(v1) for v1 in dataset.values)
@staticmethod
def iter_two_devices_circuit(dataset: Dataset) -> Iterator[Circuit]:
"""
Iterate all possible circuits with two devices without repeating equivalent topology.
:param dataset: The dataset to iterate.
:return: The iterator of circuits with two devices.
"""
# The two devices in this circuit is always swapable,
# so we iterate them without repeating.
return (
Circuit.from_two_devices(v1, v2, j2)
for (v1, v2), j2 in product(
combinations_with_replacement(dataset.values, 2),
tuple(JointKind),
)
)
@staticmethod
def iter_three_devices_circuit(dataset: Dataset) -> Iterator[Circuit]:
"""
Iterate all possible circuits with three devices without repeating equivalent topology.
:param dataset: The dataset to iterate.
:return: The iterator of circuits with three devices.
"""
# For generating three devices circuit,
# it should be consisted by 2 parts.
return chain(
# First, the whole circuit has only one joint type.
# In this case, 3 devices are swapable and we should iterate them without repeating
(
Circuit.from_three_devices(v1, v2, j, v3, j)
for (v1, v2, v3), j in product(
combinations_with_replacement(dataset.values, 3),
tuple(JointKind),
)
),
# Second, if the joint type is different, then the first 2 devices are swapable.
# So we need iterate them without repeating.
(
Circuit.from_three_devices(v1, v2, j, v3, j.flip())
for (v1, v2), v3, j in product(
combinations_with_replacement(dataset.values, 2),
dataset.values,
tuple(JointKind),
)
),
)
@staticmethod
def __bfs_iteration(
dataset: Dataset, ccalc: CircuitCalculator
) -> Iterator[BfsItem]:
return (
BfsItem(circuit, ccalc)
for circuit in chain(
BfsResolver.iter_one_device_circuit(dataset),
BfsResolver.iter_two_devices_circuit(dataset),
BfsResolver.iter_three_devices_circuit(dataset),
)
)
def resolve(self, request: Request) -> Response:
# Pick dataset from collection
dataset: Dataset
match request.device_kind:
case DeviceKind.RESISTOR:
dataset = self.__datasets.resistor_dataset
case DeviceKind.CAPACITOR:
dataset = self.__datasets.capacitor_dataset
case DeviceKind.INDUCTOR:
dataset = self.__datasets.inductor_dataset
# Iterate circuit item one by one
bucket = ResultBucket(request.count_limit)
ccalc = CircuitCalculator(request.device_kind, request.target_value)
for item in BfsResolver.__bfs_iteration(dataset, ccalc):
# 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
return Response(request, map(lambda item: item.circuit, bucket))
+69 -145
View File
@@ -1,10 +1,9 @@
import heapq import bisect
from itertools import chain, product from itertools import chain
from typing import Iterable, Iterator
from functools import cached_property
from .common import Resolver from .common import Resolver
from .bfs import BfsResolver
from ..dataset import DatasetCollection, Dataset from ..dataset import DatasetCollection, Dataset
from ..common import Circuit, DeviceKind, JointKind from ..common import Circuit, DeviceKind, CircuitCalculator
from ..query import Request, Response from ..query import Request, Response
@@ -15,125 +14,20 @@ class LutItem:
__circuit: Circuit __circuit: Circuit
"""The circuit represented by this item.""" """The circuit represented by this item."""
__device_kind: DeviceKind __value: float
"""The device kind applied for this circuit.""" """The value of this circuit."""
def __init__(self, circuit: Circuit, device_kind: DeviceKind): def __init__(self, circuit: Circuit, device_kind: DeviceKind):
self.__circuit = circuit self.__circuit = circuit
self.__device_kind = device_kind self.__value = self.__circuit.compute(device_kind)
@property @property
def circuit(self) -> Circuit: def circuit(self) -> Circuit:
return self.__circuit return self.__circuit
@cached_property @property
def value(self) -> float: def value(self) -> float:
""" return self.__value
The computed value of the circuit.
:return: The computed value.
"""
return self.__circuit.compute(self.__device_kind)
class ResultBucket(Iterable[LutItem]):
"""
A bounded bucket that keeps up to `N` LutItem entries with the smallest floats.
When the bucket is full, inserting a new item only succeeds if its float
is less than the current maximum; the maximum is then evicted.
"""
class ResultBucketItem:
"""
An item stored in a :class:`ResultBucket`.
"""
__score: float
"""The score associated with this item."""
__item: LutItem
"""The underlying LutItem."""
__seq: int
"""
Monotonic counter used as a tiebreaker when scores are equal,
ensuring that heapq never compares :class:`LutItem` directly.
"""
def __init__(self, score: float, item: LutItem, seq: int):
self.__score = score
self.__item = item
self.__seq = seq
@property
def score(self) -> float:
"""The score associated with this item."""
return self.__score
@property
def item(self) -> LutItem:
"""The underlying LutItem."""
return self.__item
def __lt__(self, other: "ResultBucket.ResultBucketItem") -> bool:
# heapq is a min-heap: it always pops the smallest element.
# We invert the comparison so that an item with a larger score
# is considered "smaller", effectively turning the min-heap
# into a max-heap (largest-score item at the top).
if self.__score != other.__score:
return self.__score > other.__score
# Counter tiebreaker: when scores are equal the later-inserted
# item (higher seq) is considered "smaller" and gets evicted first.
return self.__seq > other.__seq
__n: int
"""Maximum number of items the bucket can hold."""
__heap: list[ResultBucketItem]
"""
Min-heap of :class:`ResultBucketItem`. The heap invariant is inverted
via :meth:`ResultBucketItem.__lt__` so the entry with the largest score
sits at index 0.
"""
__counter: int
"""
Monotonic counter fed to each :class:`ResultBucketItem` as a tiebreaker,
preventing heapq from comparing :class:`LutItem` on score collisions.
"""
def __init__(self, n: int):
self.__n = n
self.__heap = []
self.__counter = 0
def __len__(self) -> int:
return len(self.__heap)
def __iter__(self) -> Iterator[LutItem]:
for entry in self.__heap:
yield entry.item
def insert(self, item: LutItem, score: float) -> bool:
"""
Insert a :class:`LutItem` with the given score.
If the bucket is not yet full the item is always inserted.
Otherwise the item is only inserted when *score* is smaller
than the largest score currently in the bucket; the entry
with the largest score is then evicted.
:param item: The LutItem to insert.
:param score: The score associated with the item.
:return: ``True`` if the item was inserted, ``False`` otherwise.
"""
entry = ResultBucket.ResultBucketItem(score, item, self.__counter)
if len(self.__heap) < self.__n:
heapq.heappush(self.__heap, entry)
self.__counter += 1
return True
if score >= self.__heap[0].score:
return False
heapq.heapreplace(self.__heap, entry)
self.__counter += 1
return True
class LutResolver(Resolver): class LutResolver(Resolver):
@@ -150,38 +44,29 @@ class LutResolver(Resolver):
def __init__(self, datasets: DatasetCollection): def __init__(self, datasets: DatasetCollection):
self.__resistor_lut = LutResolver.__build_lut( self.__resistor_lut = LutResolver.__build_lut(
datasets.resistor_values, DeviceKind.RESISTOR datasets.resistor_dataset, DeviceKind.RESISTOR
) )
self.__capacitor_lut = LutResolver.__build_lut( self.__capacitor_lut = LutResolver.__build_lut(
datasets.capacitor_values, DeviceKind.CAPACITOR datasets.capacitor_dataset, DeviceKind.CAPACITOR
) )
self.__inductor_lut = LutResolver.__build_lut( self.__inductor_lut = LutResolver.__build_lut(
datasets.inductor_values, DeviceKind.INDUCTOR datasets.inductor_dataset, DeviceKind.INDUCTOR
) )
@staticmethod @staticmethod
def __build_lut(dataset: Dataset, device_kind: DeviceKind) -> list[LutItem]: def __build_lut(dataset: Dataset, device_kind: DeviceKind) -> list[LutItem]:
values = dataset.values lut = [
joints = tuple(JointKind)
return [
LutItem(circuit, device_kind) LutItem(circuit, device_kind)
for circuit in chain( for circuit in chain(
(Circuit.from_one_device(v1) for v1 in values), BfsResolver.iter_one_device_circuit(dataset),
( BfsResolver.iter_two_devices_circuit(dataset),
Circuit.from_two_devices(v1, v2, j2) BfsResolver.iter_three_devices_circuit(dataset),
for v1, v2, j2 in product(values, values, joints)
),
(
Circuit.from_three_devices(v1, v2, j2, v3, j3)
for v1, v2, j2, v3, j3 in product(
values, values, joints, values, joints
)
),
) )
] ]
lut.sort(key=lambda item: item.value)
return lut
def resolve(self, request: Request) -> Response: def resolve(self, request: Request) -> Response:
# Fetch LUT by device kind
lut: list[LutItem] lut: list[LutItem]
match request.device_kind: match request.device_kind:
case DeviceKind.RESISTOR: case DeviceKind.RESISTOR:
@@ -191,16 +76,55 @@ class LutResolver(Resolver):
case DeviceKind.INDUCTOR: case DeviceKind.INDUCTOR:
lut = self.__inductor_lut lut = self.__inductor_lut
# Check LUT item one by one target = request.target_value
bucket = ResultBucket(min(request.count_limit, 100)) count_limit = request.count_limit
for item in lut: bucket: list[Circuit] = []
# compute absolute difference
difference = abs(request.target_value - item.value)
# If it is out of tolerance, skip it directly.
if difference > request.tolerance:
continue
# put it into bucket
bucket.insert(item, difference)
# Return result # Locate the insertion point of target in the sorted LUT.
return Response(request, map(lambda item: item.circuit, bucket)) # left/right start at the two nearest neighbours and expand outward.
idx = bisect.bisect_left(lut, target, key=lambda item: item.value)
left = idx - 1
right = idx
# 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
# difference order, so the first N items within tolerance are exactly
# the N best matches.
ccalc = CircuitCalculator(request.device_kind, target)
while left >= 0 or right < len(lut):
if len(bucket) >= count_limit:
break
if left < 0:
go_left = False
elif right >= len(lut):
go_left = True
else:
left_instance = lut[left]
left_diff = ccalc.unsigned_difference(
left_instance.circuit, value=left_instance.value
)
right_instance = lut[right]
right_diff = ccalc.unsigned_difference(
right_instance.circuit, value=right_instance.value
)
go_left = left_diff <= right_diff
if go_left:
item = lut[left]
left -= 1
else:
item = lut[right]
right += 1
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.
if diff > request.tolerance:
break
bucket.append(item.circuit)
return Response(request, bucket)