Compare commits
30
Commits
a27c5505df
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782bd86407 | ||
|
|
5a41c6266c | ||
|
|
1514d25da1 | ||
|
|
3dd8275787 | ||
|
|
5bfdfb71c2 | ||
|
|
bbd47006d4 | ||
|
|
4af8dd97a2 | ||
|
|
a62ed05d50 | ||
|
|
24e4c595bb | ||
|
|
1d9322f300 | ||
|
|
9d9c0473e8 | ||
|
|
ccd96e6801 | ||
|
|
cca66b0cac | ||
|
|
aa6c4f72bd | ||
|
|
7665de0889 | ||
|
|
e587d3e5ff | ||
|
|
73e539933a | ||
|
|
215146bf59 | ||
|
|
5ad3e11836 | ||
|
|
5b3a2c030d | ||
|
|
3bec73b718 | ||
|
|
21a625b054 | ||
|
|
826dda9841 | ||
|
|
7fa0f56495 | ||
|
|
625628cc1a | ||
|
|
181c483923 | ||
|
|
d42885f1ab | ||
|
|
96fa6263a8 | ||
|
|
cfa02e7d85 | ||
|
|
a26116e5e9 |
@@ -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.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
Generated
+284
@@ -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",
|
||||||
|
]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "3"
|
||||||
|
members = ["lcrconn", "lcrconn-cli"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,541 @@
|
|||||||
|
use crate::cli::{AppConfig, AppResolver};
|
||||||
|
use anyhow::Result;
|
||||||
|
use lcrconn::{
|
||||||
|
BfsResolver, DeviceKind, LutResolver, Request, Resolver, Response, ResponsePriority,
|
||||||
|
common::{Circuit, CircuitDeviceScale, JointKind, validate_device_value, validate_floating_point},
|
||||||
|
spec::{SpecCatalog, from_human_readable_value, to_human_readable_value},
|
||||||
|
query::MAX_RESPONSE_CNT,
|
||||||
|
};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use strum_macros::EnumString;
|
||||||
|
|
||||||
|
// region: App Utility Enums
|
||||||
|
|
||||||
|
/// The command for the main menu.
|
||||||
|
#[derive(Debug, Clone, Copy, EnumString)]
|
||||||
|
enum MainCmd {
|
||||||
|
#[strum(serialize = "query")]
|
||||||
|
Query,
|
||||||
|
#[strum(serialize = "help")]
|
||||||
|
Help,
|
||||||
|
#[strum(serialize = "exit")]
|
||||||
|
Exit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device choice for query.
|
||||||
|
#[derive(Debug, Clone, Copy, EnumString)]
|
||||||
|
enum QueryDeviceChoice {
|
||||||
|
#[strum(serialize = "r")]
|
||||||
|
Resistor,
|
||||||
|
#[strum(serialize = "c")]
|
||||||
|
Capacitor,
|
||||||
|
#[strum(serialize = "l")]
|
||||||
|
Inductor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QueryDeviceChoice {
|
||||||
|
fn to_device_kind(self) -> DeviceKind {
|
||||||
|
match self {
|
||||||
|
Self::Resistor => DeviceKind::Resistor,
|
||||||
|
Self::Capacitor => DeviceKind::Capacitor,
|
||||||
|
Self::Inductor => DeviceKind::Inductor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sort priority for query results.
|
||||||
|
#[derive(Debug, Clone, Copy, EnumString)]
|
||||||
|
enum QuerySortPriority {
|
||||||
|
#[strum(serialize = "l")]
|
||||||
|
LessDevices,
|
||||||
|
#[strum(serialize = "a")]
|
||||||
|
MoreAccuracy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuerySortPriority {
|
||||||
|
fn to_response_priority(self) -> ResponsePriority {
|
||||||
|
match self {
|
||||||
|
Self::LessDevices => ResponsePriority::LessDevices,
|
||||||
|
Self::MoreAccuracy => ResponsePriority::MoreAccuracy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The command for the page viewer.
|
||||||
|
#[derive(Debug, Clone, Copy, EnumString)]
|
||||||
|
enum PageViewerCmd {
|
||||||
|
#[strum(serialize = "f")]
|
||||||
|
PreviousPage,
|
||||||
|
#[strum(serialize = "b")]
|
||||||
|
NextPage,
|
||||||
|
#[strum(serialize = "q")]
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: App Utility Functions
|
||||||
|
|
||||||
|
/// Read a single line from stdin, trimmed of surrounding whitespace.
|
||||||
|
fn read_line() -> Result<String> {
|
||||||
|
let mut line = String::new();
|
||||||
|
std::io::stdin().read_line(&mut line)?;
|
||||||
|
Ok(line.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the unit string for a device kind.
|
||||||
|
fn get_device_unit(device_kind: DeviceKind) -> &'static str {
|
||||||
|
match device_kind {
|
||||||
|
// YYC MARK: This is ohm char.
|
||||||
|
DeviceKind::Resistor => "\u{2126}",
|
||||||
|
DeviceKind::Capacitor => "F",
|
||||||
|
DeviceKind::Inductor => "H",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
/// The app.
|
||||||
|
pub struct App {
|
||||||
|
/// The resolver for the app.
|
||||||
|
resolver: Box<dyn Resolver>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Create a new app with the given configuration.
|
||||||
|
pub fn new(config: AppConfig) -> Result<Self> {
|
||||||
|
let sepcs = SpecCatalog::from_file(
|
||||||
|
config.get_resistor_spec(),
|
||||||
|
config.get_capacitor_specs(),
|
||||||
|
config.get_inductor_specs(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let resolver: Box<dyn Resolver> = match config.get_resolver() {
|
||||||
|
AppResolver::Lut => Box::new(LutResolver::new(&sepcs)?),
|
||||||
|
AppResolver::Bfs => Box::new(BfsResolver::new(sepcs)),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { resolver })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the app.
|
||||||
|
pub fn run(&self) -> Result<()> {
|
||||||
|
println!("LCR Connector");
|
||||||
|
println!(r#"Type "help" for more info. Type "exit" to quit."#);
|
||||||
|
self.op_main()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// region: Subcommand Processors
|
||||||
|
|
||||||
|
fn op_main(&self) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
match self.accept_command::<MainCmd>()? {
|
||||||
|
MainCmd::Query => self.op_query()?,
|
||||||
|
MainCmd::Help => {
|
||||||
|
println!("LCR Connector Help:");
|
||||||
|
println!();
|
||||||
|
println!("query: do a query.");
|
||||||
|
println!("help: show all command.");
|
||||||
|
println!("exit: exit this app.");
|
||||||
|
}
|
||||||
|
MainCmd::Exit => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn op_query(&self) -> Result<()> {
|
||||||
|
// collecting request infos
|
||||||
|
println!("What are you connecting?");
|
||||||
|
println!("r: resistor");
|
||||||
|
println!("l: inductor");
|
||||||
|
println!("c: capacitor");
|
||||||
|
let device_kind = self.accept_command::<QueryDeviceChoice>()?.to_device_kind();
|
||||||
|
|
||||||
|
println!("Your target value?");
|
||||||
|
println!(r#"Example: "2.1k", "0.75m", "3.2M" and etc."#);
|
||||||
|
let target_value = self.accept_device_value()?;
|
||||||
|
|
||||||
|
println!("Your tolerance?");
|
||||||
|
println!(r#"It can be absolute value like "2.1k"."#);
|
||||||
|
println!(r#"Or relative value to your target value like "19.5%"."#);
|
||||||
|
let tolerance = self.accept_device_value_tolerance(target_value)?;
|
||||||
|
|
||||||
|
println!("How to sort result?");
|
||||||
|
println!("a: more accuracy");
|
||||||
|
println!("l: less component");
|
||||||
|
let response_priority = self
|
||||||
|
.accept_command::<QuerySortPriority>()?
|
||||||
|
.to_response_priority();
|
||||||
|
|
||||||
|
println!("How may result are you expected?");
|
||||||
|
let count_limit = self.accept_count_value()?;
|
||||||
|
|
||||||
|
// build request and ask resolver
|
||||||
|
let request = Request::new(
|
||||||
|
device_kind,
|
||||||
|
target_value,
|
||||||
|
tolerance,
|
||||||
|
response_priority,
|
||||||
|
count_limit,
|
||||||
|
)?;
|
||||||
|
let response = self.resolver.resolve(&request)?;
|
||||||
|
|
||||||
|
// use page viewer to show result
|
||||||
|
self.op_page_viewer(&response)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn op_page_viewer(&self, response: &Response) -> Result<()> {
|
||||||
|
let cnt = response.len();
|
||||||
|
if cnt == 0 {
|
||||||
|
println!("Sorry, no result!");
|
||||||
|
println!("Please consider adjusting your requirements and try again.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE: usize = 10;
|
||||||
|
let all_page = cnt / ITEMS_PER_PAGE;
|
||||||
|
let mut current_page = 0usize;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// print list
|
||||||
|
for i in 0..ITEMS_PER_PAGE - 1 {
|
||||||
|
// build index and check it
|
||||||
|
let index = current_page * (ITEMS_PER_PAGE - 1) + i;
|
||||||
|
if index >= cnt {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// and print it
|
||||||
|
self.illustrate_response(response, index)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// print page footer
|
||||||
|
println!();
|
||||||
|
println!("Page {} of {}.", current_page + 1, all_page + 1);
|
||||||
|
println!("f: previous page. b: next page. q: quit this viewer.");
|
||||||
|
// check command
|
||||||
|
match self.accept_command::<PageViewerCmd>()? {
|
||||||
|
PageViewerCmd::PreviousPage => current_page = current_page.saturating_sub(1),
|
||||||
|
PageViewerCmd::NextPage => current_page = all_page.min(current_page + 1),
|
||||||
|
PageViewerCmd::Quit => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Command Utilities
|
||||||
|
|
||||||
|
/// Accept a command from the user.
|
||||||
|
///
|
||||||
|
/// Loops until a valid command is entered.
|
||||||
|
fn accept_command<T>(&self) -> Result<T>
|
||||||
|
where
|
||||||
|
T: FromStr,
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match words.parse::<T>() {
|
||||||
|
Ok(cmd) => return Ok(cmd),
|
||||||
|
Err(_) => println!("Unknown command, please try again."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept a count value from the user.
|
||||||
|
fn accept_count_value(&self) -> Result<usize> {
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match words.parse::<usize>() {
|
||||||
|
Ok(value) => {
|
||||||
|
if value > MAX_RESPONSE_CNT || value == 0 {
|
||||||
|
println!("Wrong value, please try again.");
|
||||||
|
} else {
|
||||||
|
return Ok(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("Wrong value, please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept a device value from the user.
|
||||||
|
fn accept_device_value(&self) -> Result<f64> {
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = self.parse_human_readable_value(&words);
|
||||||
|
match value {
|
||||||
|
Some(v) => return Ok(v),
|
||||||
|
None => println!("Wrong value, please try again."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accept a tolerance value from the user.
|
||||||
|
///
|
||||||
|
/// The tolerance can be an absolute value (like "2.1k") or a percentage
|
||||||
|
/// relative to the target value (like "19.5%").
|
||||||
|
fn accept_device_value_tolerance(&self, target_value: f64) -> Result<f64> {
|
||||||
|
loop {
|
||||||
|
self.show_prompt_arrow()?;
|
||||||
|
let words = read_line()?;
|
||||||
|
if words.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let value: Option<f64> = if let Some(pct_str) = words.strip_suffix('%') {
|
||||||
|
let value = self.parse_plain_float(pct_str, |x| *x >= 0.0 && *x <= 100.0);
|
||||||
|
value
|
||||||
|
.map(|v| v / 100.0 * target_value)
|
||||||
|
.map(|v| validate_device_value(v))
|
||||||
|
.transpose()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
} else {
|
||||||
|
self.parse_human_readable_value(&words)
|
||||||
|
};
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Some(v) => return Ok(v),
|
||||||
|
None => println!("Wrong value, please try again."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_prompt_arrow(&self) -> Result<()> {
|
||||||
|
print!("> ");
|
||||||
|
std::io::stdout().flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a plain float value.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `user_value` - The value to parse.
|
||||||
|
/// * `checker` - A function that checks if the input is valid.
|
||||||
|
/// It takes a float as input and returns a bool. True means the input is valid,
|
||||||
|
/// otherwise False.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The parsed value if it is valid, otherwise `None`.
|
||||||
|
fn parse_plain_float(&self, user_value: &str, checker: impl Fn(&f64) -> bool) -> Option<f64> {
|
||||||
|
// try parsing it first then check it by checker
|
||||||
|
let value = match user_value.parse::<f64>() {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
let value = validate_floating_point(value).ok()?;
|
||||||
|
if checker(&value) { Some(value) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a human-readable device value.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `user_value` - The value to parse.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The parsed value if it is valid and positive, otherwise `None`.
|
||||||
|
fn parse_human_readable_value(&self, user_value: &str) -> Option<f64> {
|
||||||
|
// parse it
|
||||||
|
let value = from_human_readable_value(user_value).ok()?;
|
||||||
|
// then check its range
|
||||||
|
if value > 0.0 { Some(value) } else { None }
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region: Response Display Utilities
|
||||||
|
|
||||||
|
/// Format a device value for display in the circuit graph.
|
||||||
|
fn to_circuit_graph_value(&self, value: f64, device_kind: DeviceKind) -> String {
|
||||||
|
// Remove sign and append device unit
|
||||||
|
let hr = to_human_readable_value(value);
|
||||||
|
let without_sign = &hr[1..];
|
||||||
|
format!("{}{}", without_sign, get_device_unit(device_kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a device value for the plan header.
|
||||||
|
fn to_plan_head_value(&self, value: f64, device_kind: DeviceKind) -> String {
|
||||||
|
// Remove sign and append device unit
|
||||||
|
let hr = to_human_readable_value(value);
|
||||||
|
let without_sign = &hr[1..];
|
||||||
|
format!("{}{}", without_sign, get_device_unit(device_kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a difference value for the plan header.
|
||||||
|
fn to_plan_head_diff(&self, value: f64, device_kind: DeviceKind) -> String {
|
||||||
|
// Keep the sign and append device unit
|
||||||
|
format!(
|
||||||
|
"{}{}",
|
||||||
|
to_human_readable_value(value),
|
||||||
|
get_device_unit(device_kind)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a relative difference as percentage.
|
||||||
|
fn to_plan_head_diff_pct(&self, value: f64) -> String {
|
||||||
|
// Keep the sign and format it as percentage style without trailing device unit
|
||||||
|
format!("{:.2}%", value * 100.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// YYC MARK:
|
||||||
|
// The function showing circuit graph should be maintained carefully.
|
||||||
|
// First, we want they are show in console properly,
|
||||||
|
// And we also want they have good code view.
|
||||||
|
//
|
||||||
|
// I notices that the number part of the output of `to_human_readable_value` will only be
|
||||||
|
// "+999.9999" or "+9.9999e+00". So its maximum of its length is 11, considering the possibility,
|
||||||
|
// that the absolute value of exponential part is larger than 99, is close to zero.
|
||||||
|
// After putting the scale unit and device unit together like " nF",
|
||||||
|
// the whole maximum size of the built string is 14.
|
||||||
|
//
|
||||||
|
// So we need pick a larger number and odd number for the space for showing device value,
|
||||||
|
// because odd value can be divided by two so it can be split as two parts equally
|
||||||
|
// for the convenient alignment of some circuit graphs.
|
||||||
|
// My picked value is 16.
|
||||||
|
// So you will see that I use `:^16` for a center alignment to given string.
|
||||||
|
//
|
||||||
|
// After this, we also need set the padding value carefully.
|
||||||
|
// This value should consider the length of f-string syntax, pre-defined chars and required chars.
|
||||||
|
// To make sure a pretty showcase both in code and display.
|
||||||
|
|
||||||
|
/// Illustrate a response item.
|
||||||
|
fn illustrate_response(&self, response: &Response, index: usize) -> Result<()> {
|
||||||
|
let item = response.get(index).expect("unexpected invalid index");
|
||||||
|
let device_kind = response.device_kind();
|
||||||
|
// print header
|
||||||
|
println!(
|
||||||
|
"Plan {:<4} Value: {:<16} Diff: {} ({})",
|
||||||
|
index + 1,
|
||||||
|
self.to_plan_head_value(item.value(), device_kind),
|
||||||
|
self.to_plan_head_diff(item.difference(), device_kind),
|
||||||
|
self.to_plan_head_diff_pct(item.relative_difference()),
|
||||||
|
);
|
||||||
|
// print circuit graph
|
||||||
|
self.illustrate_circuit(item.circuit(), device_kind)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Illustrate a circuit based on its device scale.
|
||||||
|
fn illustrate_circuit(
|
||||||
|
&self,
|
||||||
|
circuit: &Circuit,
|
||||||
|
device_kind: DeviceKind,
|
||||||
|
) -> Result<()> {
|
||||||
|
match circuit.device_scale() {
|
||||||
|
CircuitDeviceScale::One => {
|
||||||
|
self.illustrate_one_device_circuit(circuit, device_kind);
|
||||||
|
}
|
||||||
|
CircuitDeviceScale::Two => {
|
||||||
|
self.illustrate_two_device_circuit(circuit, device_kind)?;
|
||||||
|
}
|
||||||
|
CircuitDeviceScale::Three => {
|
||||||
|
self.illustrate_three_device_circuit(circuit, device_kind)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Illustrate a one-device circuit.
|
||||||
|
fn illustrate_one_device_circuit(&self, circuit: &Circuit, device_kind: DeviceKind) {
|
||||||
|
let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||||
|
println!("──[{:^16}]──", dev1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Illustrate a two-device circuit.
|
||||||
|
fn illustrate_two_device_circuit(
|
||||||
|
&self,
|
||||||
|
circuit: &Circuit,
|
||||||
|
device_kind: DeviceKind,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||||
|
let j2 = circuit.second_device_joint()?;
|
||||||
|
let dev2 = self.to_circuit_graph_value(circuit.second_device_value()?, device_kind);
|
||||||
|
match j2 {
|
||||||
|
JointKind::Series => {
|
||||||
|
println!("──[{:^16}]──[{:^16}]──", dev1, dev2);
|
||||||
|
}
|
||||||
|
JointKind::Parallel => {
|
||||||
|
let sep0 = " ".repeat(6 + (16 - 10));
|
||||||
|
println!(" ┌──[{:^16}]──┐ ", dev1);
|
||||||
|
println!("──┤ {} ├──", sep0);
|
||||||
|
println!(" └──[{:^16}]──┘ ", dev2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Illustrate a three-device circuit.
|
||||||
|
fn illustrate_three_device_circuit(
|
||||||
|
&self,
|
||||||
|
circuit: &Circuit,
|
||||||
|
device_kind: DeviceKind,
|
||||||
|
) -> Result<()> {
|
||||||
|
let dev1 = self.to_circuit_graph_value(circuit.first_device_value(), device_kind);
|
||||||
|
let j2 = circuit.second_device_joint()?;
|
||||||
|
let dev2 = self.to_circuit_graph_value(circuit.second_device_value()?, device_kind);
|
||||||
|
let j3 = circuit.third_device_joint()?;
|
||||||
|
let dev3 = self.to_circuit_graph_value(circuit.third_device_value()?, device_kind);
|
||||||
|
match j2 {
|
||||||
|
JointKind::Series => match j3 {
|
||||||
|
JointKind::Series => {
|
||||||
|
// All in series
|
||||||
|
println!("──[{dev1:^16}]──[{dev2:^16}]──[{dev3:^16}]──");
|
||||||
|
}
|
||||||
|
JointKind::Parallel => {
|
||||||
|
// First series then parallel
|
||||||
|
let sep0 = "─".repeat(6 + ((16 - 10) / 2));
|
||||||
|
let sep1 = " ".repeat(6 + 2 * (16 - 10));
|
||||||
|
println!(" ┌──[{dev1:^16}]──[{dev2:^16}]──┐ ");
|
||||||
|
println!("──┤ {sep1} ├──");
|
||||||
|
println!(" └───{sep0}[{dev3:^16}]{sep0}───┘ ");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
JointKind::Parallel => match j3 {
|
||||||
|
JointKind::Series => {
|
||||||
|
// First parallel then series
|
||||||
|
let sep0 = " ".repeat(6 + (16 - 10));
|
||||||
|
println!(" {sep0} ┌──[{dev1:^16}]──┐ ");
|
||||||
|
println!("──[{dev3:^16}]──┤ {sep0} ├──");
|
||||||
|
println!(" {sep0} └──[{dev2:^16}]──┘ ");
|
||||||
|
}
|
||||||
|
JointKind::Parallel => {
|
||||||
|
// All in parallel
|
||||||
|
println!(" ┌──[{dev1:^16}]──┐ ");
|
||||||
|
println!("──┼──[{dev2:^16}]──┼──");
|
||||||
|
println!(" └──[{dev3:^16}]──┘ ");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use clap::{Parser, ValueEnum};
|
||||||
|
|
||||||
|
/// The configuration for the app.
|
||||||
|
pub struct AppConfig {
|
||||||
|
/// The resolver for the app.
|
||||||
|
resolver: AppResolver,
|
||||||
|
/// The path to the resistor specs file.
|
||||||
|
resistor_specs: PathBuf,
|
||||||
|
/// The path to the capacitor specs file.
|
||||||
|
capacitor_specs: PathBuf,
|
||||||
|
/// The path to the inductor specs file.
|
||||||
|
inductor_specs: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
/// Get the resolver.
|
||||||
|
pub fn get_resolver(&self) -> &AppResolver {
|
||||||
|
&self.resolver
|
||||||
|
}
|
||||||
|
/// Get the path to the resistor specs file.
|
||||||
|
pub fn get_resistor_spec(&self) -> &Path {
|
||||||
|
&self.resistor_specs
|
||||||
|
}
|
||||||
|
/// Get the path to the capacitor specs file.
|
||||||
|
pub fn get_capacitor_specs(&self) -> &Path {
|
||||||
|
&self.capacitor_specs
|
||||||
|
}
|
||||||
|
/// Get the path to the inductor specs file.
|
||||||
|
pub fn get_inductor_specs(&self) -> &Path {
|
||||||
|
&self.inductor_specs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resolver for the app.
|
||||||
|
#[derive(Debug, Clone, ValueEnum)]
|
||||||
|
pub enum AppResolver {
|
||||||
|
/// The look-up table resolver.
|
||||||
|
#[value(name = "lut")]
|
||||||
|
Lut,
|
||||||
|
/// The BFS resolver.
|
||||||
|
#[value(name = "bfs")]
|
||||||
|
Bfs,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the resistor, capacitor, or inductor circuit which has the closest value
|
||||||
|
/// for your given value within at most 3 devices.
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "LCR Connector",
|
||||||
|
version,
|
||||||
|
about = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices."
|
||||||
|
)]
|
||||||
|
struct Cli {
|
||||||
|
/// The resolver you want to use.
|
||||||
|
#[arg(short = 's', long = "resolver", required = true, value_enum)]
|
||||||
|
resolver: AppResolver,
|
||||||
|
|
||||||
|
/// The path to the resistor specs file.
|
||||||
|
#[arg(
|
||||||
|
short = 'r',
|
||||||
|
long = "resistor",
|
||||||
|
required = true,
|
||||||
|
value_name = "RESISTOR.TXT"
|
||||||
|
)]
|
||||||
|
resistor_specs: PathBuf,
|
||||||
|
|
||||||
|
/// The path to the inductor specs file.
|
||||||
|
#[arg(
|
||||||
|
short = 'l',
|
||||||
|
long = "inductor",
|
||||||
|
required = true,
|
||||||
|
value_name = "INDUCTOR.TXT"
|
||||||
|
)]
|
||||||
|
inductor_specs: PathBuf,
|
||||||
|
|
||||||
|
/// The path to the capacitor specs file.
|
||||||
|
#[arg(
|
||||||
|
short = 'c',
|
||||||
|
long = "capacitor",
|
||||||
|
required = true,
|
||||||
|
value_name = "CAPACITOR.TXT"
|
||||||
|
)]
|
||||||
|
capacitor_specs: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Cli> for AppConfig {
|
||||||
|
fn from(args: Cli) -> Self {
|
||||||
|
Self {
|
||||||
|
resolver: args.resolver,
|
||||||
|
resistor_specs: args.resistor_specs,
|
||||||
|
capacitor_specs: args.capacitor_specs,
|
||||||
|
inductor_specs: args.inductor_specs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_args() -> AppConfig {
|
||||||
|
let args = Cli::parse();
|
||||||
|
let config = AppConfig::from(args);
|
||||||
|
config
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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};
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
//! Types for managing the rated values of components available in your lab.
|
||||||
|
//!
|
||||||
|
//! In this module, a "spec" means a single rated parameter value of a component,
|
||||||
|
//! such as `100` Ohms, `4.7k` Ohms, or `10u` Farads. It is **not** a general
|
||||||
|
//! technical specification document — it is simply the nominal value printed on
|
||||||
|
//! the component's body.
|
||||||
|
//!
|
||||||
|
//! - [`Spec`] — one rated value (e.g., 4.7k).
|
||||||
|
//! - [`SpecGroup`] — all rated values of a given component type that your lab
|
||||||
|
//! actually has in stock (e.g., all resistor values available in your drawer).
|
||||||
|
//! - [`SpecCatalog`] — the complete collection of rated values for resistors,
|
||||||
|
//! capacitors, and inductors.
|
||||||
|
//!
|
||||||
|
//! In short: these types answer the question "which exact component values can
|
||||||
|
//! I pick from the shelf?".
|
||||||
|
|
||||||
|
use crate::common::{
|
||||||
|
DeviceValueError, FloatingPointError, validate_device_value, validate_floating_point,
|
||||||
|
};
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufRead, BufReader, BufWriter, Error as IoError, Write};
|
||||||
|
use std::num::ParseFloatError;
|
||||||
|
use std::path::Path;
|
||||||
|
use thiserror::Error as TeError;
|
||||||
|
|
||||||
|
/// Errors that can occur when working with rated component values.
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
pub enum SpecError {
|
||||||
|
#[error("invalid device value: {0}")]
|
||||||
|
BadDeviceValue(#[from] DeviceValueError),
|
||||||
|
#[error("bad string form of device value: {0}")]
|
||||||
|
ParseHumanReadableValue(#[from] ParseHumanReadableValueError),
|
||||||
|
#[error("duplicate rated value: {0}")]
|
||||||
|
DupSpecItem(String),
|
||||||
|
#[error("empty rated value group")]
|
||||||
|
EmptySpecGroup,
|
||||||
|
#[error("fail to open rated values file: {0}")]
|
||||||
|
OpenSpecFile(IoError),
|
||||||
|
#[error("fail to read rated values file: {0}")]
|
||||||
|
ReadSpecFile(IoError),
|
||||||
|
#[error("fail to write rated values file: {0}")]
|
||||||
|
WriteSpecFile(IoError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One rated value of a component (e.g., `4.7k` standing for 4700 Ohms).
|
||||||
|
///
|
||||||
|
/// A `Spec` stores both the parsed numeric value and the original human-readable
|
||||||
|
/// string so that the value can be re-serialized exactly as it was entered.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Spec {
|
||||||
|
/// The numeric rated value (e.g., `4700.0` for `"4.7k"`).
|
||||||
|
value: f64,
|
||||||
|
/// The original human-readable form (e.g., `"4.7k"`), kept for faithful
|
||||||
|
/// round-trip serialization.
|
||||||
|
str_value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spec {
|
||||||
|
/// Create a new rated value from its human-readable representations.
|
||||||
|
pub fn new(str_value: String) -> Result<Self, SpecError> {
|
||||||
|
// Try parsing value and check its range
|
||||||
|
let value = from_human_readable_value(&str_value)?;
|
||||||
|
let value = validate_device_value(value)?;
|
||||||
|
Ok(Self { value, str_value })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the numeric rated value (e.g., `4700.0` for `"4.7k"`).
|
||||||
|
pub fn get_value(&self) -> f64 {
|
||||||
|
self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the original human-readable value form (e.g., `"4.7k"`).
|
||||||
|
pub fn get_str_value(&self) -> &str {
|
||||||
|
&self.str_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All rated values that your lab stocks for a single component type.
|
||||||
|
///
|
||||||
|
/// For example, a `SpecGroup` for resistors might hold `{100, 220, 470, 1k, 4.7k, 10k}`
|
||||||
|
/// — these are the actual resistor values you have on hand. The same concept applies
|
||||||
|
/// to capacitors and inductors.
|
||||||
|
pub struct SpecGroup {
|
||||||
|
/// The rated values belonging to this group.
|
||||||
|
specs: Vec<Spec>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpecGroup {
|
||||||
|
/// Internal constructor: parse and deduplicate a sequence of human-readable rated values.
|
||||||
|
fn new<I>(str_values: I) -> Result<Self, SpecError>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = String>,
|
||||||
|
{
|
||||||
|
// Check string form value one by one
|
||||||
|
let mut specs: Vec<Spec> = Vec::new();
|
||||||
|
let mut seen: HashSet<OrderedFloat<f64>> = HashSet::new();
|
||||||
|
|
||||||
|
for str_value in str_values {
|
||||||
|
// Build spec instance
|
||||||
|
let spec = Spec::new(str_value)?;
|
||||||
|
// Check and update set
|
||||||
|
if !seen.insert(OrderedFloat(spec.get_value())) {
|
||||||
|
return Err(SpecError::DupSpecItem(spec.get_str_value().to_string()));
|
||||||
|
}
|
||||||
|
// Add into result
|
||||||
|
specs.push(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check empty case
|
||||||
|
if specs.is_empty() {
|
||||||
|
return Err(SpecError::EmptySpecGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, assign it
|
||||||
|
Ok(Self { specs })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a spec group from any iterable of human-readable rated values (e.g., `"4.7k"`, `"100"`).
|
||||||
|
pub fn from_iterator<I, S>(str_values: I) -> Result<Self, SpecError>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = S>,
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
Self::new(str_values.into_iter().map(|i| i.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read rated values from a text block, one value per non-empty line.
|
||||||
|
pub fn from_text(text: &str) -> Result<Self, SpecError> {
|
||||||
|
let lines = text
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.trim().to_string())
|
||||||
|
.filter(|line| !line.is_empty());
|
||||||
|
Self::from_iterator(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read rated values from a file, one value per non-empty line.
|
||||||
|
pub fn from_file<P>(path: P) -> Result<Self, SpecError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let lines = reader
|
||||||
|
.lines()
|
||||||
|
.map(|line| line.map(|line| line.trim().to_string()))
|
||||||
|
.filter(|line| !matches!(line, Ok(line) if line.is_empty()))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|err| SpecError::ReadSpecFile(err))?;
|
||||||
|
Self::from_iterator(lines.into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A commonly used set of resistor rated values (E12‑derived).
|
||||||
|
pub fn resistor_preset() -> Self {
|
||||||
|
Self::from_iterator([
|
||||||
|
"100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k",
|
||||||
|
"6.8k", "10k", "47k", "100k", "1M",
|
||||||
|
]).expect("unexpected bad rated values preset")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A commonly used set of capacitor rated values.
|
||||||
|
pub fn capacitor_preset() -> Self {
|
||||||
|
Self::from_iterator([
|
||||||
|
"10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p",
|
||||||
|
"1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u",
|
||||||
|
]).expect("unexpected bad rated values preset")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A commonly used set of inductor rated values.
|
||||||
|
pub fn inductor_preset() -> Self {
|
||||||
|
Self::from_iterator([
|
||||||
|
"0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u",
|
||||||
|
"8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u",
|
||||||
|
]).expect("unexpected bad rated values preset")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.specs.iter().map(|i| i.str_value.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over the human-readable form of every rated value (for re-serialization).
|
||||||
|
pub fn save_iterator(&self) -> impl Iterator<Item = &str> {
|
||||||
|
self.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Join all rated values with newlines into a single string (for re-serialization).
|
||||||
|
pub fn save_text(&self) -> String {
|
||||||
|
itertools::join(self.save_iterator(), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write all rated values to a file, one per line.
|
||||||
|
pub fn save_file<P>(&self, path: P) -> Result<(), SpecError>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let file = File::open(path).map_err(|err| SpecError::OpenSpecFile(err))?;
|
||||||
|
let mut writer = BufWriter::new(file);
|
||||||
|
for line in self.save_iterator() {
|
||||||
|
writer
|
||||||
|
.write_all(line.as_bytes())
|
||||||
|
.map_err(|err| SpecError::WriteSpecFile(err))?;
|
||||||
|
writer
|
||||||
|
.write_all("\n".as_bytes())
|
||||||
|
.map_err(|err| SpecError::WriteSpecFile(err))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many rated values this group contains.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.specs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the numeric rated value at the given index.
|
||||||
|
pub fn get(&self, index: usize) -> Option<f64> {
|
||||||
|
self.specs.get(index).map(|i| i.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterate over all numeric rated values in this group.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = f64> + Clone {
|
||||||
|
self.specs.iter().map(|i| i.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full catalogue of rated component values your lab stocks.
|
||||||
|
///
|
||||||
|
/// Bundles three [`SpecGroup`]s — one each for resistors, capacitors, and
|
||||||
|
/// inductors. This is the top-level entry point for answering "which component
|
||||||
|
/// values are available?".
|
||||||
|
pub struct SpecCatalog {
|
||||||
|
/// Rated values available for resistors.
|
||||||
|
resistor: SpecGroup,
|
||||||
|
/// Rated values available for capacitors.
|
||||||
|
capacitor: SpecGroup,
|
||||||
|
/// Rated values available for inductors.
|
||||||
|
inductor: SpecGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpecCatalog {
|
||||||
|
/// Assemble a catalogue from the three device‑type spec groups.
|
||||||
|
pub fn new(resistor: SpecGroup, capacitor: SpecGroup, inductor: SpecGroup) -> Self {
|
||||||
|
Self {
|
||||||
|
resistor,
|
||||||
|
capacitor,
|
||||||
|
inductor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a catalogue from three iterables of human‑readable rated values.
|
||||||
|
///
|
||||||
|
/// * `resistor` — values such as `"100"`, `"4.7k"`, etc.
|
||||||
|
/// * `capacitor` — values such as `"10p"`, `"4.7u"`, etc.
|
||||||
|
/// * `inductor` — values such as `"1u"`, `"10u"`, etc.
|
||||||
|
pub fn from_iterable<I1, S1, I2, S2, I3, S3>(
|
||||||
|
resistor: I1,
|
||||||
|
capacitor: I2,
|
||||||
|
inductor: I3,
|
||||||
|
) -> Result<Self, SpecError>
|
||||||
|
where
|
||||||
|
I1: IntoIterator<Item = S1>,
|
||||||
|
S1: Into<String>,
|
||||||
|
I2: IntoIterator<Item = S2>,
|
||||||
|
S2: Into<String>,
|
||||||
|
I3: IntoIterator<Item = S3>,
|
||||||
|
S3: Into<String>,
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
resistor: SpecGroup::from_iterator(resistor)?,
|
||||||
|
capacitor: SpecGroup::from_iterator(capacitor)?,
|
||||||
|
inductor: SpecGroup::from_iterator(inductor)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a catalogue from three text blocks, one value per line.
|
||||||
|
///
|
||||||
|
/// * `resistor` — the resistor rated‑values text.
|
||||||
|
/// * `capacitor` — the capacitor rated‑values text.
|
||||||
|
/// * `inductor` — the inductor rated‑values text.
|
||||||
|
pub fn from_text(resistor: &str, capacitor: &str, inductor: &str) -> Result<Self, SpecError> {
|
||||||
|
Ok(Self {
|
||||||
|
resistor: SpecGroup::from_text(resistor)?,
|
||||||
|
capacitor: SpecGroup::from_text(capacitor)?,
|
||||||
|
inductor: SpecGroup::from_text(inductor)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a catalogue from three files, one value per line.
|
||||||
|
///
|
||||||
|
/// * `resistor` — path to the resistor rated‑values file.
|
||||||
|
/// * `capacitor` — path to the capacitor rated‑values file.
|
||||||
|
/// * `inductor` — path to the inductor rated‑values file.
|
||||||
|
pub fn from_file<P1, P2, P3>(
|
||||||
|
resistor: P1,
|
||||||
|
capacitor: P2,
|
||||||
|
inductor: P3,
|
||||||
|
) -> Result<Self, SpecError>
|
||||||
|
where
|
||||||
|
P1: AsRef<Path>,
|
||||||
|
P2: AsRef<Path>,
|
||||||
|
P3: AsRef<Path>,
|
||||||
|
{
|
||||||
|
Ok(Self {
|
||||||
|
resistor: SpecGroup::from_file(resistor)?,
|
||||||
|
capacitor: SpecGroup::from_file(capacitor)?,
|
||||||
|
inductor: SpecGroup::from_file(inductor)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A ready‑to‑use catalogue with common resistor, capacitor and inductor rated values.
|
||||||
|
pub fn devices_preset() -> Self {
|
||||||
|
Self {
|
||||||
|
resistor: SpecGroup::resistor_preset(),
|
||||||
|
capacitor: SpecGroup::capacitor_preset(),
|
||||||
|
inductor: SpecGroup::inductor_preset(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return one save‑iterator for each of the three device types.
|
||||||
|
pub fn save_iterator(
|
||||||
|
&self,
|
||||||
|
) -> (
|
||||||
|
impl Iterator<Item = &str>,
|
||||||
|
impl Iterator<Item = &str>,
|
||||||
|
impl Iterator<Item = &str>,
|
||||||
|
) {
|
||||||
|
(
|
||||||
|
self.resistor.save_iterator(),
|
||||||
|
self.capacitor.save_iterator(),
|
||||||
|
self.inductor.save_iterator(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the text representation of all three device‑type value sets.
|
||||||
|
pub fn save_text(&self) -> (String, String, String) {
|
||||||
|
(
|
||||||
|
self.resistor.save_text(),
|
||||||
|
self.capacitor.save_text(),
|
||||||
|
self.inductor.save_text(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save all three device‑type value sets to files, one value per line.
|
||||||
|
///
|
||||||
|
/// * `resistor` — file path for the resistor values.
|
||||||
|
/// * `capacitor` — file path for the capacitor values.
|
||||||
|
/// * `inductor` — file path for the inductor values.
|
||||||
|
pub fn save_file<P1, P2, P3>(
|
||||||
|
&self,
|
||||||
|
resistor: P1,
|
||||||
|
capacitor: P2,
|
||||||
|
inductor: P3,
|
||||||
|
) -> Result<(), SpecError>
|
||||||
|
where
|
||||||
|
P1: AsRef<Path>,
|
||||||
|
P2: AsRef<Path>,
|
||||||
|
P3: AsRef<Path>,
|
||||||
|
{
|
||||||
|
self.resistor.save_file(resistor)?;
|
||||||
|
self.capacitor.save_file(capacitor)?;
|
||||||
|
self.inductor.save_file(inductor)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the resistor rated‑value set.
|
||||||
|
pub fn resistor_specs(&self) -> &SpecGroup {
|
||||||
|
&self.resistor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the capacitor rated‑value set.
|
||||||
|
pub fn capacitor_specs(&self) -> &SpecGroup {
|
||||||
|
&self.capacitor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the inductor rated‑value set.
|
||||||
|
pub fn inductor_specs(&self) -> &SpecGroup {
|
||||||
|
&self.inductor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region: Human Readable Value
|
||||||
|
|
||||||
|
#[derive(Debug, TeError)]
|
||||||
|
pub enum ParseHumanReadableValueError {
|
||||||
|
#[error("fail to parse floating point part of given human readable value: {0}")]
|
||||||
|
ParseFloat(#[from] ParseFloatError),
|
||||||
|
#[error("arithmetic error: {0}")]
|
||||||
|
BadArithmetic(#[from] FloatingPointError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert human readable value to float.
|
||||||
|
///
|
||||||
|
/// `strl` is the human readable value.
|
||||||
|
/// The return value is the parsed float value. or error occurs when parsing.
|
||||||
|
///
|
||||||
|
/// This function guarantee that return value must be a valid floating value.
|
||||||
|
/// But do not guarantee that it can be used as device value.
|
||||||
|
/// It is possible that it is negative or zero floating point value.
|
||||||
|
pub fn from_human_readable_value(strl: &str) -> Result<f64, ParseHumanReadableValueError> {
|
||||||
|
let strl = strl.trim();
|
||||||
|
|
||||||
|
let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') {
|
||||||
|
(stripped, 1e-12)
|
||||||
|
} else if let Some(stripped) = strl.strip_suffix('p') {
|
||||||
|
(stripped, 1e-9)
|
||||||
|
} else if let Some(stripped) = strl.strip_suffix('u') {
|
||||||
|
(stripped, 1e-6)
|
||||||
|
} else if let Some(stripped) = strl.strip_suffix('m') {
|
||||||
|
(stripped, 1e-3)
|
||||||
|
} else if let Some(stripped) = strl.strip_suffix('k') {
|
||||||
|
(stripped, 1e3)
|
||||||
|
} else if let Some(stripped) = strl.strip_suffix('M') {
|
||||||
|
(stripped, 1e6)
|
||||||
|
} else if let Some(stripped) = strl.strip_suffix('G') {
|
||||||
|
(stripped, 1e9)
|
||||||
|
} else {
|
||||||
|
(strl, 1.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let num = num_part.parse::<f64>()?;
|
||||||
|
Ok(validate_floating_point(num * multiplier)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unit scale for human readable value.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum UnitScale {
|
||||||
|
NanoLower,
|
||||||
|
Nano,
|
||||||
|
Micro,
|
||||||
|
Milli,
|
||||||
|
None,
|
||||||
|
Kilo,
|
||||||
|
Mega,
|
||||||
|
Giga,
|
||||||
|
GigaHigher,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the unit scale of human readable value.
|
||||||
|
///
|
||||||
|
/// `v` is the value for analyzing scale.
|
||||||
|
/// It must be a valid floating point value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics when given floating point value is bad.
|
||||||
|
pub fn get_human_readable_value_scale(v: f64) -> UnitScale {
|
||||||
|
let v = validate_floating_point(v).expect("unexpected bad floating point value");
|
||||||
|
let v = v.abs();
|
||||||
|
if v < 1e-12 {
|
||||||
|
UnitScale::NanoLower
|
||||||
|
} else if v < 1e-9 {
|
||||||
|
UnitScale::Nano
|
||||||
|
} else if v < 1e-6 {
|
||||||
|
UnitScale::Micro
|
||||||
|
} else if v < 1e-3 {
|
||||||
|
UnitScale::Milli
|
||||||
|
} else if v < 1e3 {
|
||||||
|
UnitScale::None
|
||||||
|
} else if v < 1e6 {
|
||||||
|
UnitScale::Kilo
|
||||||
|
} else if v < 1e9 {
|
||||||
|
UnitScale::Mega
|
||||||
|
} else if v < 1e12 {
|
||||||
|
UnitScale::Giga
|
||||||
|
} else {
|
||||||
|
UnitScale::GigaHigher
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert float value to human readable value.
|
||||||
|
///
|
||||||
|
/// `v`is the float value for formatting as human readable value.
|
||||||
|
/// It must be a valid floating point value.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This function panics when given floating point value is bad.
|
||||||
|
pub fn to_human_readable_value(v: f64) -> String {
|
||||||
|
let scale = get_human_readable_value_scale(v);
|
||||||
|
match scale {
|
||||||
|
UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12),
|
||||||
|
UnitScale::Nano => format!("{:+.4} p", v / 1e-9),
|
||||||
|
UnitScale::Micro => format!("{:+.4} u", v / 1e-6),
|
||||||
|
UnitScale::Milli => format!("{:+.4} m", v / 1e-3),
|
||||||
|
// YYC MARK:
|
||||||
|
// The space of this format string is by design
|
||||||
|
// for keeping the same style with other format strings.
|
||||||
|
UnitScale::None => format!("{:+.4} ", v),
|
||||||
|
UnitScale::Kilo => format!("{:+.4} k", v / 1e3),
|
||||||
|
UnitScale::Mega => format!("{:+.4} M", v / 1e6),
|
||||||
|
UnitScale::Giga => format!("{:+.4} G", v / 1e9),
|
||||||
|
UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use lcrconn::spec;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spec_preset() {
|
||||||
|
// All individual preset and catalog preset should nit panic
|
||||||
|
let _ = spec::SpecGroup::resistor_preset();
|
||||||
|
let _ = spec::SpecGroup::capacitor_preset();
|
||||||
|
let _ = spec::SpecGroup::inductor_preset();
|
||||||
|
|
||||||
|
let _ = spec::SpecCatalog::devices_preset();
|
||||||
|
}
|
||||||
+52
-1
@@ -1,3 +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.
|
||||||
|
|
||||||
|
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,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" }
|
||||||
|
|||||||
@@ -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,15 +181,9 @@ 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}\t{1}\t{2:.2%}".format(
|
|
||||||
index + 1,
|
|
||||||
to_human_readable_value(item.value),
|
|
||||||
item.relative_difference,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# print page footer
|
# print page footer
|
||||||
print("")
|
print("")
|
||||||
print("Page {} of {}.".format(current_page + 1, all_page + 1))
|
print("Page {} of {}.".format(current_page + 1, all_page + 1))
|
||||||
@@ -196,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.
|
||||||
@@ -204,16 +209,41 @@ class App:
|
|||||||
:return: The command. It is an instance of `cmd_enum`.
|
:return: The command. It is an instance of `cmd_enum`.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
print("> ", end=None)
|
self.__show_prompt_arrow()
|
||||||
words = input()
|
words = input().strip()
|
||||||
words = words.strip()
|
if words == "":
|
||||||
if words in cmd_enum:
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
return cmd_enum(words)
|
return cmd_enum(words)
|
||||||
|
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()
|
||||||
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.")
|
||||||
@@ -222,7 +252,10 @@ class App:
|
|||||||
|
|
||||||
def __accept_device_value_tolerance(self, target_value: float) -> float:
|
def __accept_device_value_tolerance(self, target_value: float) -> float:
|
||||||
while True:
|
while True:
|
||||||
|
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(
|
||||||
@@ -238,6 +271,9 @@ class App:
|
|||||||
else:
|
else:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def __show_prompt_arrow(self) -> None:
|
||||||
|
print("> ", end="")
|
||||||
|
|
||||||
def __parse_plain_float(
|
def __parse_plain_float(
|
||||||
self, user_value: str, checker: Callable[[float], bool]
|
self, user_value: str, checker: Callable[[float], bool]
|
||||||
) -> float | None:
|
) -> float | None:
|
||||||
@@ -278,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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -1,345 +0,0 @@
|
|||||||
import math
|
|
||||||
import struct
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
OneComponentList = []
|
|
||||||
TwoComponentList = []
|
|
||||||
ThreeComponentList = []
|
|
||||||
|
|
||||||
ResultList = []
|
|
||||||
|
|
||||||
class OneComponent(object):
|
|
||||||
Value1 = 0.0
|
|
||||||
|
|
||||||
Value = 0.0
|
|
||||||
def PrintCircuit(self):
|
|
||||||
print(OutputAsHuman(self.Value1))
|
|
||||||
|
|
||||||
class TwoComponent(object):
|
|
||||||
Value1 = 0.0
|
|
||||||
Value2 = 0.0
|
|
||||||
IsSeries = True
|
|
||||||
|
|
||||||
Value = 0.0
|
|
||||||
def PrintCircuit(self):
|
|
||||||
print("[{}] ┬ {}".format('S' if self.IsSeries else 'P', OutputAsHuman(self.Value1)))
|
|
||||||
print(" └ {}".format(OutputAsHuman(self.Value2)))
|
|
||||||
|
|
||||||
class ThreeComponent(object):
|
|
||||||
Value1 = 0.0
|
|
||||||
Value2 = 0.0
|
|
||||||
Value3 = 0.0
|
|
||||||
IsSeries1 = True
|
|
||||||
IsSeries2 = True
|
|
||||||
|
|
||||||
Value = 0.0
|
|
||||||
def PrintCircuit(self):
|
|
||||||
print("[{}] ┬ [{}] ┬ {}".format('S' if self.IsSeries2 else 'P', 'S' if self.IsSeries1 else 'P', OutputAsHuman(self.Value1)))
|
|
||||||
print(" │ └ {}".format(OutputAsHuman(self.Value2)))
|
|
||||||
print(" └ {}".format(OutputAsHuman(self.Value3)))
|
|
||||||
|
|
||||||
class ResultStruct(object):
|
|
||||||
Subtraction = 0.0
|
|
||||||
ComponentCount = 0
|
|
||||||
CorrespondingClass = None
|
|
||||||
|
|
||||||
def LoadFromFile(name):
|
|
||||||
f = open(name, 'r', encoding = 'utf-8')
|
|
||||||
|
|
||||||
# read file to get one elements state
|
|
||||||
while True:
|
|
||||||
cache = f.readline()
|
|
||||||
if cache == '':
|
|
||||||
break
|
|
||||||
|
|
||||||
(status, value) = InputAsHuman(cache.strip())
|
|
||||||
if status:
|
|
||||||
newobj = None
|
|
||||||
newobj = OneComponent()
|
|
||||||
newobj.Value = value
|
|
||||||
newobj.Value1 = value
|
|
||||||
OneComponentList.append(newobj)
|
|
||||||
|
|
||||||
# construct two component list
|
|
||||||
length = len(OneComponentList)
|
|
||||||
for i_index in range(length):
|
|
||||||
for j_index in range(i_index, length):
|
|
||||||
i = OneComponentList[i_index]
|
|
||||||
j = OneComponentList[j_index]
|
|
||||||
|
|
||||||
# series
|
|
||||||
newobj = TwoComponent()
|
|
||||||
newobj.Value1 = i.Value
|
|
||||||
newobj.Value2 = j.Value
|
|
||||||
newobj.IsSeries = True
|
|
||||||
newobj.Value = i.Value + j.Value
|
|
||||||
TwoComponentList.append(newobj)
|
|
||||||
|
|
||||||
# parallel
|
|
||||||
newobj = TwoComponent()
|
|
||||||
newobj.Value1 = i.Value
|
|
||||||
newobj.Value2 = j.Value
|
|
||||||
newobj.IsSeries = False
|
|
||||||
newobj.Value = (i.Value * j.Value) / (i.Value + j.Value)
|
|
||||||
TwoComponentList.append(newobj)
|
|
||||||
|
|
||||||
# construct three component list
|
|
||||||
for i in OneComponentList:
|
|
||||||
for j in TwoComponentList:
|
|
||||||
# series
|
|
||||||
newobj = ThreeComponent()
|
|
||||||
newobj.Value1 = j.Value1
|
|
||||||
newobj.Value2 = j.Value2
|
|
||||||
newobj.Value3 = i.Value
|
|
||||||
newobj.IsSeries1 = j.IsSeries
|
|
||||||
newobj.IsSeries2 = True
|
|
||||||
newobj.Value = j.Value + i.Value
|
|
||||||
ThreeComponentList.append(newobj)
|
|
||||||
|
|
||||||
# parallel
|
|
||||||
newobj = ThreeComponent()
|
|
||||||
newobj.Value1 = j.Value1
|
|
||||||
newobj.Value2 = j.Value2
|
|
||||||
newobj.Value3 = i.Value
|
|
||||||
newobj.IsSeries1 = j.IsSeries
|
|
||||||
newobj.IsSeries2 = False
|
|
||||||
newobj.Value = (j.Value * i.Value) / (j.Value + i.Value)
|
|
||||||
ThreeComponentList.append(newobj)
|
|
||||||
|
|
||||||
f.close()
|
|
||||||
SaveAsCache(name)
|
|
||||||
|
|
||||||
def LoadFromCache(name):
|
|
||||||
f = open(name + '.cache', 'rb')
|
|
||||||
counter = 0
|
|
||||||
(counter, ) = struct.unpack("I", f.read(4))
|
|
||||||
for i in range(counter):
|
|
||||||
newobj = OneComponent()
|
|
||||||
newobj.Value1 = ReadDouble(f)
|
|
||||||
newobj.Value = ReadDouble(f)
|
|
||||||
OneComponentList.append(newobj)
|
|
||||||
(counter, ) = struct.unpack("I", f.read(4))
|
|
||||||
for i in range(counter):
|
|
||||||
newobj = TwoComponent()
|
|
||||||
newobj.Value1 = ReadDouble(f)
|
|
||||||
newobj.Value2 = ReadDouble(f)
|
|
||||||
newobj.IsSeries = ReadBoolean(f)
|
|
||||||
newobj.Value = ReadDouble(f)
|
|
||||||
TwoComponentList.append(newobj)
|
|
||||||
(counter, ) = struct.unpack("I", f.read(4))
|
|
||||||
for i in range(counter):
|
|
||||||
newobj = ThreeComponent()
|
|
||||||
newobj.Value1 = ReadDouble(f)
|
|
||||||
newobj.Value2 = ReadDouble(f)
|
|
||||||
newobj.Value3 = ReadDouble(f)
|
|
||||||
newobj.IsSeries1 = ReadBoolean(f)
|
|
||||||
newobj.IsSeries2 = ReadBoolean(f)
|
|
||||||
newobj.Value = ReadDouble(f)
|
|
||||||
ThreeComponentList.append(newobj)
|
|
||||||
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def SaveAsCache(name):
|
|
||||||
# in cache, is series should follow resistor mode
|
|
||||||
f = open(name + '.cache', 'wb')
|
|
||||||
WriteInt(f, len(OneComponentList))
|
|
||||||
for i in OneComponentList:
|
|
||||||
WriteDouble(f, i.Value1)
|
|
||||||
WriteDouble(f, i.Value)
|
|
||||||
WriteInt(f, len(TwoComponentList))
|
|
||||||
for i in TwoComponentList:
|
|
||||||
WriteDouble(f, i.Value1)
|
|
||||||
WriteDouble(f, i.Value2)
|
|
||||||
WriteBoolean(f, i.IsSeries)
|
|
||||||
WriteDouble(f, i.Value)
|
|
||||||
WriteInt(f, len(ThreeComponentList))
|
|
||||||
for i in ThreeComponentList:
|
|
||||||
WriteDouble(f, i.Value1)
|
|
||||||
WriteDouble(f, i.Value2)
|
|
||||||
WriteDouble(f, i.Value3)
|
|
||||||
WriteBoolean(f, i.IsSeries1)
|
|
||||||
WriteBoolean(f, i.IsSeries2)
|
|
||||||
WriteDouble(f, i.Value)
|
|
||||||
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def ReadDouble(fs):
|
|
||||||
return struct.unpack("d", fs.read(8))[0]
|
|
||||||
|
|
||||||
def ReadInt(fs):
|
|
||||||
return struct.unpack("I", fs.read(4))[0]
|
|
||||||
|
|
||||||
def ReadBoolean(fs):
|
|
||||||
return struct.unpack("?", fs.read(1))[0]
|
|
||||||
|
|
||||||
def WriteDouble(fs, num):
|
|
||||||
fs.write(struct.pack("d", num))
|
|
||||||
|
|
||||||
def WriteInt(fs, num):
|
|
||||||
fs.write(struct.pack("I", num))
|
|
||||||
|
|
||||||
def WriteBoolean(fs, num):
|
|
||||||
fs.write(struct.pack("?", num))
|
|
||||||
|
|
||||||
def OutputAsHuman(v):
|
|
||||||
if v / 1e-12 < 1e3:
|
|
||||||
return "{:e} n".format(v / 1e-12)
|
|
||||||
if v / 1e-9 < 1e3:
|
|
||||||
return "{:.4f} p".format(v / 1e-9)
|
|
||||||
if v / 1e-6 < 1e3:
|
|
||||||
return "{:.4f} u".format(v / 1e-6)
|
|
||||||
if v / 1e-3 < 1e3:
|
|
||||||
return "{:.4f} m".format(v / 1e-3)
|
|
||||||
if v < 1e3:
|
|
||||||
return "{:.4f}".format(v)
|
|
||||||
if v / 1e3 < 1e3:
|
|
||||||
return "{:.4f} k".format(v / 1e3)
|
|
||||||
if v / 1e6 < 1e3:
|
|
||||||
return "{:.4f} M".format(v / 1e6)
|
|
||||||
|
|
||||||
return "{:e}".format(v)
|
|
||||||
|
|
||||||
def InputAsHuman(strl):
|
|
||||||
try:
|
|
||||||
if strl.endswith('n'):
|
|
||||||
return (True, float(strl[0:-1]) * 1e-12)
|
|
||||||
if strl.endswith('p'):
|
|
||||||
return (True, float(strl[0:-1]) * 1e-9)
|
|
||||||
if strl.endswith('u'):
|
|
||||||
return (True, float(strl[0:-1]) * 1e-6)
|
|
||||||
if strl.endswith('m'):
|
|
||||||
return (True, float(strl[0:-1]) * 1e-3)
|
|
||||||
if strl.endswith('k'):
|
|
||||||
return (True, float(strl[0:-1]) * 1e3)
|
|
||||||
if strl.endswith('M'):
|
|
||||||
return (True, float(strl[0:-1]) * 1e6)
|
|
||||||
return (True, float(strl))
|
|
||||||
except:
|
|
||||||
return (False, 0.0)
|
|
||||||
|
|
||||||
def ValidCommandInput(valid_list):
|
|
||||||
while True:
|
|
||||||
cache = input()
|
|
||||||
if cache not in valid_list:
|
|
||||||
print('Wrong command, please input again.')
|
|
||||||
else:
|
|
||||||
return cache
|
|
||||||
|
|
||||||
def ValidNumberInput():
|
|
||||||
while True:
|
|
||||||
cache = input()
|
|
||||||
(status, num) = InputAsHuman(cache)
|
|
||||||
if not status:
|
|
||||||
print('Wrong number, please input again.')
|
|
||||||
else:
|
|
||||||
return num
|
|
||||||
|
|
||||||
def DoQuery():
|
|
||||||
# get config
|
|
||||||
|
|
||||||
print('What are you connecting?')
|
|
||||||
print('r: resistor')
|
|
||||||
print('l: inductor')
|
|
||||||
print('c: capacitor')
|
|
||||||
mode = ValidCommandInput(['c', 'l', 'r'])
|
|
||||||
is_resistor_mode = mode != 'c'
|
|
||||||
|
|
||||||
print('Your target value?')
|
|
||||||
target = ValidNumberInput()
|
|
||||||
|
|
||||||
print('Your tolerance?')
|
|
||||||
target_tolerance = ValidNumberInput()
|
|
||||||
|
|
||||||
print('How to sort result?')
|
|
||||||
print('l: less component')
|
|
||||||
print('a: more accuracy')
|
|
||||||
sort_mode = ValidCommandInput(['l', 'a'])
|
|
||||||
|
|
||||||
# start computing
|
|
||||||
print('Collecting and sorting data...')
|
|
||||||
ResultList.clear()
|
|
||||||
for i in OneComponentList:
|
|
||||||
cache = i.Value - target
|
|
||||||
if abs(cache) < target_tolerance:
|
|
||||||
newobj = ResultStruct()
|
|
||||||
newobj.Subtraction = cache
|
|
||||||
newobj.ComponentCount = 1
|
|
||||||
newobj.CorrespondingClass = i
|
|
||||||
ResultList.append(newobj)
|
|
||||||
for i in TwoComponentList:
|
|
||||||
cache = i.Value - target
|
|
||||||
if abs(cache) < target_tolerance:
|
|
||||||
newobj = ResultStruct()
|
|
||||||
newobj.Subtraction = cache
|
|
||||||
newobj.ComponentCount = 2
|
|
||||||
newobj.CorrespondingClass = i
|
|
||||||
ResultList.append(newobj)
|
|
||||||
for i in ThreeComponentList:
|
|
||||||
cache = i.Value - target
|
|
||||||
if abs(cache) < target_tolerance:
|
|
||||||
newobj = ResultStruct()
|
|
||||||
newobj.Subtraction = cache
|
|
||||||
newobj.ComponentCount = 3
|
|
||||||
newobj.CorrespondingClass = i
|
|
||||||
ResultList.append(newobj)
|
|
||||||
|
|
||||||
if sort_mode == 'l':
|
|
||||||
ResultList.sort(key = lambda x: (x.ComponentCount, abs(x.Subtraction)))
|
|
||||||
else:
|
|
||||||
ResultList.sort(key = lambda x: abs(x.Subtraction))
|
|
||||||
|
|
||||||
# display result
|
|
||||||
if len(ResultList) == 0:
|
|
||||||
print('Sorry, no result!')
|
|
||||||
return
|
|
||||||
|
|
||||||
count = len(ResultList)
|
|
||||||
all_page = int(count / 10)
|
|
||||||
current_page = 0
|
|
||||||
while current_page <= all_page:
|
|
||||||
for i in range(9):
|
|
||||||
picked_index = current_page * 9 + i
|
|
||||||
if (picked_index < count):
|
|
||||||
picked_item = ResultList[picked_index]
|
|
||||||
print("Plan {}\t{}\t{:.2%}".format(picked_index + 1, OutputAsHuman(picked_item.CorrespondingClass.Value), picked_item.Subtraction / target))
|
|
||||||
picked_item.CorrespondingClass.PrintCircuit()
|
|
||||||
|
|
||||||
print('')
|
|
||||||
print("Page {} of {}. p: next page. q: exit".format(current_page + 1, all_page + 1))
|
|
||||||
command = ValidCommandInput(['p', 'q'])
|
|
||||||
if command == 'p':
|
|
||||||
current_page += 1
|
|
||||||
elif command == 'q':
|
|
||||||
break
|
|
||||||
|
|
||||||
def DoHelp():
|
|
||||||
print('LCR Connect Help:')
|
|
||||||
print('')
|
|
||||||
print('query: do a query immediately. following the guider and find the result.')
|
|
||||||
print('help: print this')
|
|
||||||
print('exit: exit this app')
|
|
||||||
|
|
||||||
# ===================================================== program start
|
|
||||||
|
|
||||||
print('LCR Connect')
|
|
||||||
|
|
||||||
# loading file
|
|
||||||
print('Input the component value list file name:')
|
|
||||||
filename = input()
|
|
||||||
if not os.path.isfile(filename + '.cache'):
|
|
||||||
LoadFromFile(filename)
|
|
||||||
else:
|
|
||||||
LoadFromCache(filename)
|
|
||||||
|
|
||||||
# ready for command
|
|
||||||
while True:
|
|
||||||
sys.stdout.write('> ')
|
|
||||||
sys.stdout.flush()
|
|
||||||
cmd = ValidCommandInput(['exit', 'query', 'help'])
|
|
||||||
if cmd == 'exit':
|
|
||||||
break
|
|
||||||
elif cmd == 'query':
|
|
||||||
DoQuery()
|
|
||||||
elif cmd == 'help':
|
|
||||||
DoHelp()
|
|
||||||
@@ -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:
|
||||||
@@ -60,16 +74,16 @@ class ResponseItem:
|
|||||||
|
|
||||||
:return: The circuit.
|
:return: The circuit.
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
|
|
||||||
:return: The device count.
|
:return: The device count.
|
||||||
"""
|
"""
|
||||||
return self.circuit.device_scale.to_device_count()
|
return self.__circuit.device_scale.to_device_count()
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def value(self) -> float:
|
def value(self) -> float:
|
||||||
@@ -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,24 +152,31 @@ 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
|
|
||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user