From aa6c4f72bd78b800f16d56d1705ee6780d6777dc Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Sun, 28 Jun 2026 20:47:00 +0800 Subject: [PATCH] feat: use AI to migrate project (no fix now) --- kernel/Cargo.lock | 20 + kernel/lcrconn-cli/src/main.rs | 642 ++++++++++++++++++++++++++++- kernel/lcrconn/Cargo.toml | 2 + kernel/lcrconn/src/common.rs | 512 +++++++++++++++++++++++ kernel/lcrconn/src/dataset.rs | 445 ++++++++++++++++++++ kernel/lcrconn/src/lib.rs | 26 +- kernel/lcrconn/src/query.rs | 256 ++++++++++++ kernel/lcrconn/src/resolver/bfs.rs | 481 +++++++++++++++++++++ kernel/lcrconn/src/resolver/lut.rs | 156 +++++++ kernel/lcrconn/src/resolver/mod.rs | 26 ++ 10 files changed, 2551 insertions(+), 15 deletions(-) create mode 100644 kernel/lcrconn/src/common.rs create mode 100644 kernel/lcrconn/src/dataset.rs create mode 100644 kernel/lcrconn/src/query.rs create mode 100644 kernel/lcrconn/src/resolver/bfs.rs create mode 100644 kernel/lcrconn/src/resolver/lut.rs create mode 100644 kernel/lcrconn/src/resolver/mod.rs diff --git a/kernel/Cargo.lock b/kernel/Cargo.lock index 1840fe0..cf3a1d9 100644 --- a/kernel/Cargo.lock +++ b/kernel/Cargo.lock @@ -114,6 +114,8 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" name = "lcrconn" version = "1.0.0" dependencies = [ + "strum", + "strum_macros", "thiserror", ] @@ -156,6 +158,24 @@ 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" diff --git a/kernel/lcrconn-cli/src/main.rs b/kernel/lcrconn-cli/src/main.rs index e7a11a9..e080906 100644 --- a/kernel/lcrconn-cli/src/main.rs +++ b/kernel/lcrconn-cli/src/main.rs @@ -1,3 +1,641 @@ -fn main() { - println!("Hello, world!"); +use std::io::{self, Write}; +use std::path::PathBuf; + +use clap::Parser; +use lcrconn::{ + from_human_readable_value, to_human_readable_value, BfsResolver, Circuit, CircuitDeviceScale, + DatasetCollection, DeviceKind, JointKind, LcrConnError, LutResolver, Request, Resolver, + Response, ResponsePriority, MAX_RESPONSE_CNT, +}; + +// ============================================================================ +// Command-line arguments +// ============================================================================ + +/// The resolver for the app. +#[derive(Clone, Debug, clap::ValueEnum)] +pub enum AppResolver { + /// The look-up table resolver. + #[value(name = "lut")] + Lut, + /// The BFS resolver. + #[value(name = "bfs")] + Bfs, +} + +/// The configuration for the app. +struct AppConfig { + /// The resolver for the app. + resolver: AppResolver, + /// The path to the resistor dataset file. + resistor_dataset: PathBuf, + /// The path to the capacitor dataset file. + capacitor_dataset: PathBuf, + /// The path to the inductor dataset file. + inductor_dataset: PathBuf, +} + +/// Get the resistor, capacitor, or inductor circuit which has the closest value +/// for your given value within at most 3 devices. +#[derive(Parser)] +#[command( + name = "LCR Connector", + about = "Get the resistor, capacitor, or inductor circuit which has the closest value for your given value within at most 3 devices." +)] +struct Args { + /// The resolver you want to use. + #[arg(short = 's', long)] + resolver: AppResolver, + + /// The path to the resistor dataset file. + #[arg(short = 'r', long, value_name = "RESISTOR.TXT")] + resistor_dataset: PathBuf, + + /// The path to the inductor dataset file. + #[arg(short = 'l', long, value_name = "INDUCTOR.TXT")] + inductor_dataset: PathBuf, + + /// The path to the capacitor dataset file. + #[arg(short = 'c', long, value_name = "CAPACITOR.TXT")] + capacitor_dataset: PathBuf, +} + +impl From for AppConfig { + fn from(args: Args) -> Self { + Self { + resolver: args.resolver, + resistor_dataset: args.resistor_dataset, + capacitor_dataset: args.capacitor_dataset, + inductor_dataset: args.inductor_dataset, + } + } +} + +// ============================================================================ +// Interactive command enums +// ============================================================================ + +/// The command for the main menu. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MainCmd { + Query, + Help, + Exit, +} + +fn parse_main_cmd(s: &str) -> Option { + match s { + "query" => Some(MainCmd::Query), + "help" => Some(MainCmd::Help), + "exit" => Some(MainCmd::Exit), + _ => None, + } +} + +/// The device choice for query. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum QueryDeviceChoice { + Resistor, + Capacitor, + Inductor, +} + +impl QueryDeviceChoice { + fn parse(s: &str) -> Option { + match s { + "r" => Some(Self::Resistor), + "c" => Some(Self::Capacitor), + "l" => Some(Self::Inductor), + _ => None, + } + } + + fn to_device_kind(self) -> DeviceKind { + match self { + Self::Resistor => DeviceKind::Resistor, + Self::Capacitor => DeviceKind::Capacitor, + Self::Inductor => DeviceKind::Inductor, + } + } +} + +/// The sort priority for query results. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum QuerySortPriority { + LessDevices, + MoreAccuracy, +} + +impl QuerySortPriority { + fn parse(s: &str) -> Option { + match s { + "l" => Some(Self::LessDevices), + "a" => Some(Self::MoreAccuracy), + _ => None, + } + } + + fn to_response_priority(self) -> ResponsePriority { + match self { + Self::LessDevices => ResponsePriority::LessDevices, + Self::MoreAccuracy => ResponsePriority::MoreAccuracy, + } + } +} + +/// The command for the page viewer. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum PageViewerCmd { + PreviousPage, + NextPage, + Quit, +} + +impl PageViewerCmd { + fn parse(s: &str) -> Option { + match s { + "f" => Some(Self::PreviousPage), + "b" => Some(Self::NextPage), + "q" => Some(Self::Quit), + _ => None, + } + } +} + +// ============================================================================ +// Input utilities +// ============================================================================ + +/// Read a single line from stdin, trimmed of surrounding whitespace. +fn read_line() -> String { + let mut line = String::new(); + io::stdin() + .read_line(&mut line) + .expect("Failed to read from stdin"); + line.trim().to_string() +} + +/// Parse a plain float value. +/// +/// # Arguments +/// +/// * `user_value` - The value to parse. +/// * `checker` - A function that checks if the input is valid. +/// It takes a float as input and returns a bool. True means the input is valid, +/// otherwise False. +/// +/// # Returns +/// +/// The parsed value if it is valid, otherwise `None`. +fn parse_plain_float(user_value: &str, checker: impl Fn(&f64) -> bool) -> Option { + // try parsing it first + let value = user_value.parse::().ok()?; + // then check it by checker + if checker(&value) { + Some(value) + } else { + None + } +} + +/// Parse a human-readable device value. +/// +/// # Arguments +/// +/// * `user_value` - The value to parse. +/// +/// # Returns +/// +/// The parsed value if it is valid and positive, otherwise `None`. +fn parse_human_readable_value(user_value: &str) -> Option { + // parse it + let value = from_human_readable_value(user_value).ok()?; + // then check its range + if value > 0.0 { + Some(value) + } else { + None + } +} + +// ============================================================================ +// Response display utilities +// ============================================================================ + +/// Get the unit string for a device kind. +fn get_device_unit(device_kind: DeviceKind) -> &'static str { + match device_kind { + DeviceKind::Resistor => "\u{2126}", + DeviceKind::Capacitor => "F", + DeviceKind::Inductor => "H", + } +} + +/// Format a device value for display in the circuit graph. +fn to_circuit_graph_value(value: f64, device_kind: DeviceKind) -> String { + // Remove sign and append device unit + let hr = to_human_readable_value(value); + let without_sign = &hr[1..]; + format!("{}{}", without_sign, get_device_unit(device_kind)) +} + +/// Format a device value for the plan header. +fn to_plan_head_value(value: f64, device_kind: DeviceKind) -> String { + // Remove sign and append device unit + let hr = to_human_readable_value(value); + let without_sign = &hr[1..]; + format!("{}{}", without_sign, get_device_unit(device_kind)) +} + +/// Format a difference value for the plan header. +fn to_plan_head_diff(value: f64, device_kind: DeviceKind) -> String { + // Keep the sign and append device unit + format!("{}{}", to_human_readable_value(value), get_device_unit(device_kind)) +} + +/// Format a relative difference as percentage. +fn to_plan_head_diff_pct(value: f64) -> String { + // Keep the sign and format it as percentage style without trailing device unit + format!("{:.2}%", value * 100.0) +} + +// YYC MARK: +// The function showing circuit graph should be maintained carefully. +// First, we want they are show in console properly, +// And we also want they have good code view. +// +// I notices that the number part of the output of `to_human_readable_value` will only be +// "+999.9999" or "+9.9999e+00". So its maximum of its length is 11, considering the possibility, +// that the absolute value of exponential part is larger than 99, is close to zero. +// After putting the scale unit and device unit together like " nF", +// the whole maximum size of the built string is 14. +// +// So we need pick a larger number and odd number for the space for showing device value, +// because odd value can be divided by two so it can be split as two parts equally +// for the convenient alignment of some circuit graphs. +// My picked value is 16. +// So you will see that I use `:^16` for a center alignment to given string. +// +// After this, we also need set the padding value carefully. +// This value should consider the length of f-string syntax, pre-defined chars and required chars. +// To make sure a pretty showcase both in code and display. + +/// Illustrate a one-device circuit. +fn illustrate_one_device_circuit(circuit: &Circuit, device_kind: DeviceKind) { + let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind); + println!("──[{:^16}]──", dev1); +} + +/// Illustrate a two-device circuit. +fn illustrate_two_device_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> { + let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind); + let j2 = circuit.second_device_joint()?; + let dev2 = to_circuit_graph_value(circuit.second_device_value()?, device_kind); + match j2 { + JointKind::Series => { + println!("──[{:^16}]──[{:^16}]──", dev1, dev2); + } + JointKind::Parallel => { + let sep0 = " ".repeat(6 + (16 - 10)); + println!(" ┌──[{:^16}]──┐ ", dev1); + println!("──┤ {} ├──", sep0); + println!(" └──[{:^16}]──┘ ", dev2); + } + } + Ok(()) +} + +/// Illustrate a three-device circuit. +fn illustrate_three_device_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> { + let dev1 = to_circuit_graph_value(circuit.first_device_value(), device_kind); + let j2 = circuit.second_device_joint()?; + let dev2 = to_circuit_graph_value(circuit.second_device_value()?, device_kind); + let j3 = circuit.third_device_joint()?; + let dev3 = to_circuit_graph_value(circuit.third_device_value()?, device_kind); + match j2 { + JointKind::Series => match j3 { + JointKind::Series => { + // All in series + println!("──[{:^16}]──[{:^16}]──[{:^16}]──", dev1, dev2, dev3); + } + JointKind::Parallel => { + // First series then parallel + let sep0 = "\u{2500}".repeat(6 + ((16 - 10) / 2)); + let sep1 = " ".repeat(6 + 2 * (16 - 10)); + println!(" ┌──[{:^16}]──[{:^16}]──┐ ", dev1, dev2); + println!("──┤ {} ├──", sep1); + println!(" └───{}[{:^16}]{}───┘ ", sep0, dev3, sep0); + } + }, + JointKind::Parallel => match j3 { + JointKind::Series => { + // First parallel then series + let sep0 = " ".repeat(6 + (16 - 10)); + println!(" {} ┌──[{:^16}]──┐ ", sep0, dev1); + println!("──[{:^16}]──┤ {} ├──", dev3, sep0); + println!(" {} └──[{:^16}]──┘ ", sep0, dev2); + } + JointKind::Parallel => { + // All in parallel + println!(" ┌──[{:^16}]──┐ ", dev1); + println!("──┼──[{:^16}]──┼──", dev2); + println!(" └──[{:^16}]──┘ ", dev3); + } + }, + } + Ok(()) +} + +/// Illustrate a circuit based on its device scale. +fn illustrate_circuit(circuit: &Circuit, device_kind: DeviceKind) -> Result<(), LcrConnError> { + match circuit.device_scale() { + CircuitDeviceScale::One => { + illustrate_one_device_circuit(circuit, device_kind); + } + CircuitDeviceScale::Two => { + illustrate_two_device_circuit(circuit, device_kind)?; + } + CircuitDeviceScale::Three => { + illustrate_three_device_circuit(circuit, device_kind)?; + } + } + Ok(()) +} + +/// Illustrate a response item. +fn illustrate_response(response: &Response, index: usize) -> Result<(), LcrConnError> { + let item = &response[index]; + let device_kind = response.device_kind(); + // print header + println!( + "Plan {:<4} Value: {:<16} Diff: {} ({})", + index + 1, + to_plan_head_value(item.value(), device_kind), + to_plan_head_diff(item.difference(), device_kind), + to_plan_head_diff_pct(item.relative_difference()), + ); + // print circuit graph + illustrate_circuit(item.circuit(), device_kind)?; + Ok(()) +} + +// ============================================================================ +// App +// ============================================================================ + +/// The app. +struct App { + /// The resolver for the app. + resolver: Box, +} + +impl App { + /// Create a new app with the given configuration. + /// + /// # Errors + /// + /// See [`DatasetCollection::from_file`] and [`LutResolver::new`]. + fn new(config: AppConfig) -> Result { + let datasets = DatasetCollection::from_file( + &config.resistor_dataset, + &config.capacitor_dataset, + &config.inductor_dataset, + )?; + + let resolver: Box = match config.resolver { + AppResolver::Lut => Box::new(LutResolver::new(&datasets)?), + AppResolver::Bfs => Box::new(BfsResolver::new(datasets)), + }; + + Ok(Self { resolver }) + } + + /// Run the app. + fn run(&self) -> Result<(), LcrConnError> { + println!("LCR Connector"); + println!("Type \"help\" for more info. Type \"exit\" to quit."); + self.op_main()?; + Ok(()) + } + + // ======================================================================== + // Subcommand Processors + // ======================================================================== + + fn op_main(&self) -> Result<(), LcrConnError> { + loop { + match self.accept_command(parse_main_cmd) { + MainCmd::Query => self.op_query()?, + MainCmd::Help => { + println!("LCR Connector Help:"); + println!(); + println!("query: do a query."); + println!("help: show all command."); + println!("exit: exit this app."); + } + MainCmd::Exit => break, + } + } + Ok(()) + } + + fn op_query(&self) -> Result<(), LcrConnError> { + // collecting request infos + println!("What are you connecting?"); + println!("r: resistor"); + println!("l: inductor"); + println!("c: capacitor"); + let device_kind = self + .accept_command(QueryDeviceChoice::parse) + .to_device_kind(); + + println!("Your target value?"); + println!("Example: \"2.1k\", \"0.75m\", \"3.2M\" and etc."); + let target_value = self.accept_device_value(); + + println!("Your tolerance?"); + println!("It can be absolute value like \"2.1k\"."); + println!("Or relative value to your target value like \"19.5%\"."); + let tolerance = self.accept_device_value_tolerance(target_value); + + println!("How to sort result?"); + println!("a: more accuracy"); + println!("l: less component"); + let response_priority = self + .accept_command(QuerySortPriority::parse) + .to_response_priority(); + + println!("How may result are you expected?"); + let count_limit = self.accept_count_value(); + + // build request and ask resolver + let request = Request::new( + device_kind, + target_value, + tolerance, + response_priority, + count_limit, + )?; + let response = self.resolver.resolve(&request)?; + + // use page viewer to show result + self.op_page_viewer(&response)?; + + Ok(()) + } + + fn op_page_viewer(&self, response: &Response) -> Result<(), LcrConnError> { + let cnt = response.len(); + if cnt == 0 { + println!("Sorry, no result!"); + println!("Please consider adjusting your requirements and try again."); + return Ok(()); + } + + const ITEMS_PER_PAGE: usize = 10; + let all_page = cnt / ITEMS_PER_PAGE; + let mut current_page = 0usize; + + loop { + // print list + for i in 0..ITEMS_PER_PAGE - 1 { + // build index and check it + let index = current_page * (ITEMS_PER_PAGE - 1) + i; + if index >= cnt { + continue; + } + // and print it + illustrate_response(response, index)?; + } + + // print page footer + println!(); + println!("Page {} of {}.", current_page + 1, all_page + 1); + println!("f: previous page. b: next page. q: quit this viewer."); + // check command + match self.accept_command(PageViewerCmd::parse) { + PageViewerCmd::PreviousPage => current_page = current_page.saturating_sub(1), + PageViewerCmd::NextPage => current_page = all_page.min(current_page + 1), + PageViewerCmd::Quit => break, + } + } + + Ok(()) + } + + // ======================================================================== + // Command Utilities + // ======================================================================== + + /// Accept a command from the user. + /// + /// Loops until a valid command is entered. + fn accept_command(&self, parser: impl Fn(&str) -> Option) -> T { + loop { + self.show_prompt_arrow(); + let words = read_line(); + if words.is_empty() { + continue; + } + + match parser(&words) { + Some(cmd) => return cmd, + None => println!("Unknown command, please try again."), + } + } + } + + /// Accept a count value from the user. + fn accept_count_value(&self) -> usize { + loop { + self.show_prompt_arrow(); + let words = read_line(); + if words.is_empty() { + continue; + } + + match words.parse::() { + Ok(value) => { + if value > MAX_RESPONSE_CNT || value == 0 { + println!("Wrong value, please try again."); + } else { + return value; + } + } + Err(_) => { + println!("Wrong value, please try again."); + } + } + } + } + + /// Accept a device value from the user. + fn accept_device_value(&self) -> f64 { + loop { + self.show_prompt_arrow(); + let words = read_line(); + if words.is_empty() { + continue; + } + + let value = parse_human_readable_value(&words); + match value { + Some(v) => return v, + None => println!("Wrong value, please try again."), + } + } + } + + /// Accept a tolerance value from the user. + /// + /// The tolerance can be an absolute value (like "2.1k") or a percentage + /// relative to the target value (like "19.5%"). + fn accept_device_value_tolerance(&self, target_value: f64) -> f64 { + loop { + self.show_prompt_arrow(); + let words = read_line(); + if words.is_empty() { + continue; + } + + let value: Option = if let Some(pct_str) = words.strip_suffix('%') { + let value = parse_plain_float(pct_str, |x| *x >= 0.0 && *x <= 100.0); + value.map(|v| v / 100.0 * target_value) + } else { + parse_human_readable_value(&words) + }; + + match value { + Some(v) => return v, + None => println!("Wrong value, please try again."), + } + } + } + + fn show_prompt_arrow(&self) { + print!("> "); + io::stdout().flush().expect("Failed to flush stdout"); + } +} + +// ============================================================================ +// Entry point +// ============================================================================ + +fn main() { + let args = Args::parse(); + let config = AppConfig::from(args); + + let app = match App::new(config) { + Ok(app) => app, + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + }; + + if let Err(e) = app.run() { + eprintln!("Error: {}", e); + std::process::exit(1); + } } diff --git a/kernel/lcrconn/Cargo.toml b/kernel/lcrconn/Cargo.toml index 0166b33..9a58fcc 100644 --- a/kernel/lcrconn/Cargo.toml +++ b/kernel/lcrconn/Cargo.toml @@ -5,3 +5,5 @@ edition = "2024" [dependencies] thiserror = { workspace = true } +strum = "=0.28.0" +strum_macros = "=0.28.0" diff --git a/kernel/lcrconn/src/common.rs b/kernel/lcrconn/src/common.rs new file mode 100644 index 0000000..0e36c2a --- /dev/null +++ b/kernel/lcrconn/src/common.rs @@ -0,0 +1,512 @@ +use strum::IntoEnumIterator; +use strum_macros::EnumIter; +use thiserror::Error as TeError; + +// /// The error thrown by LCR Connector. +// #[derive(Debug, TeError)] +// pub enum LcrConnError { +// #[error("Device value must be greater than 0")] +// InvalidDeviceValue, + +// #[error("Third device cannot exist without second device")] +// ThirdDeviceWithoutSecond, + +// #[error("No second device")] +// NoSecondDevice, + +// #[error("No third device")] +// NoThirdDevice, + +// #[error("Invalid value {0} in dataset")] +// InvalidDatasetValue(f64), + +// #[error("Unexpected empty string in dataset item")] +// EmptyDatasetItem, + +// #[error("Duplicate item {0} in standard value list")] +// DuplicateDatasetItem(String), + +// #[error("Empty standard value list is not allowed")] +// EmptyDataset, + +// #[error("Invalid value {0} for target value in request")] +// InvalidTargetValue(f64), + +// #[error("Invalid value {0} for tolerance in request")] +// InvalidTolerance(f64), + +// #[error("Too large or too less value {0} for response count limit in request")] +// InvalidCountLimit(usize), + +// #[error("Invalid human readable value: {0}")] +// InvalidHumanReadableValue(String), + +// #[error(transparent)] +// Io(#[from] std::io::Error), +// } + +/// 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 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. + /// + /// # Panics + /// + /// This function will panic if given device value is equal or lower than zero. + pub fn new(device_value: f64, joint_kind: JointKind) -> Self { + // Make sure value is greater than zero. + assert!( + device_value > 0f64, + "given device value {} should greater than zero", + device_value + ); + // Okey, build and return self + Self { + device_value, + joint_kind, + } + } + + /// Compute the joint value. + /// + /// # Arguments + /// + /// * `value` - The value computed from previous devices. + /// * `device_kind` - The kind of the device. + /// + /// # Returns + /// + /// The joint value computed. + /// + /// # Errors + /// + /// Returns [`LcrConnError::InvalidDeviceValue`] if any device value is not greater than 0. + pub fn compute(&self, value: f64, device_kind: DeviceKind) -> Result { + if self.device_value <= 0.0 || value <= 0.0 { + return Err(LcrConnError::InvalidDeviceValue); + } + + // 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 = if device_kind == DeviceKind::Capacitor { + self.joint_kind.flip() + } else { + self.joint_kind + }; + + Ok(match joint_kind { + JointKind::Series => self.device_value + value, + JointKind::Parallel => (self.device_value * value) / (self.device_value + value), + }) + } + + /// 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 scale of devices in the circuit. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum CircuitDeviceScale { + /// One device. + One, + /// Two devices. + Two, + /// Three devices. + Three, +} + +impl CircuitDeviceScale { + /// Convert circuit device scale to device count. + /// + /// # Returns + /// + /// The device count. + pub fn to_device_count(self) -> usize { + match self { + CircuitDeviceScale::One => 1, + CircuitDeviceScale::Two => 2, + CircuitDeviceScale::Three => 3, + } + } +} + +/// 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, + /// The third device and its joint property. + third_device_subckt: Option, +} + +impl Circuit { + /// Initialize the circuit. + /// + /// # Arguments + /// + /// * `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. + /// + /// # Errors + /// + /// Returns [`LcrConnError::ThirdDeviceWithoutSecond`] if a third device is provided + /// without a second device. + pub fn new( + first_device_value: f64, + second_device_subckt: Option, + third_device_subckt: Option, + ) -> Result { + if second_device_subckt.is_none() && third_device_subckt.is_some() { + return Err(LcrConnError::ThirdDeviceWithoutSecond); + } + + 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) -> Self { + Self { + first_device_value: device1_value, + second_device_subckt: None, + third_device_subckt: None, + } + } + + /// Create a circuit from two devices. + pub fn from_two_devices( + device1_value: f64, + device2_value: f64, + device2_joint: JointKind, + ) -> Self { + Self { + first_device_value: device1_value, + second_device_subckt: Some(SubCircuit::new(device2_value, device2_joint)), + third_device_subckt: 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, + ) -> Self { + Self { + first_device_value: device1_value, + second_device_subckt: Some(SubCircuit::new(device2_value, device2_joint)), + third_device_subckt: Some(SubCircuit::new(device3_value, device3_joint)), + } + } + + /// Compute the circuit value. + /// + /// # Arguments + /// + /// * `device_kind` - The kind of the device. + /// + /// # Returns + /// + /// The circuit value. + /// + /// # Errors + /// + /// Returns [`LcrConnError::InvalidDeviceValue`] if any device value is not greater than 0. + pub fn compute(&self, device_kind: DeviceKind) -> Result { + if self.first_device_value <= 0.0 { + return Err(LcrConnError::InvalidDeviceValue); + } + + let mut value = self.first_device_value; + if let Some(subckt) = &self.second_device_subckt { + value = subckt.compute(value, device_kind)?; + } else { + return Ok(value); + } + if let Some(subckt) = &self.third_device_subckt { + value = subckt.compute(value, device_kind)?; + } + 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. + /// + /// # Errors + /// + /// Returns [`LcrConnError::NoSecondDevice`] if there is no second device. + pub fn second_device_joint(&self) -> Result { + self.second_device_subckt + .map(|s| s.joint_kind()) + .ok_or(LcrConnError::NoSecondDevice) + } + + /// Get the value of the second device. + /// + /// # Errors + /// + /// Returns [`LcrConnError::NoSecondDevice`] if there is no second device. + pub fn second_device_value(&self) -> Result { + self.second_device_subckt + .map(|s| s.device_value()) + .ok_or(LcrConnError::NoSecondDevice) + } + + /// Get the joint kind of the third device. + /// + /// # Errors + /// + /// Returns [`LcrConnError::NoThirdDevice`] if there is no third device. + pub fn third_device_joint(&self) -> Result { + self.third_device_subckt + .map(|s| s.joint_kind()) + .ok_or(LcrConnError::NoThirdDevice) + } + + /// Get the value of the third device. + /// + /// # Errors + /// + /// Returns [`LcrConnError::NoThirdDevice`] if there is no third device. + pub fn third_device_value(&self) -> Result { + self.third_device_subckt + .map(|s| s.device_value()) + .ok_or(LcrConnError::NoThirdDevice) + } +} + +/// The helper for circuit value computation. +#[derive(Clone, Debug)] +pub struct CircuitValueTrait { + /// The kind of the device. + device_kind: DeviceKind, + /// The target value. + target_value: f64, +} + +impl CircuitValueTrait { + pub fn new(device_kind: DeviceKind, target_value: f64) -> Self { + Self { + device_kind, + target_value, + } + } + + /// The value of this circuit. + /// + /// # Arguments + /// + /// * `circuit` - The circuit for computation. + /// + /// # Returns + /// + /// The value. + /// + /// # Errors + /// + /// See [`Circuit::compute`]. + pub fn value(&self, circuit: &Circuit) -> Result { + circuit.compute(self.device_kind) + } + + /// 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. + /// + /// # Arguments + /// + /// * `circuit` - The circuit for computation. + /// * `value` - The value of the circuit computed by the [`value`](Self::value) method + /// for reducing computation steps, or `None` if you request this method to compute the value. + /// + /// # Returns + /// + /// The signed difference. + /// + /// # Errors + /// + /// See [`Circuit::compute`]. + pub fn difference(&self, circuit: &Circuit, value: Option) -> Result { + let value = match value { + Some(v) => v, + None => self.value(circuit)?, + }; + Ok(value - self.target_value) + } + + /// The unsigned difference between the target value and the value of this circuit. + /// + /// # Arguments + /// + /// * `circuit` - The circuit for computation. + /// * `value` - The value of the circuit computed by the [`value`](Self::value) method + /// for reducing computation steps, or `None` if you request this method to compute the value. + /// * `difference` - The difference of the circuit computed by the + /// [`difference`](Self::difference) method for reducing computation steps, + /// or `None` if you request this method to compute the difference. + /// + /// # Returns + /// + /// The unsigned difference. + /// + /// # Errors + /// + /// See [`Circuit::compute`]. + pub fn unsigned_difference( + &self, + circuit: &Circuit, + value: Option, + difference: Option, + ) -> Result { + let diff = match difference { + Some(d) => d, + None => self.difference(circuit, value)?, + }; + Ok(diff.abs()) + } + + /// 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. + /// + /// # Arguments + /// + /// * `circuit` - The circuit for computation. + /// * `value` - The value of the circuit computed by the [`value`](Self::value) method + /// for reducing computation steps, or `None` if you request this method to compute the value. + /// * `difference` - The difference of the circuit computed by the + /// [`difference`](Self::difference) method for reducing computation steps, + /// or `None` if you request this method to compute the difference. + /// + /// # Returns + /// + /// The signed relative difference. + /// + /// # Errors + /// + /// See [`Circuit::compute`]. + pub fn relative_difference( + &self, + circuit: &Circuit, + value: Option, + difference: Option, + ) -> Result { + let diff = match difference { + Some(d) => d, + None => self.difference(circuit, value)?, + }; + Ok(diff / self.target_value) + } + + /// The unsigned relative difference between the target value and the value of this circuit. + /// + /// # Arguments + /// + /// * `circuit` - The circuit for computation. + /// * `value` - The value of the circuit computed by the [`value`](Self::value) method + /// for reducing computation steps, or `None` if you request this method to compute the value. + /// * `difference` - The difference of the circuit computed by the + /// [`difference`](Self::difference) method for reducing computation steps, + /// or `None` if you request this method to compute the difference. + /// * `relative_difference` - The relative difference of the circuit computed by the + /// [`relative_difference`](Self::relative_difference) method for reducing computation steps, + /// or `None` if you request this method to compute the relative difference. + /// + /// # Returns + /// + /// The unsigned relative difference. + /// + /// # Errors + /// + /// See [`Circuit::compute`]. + pub fn unsigned_relative_difference( + &self, + circuit: &Circuit, + value: Option, + difference: Option, + relative_difference: Option, + ) -> Result { + let rel_diff = match relative_difference { + Some(rd) => rd, + None => self.relative_difference(circuit, value, difference)?, + }; + Ok(rel_diff.abs()) + } +} diff --git a/kernel/lcrconn/src/dataset.rs b/kernel/lcrconn/src/dataset.rs new file mode 100644 index 0000000..0dd7c5e --- /dev/null +++ b/kernel/lcrconn/src/dataset.rs @@ -0,0 +1,445 @@ +use std::collections::HashSet; +use std::path::Path; + +use crate::common::LcrConnError; + +/// An item in the dataset. +#[derive(Clone, Debug)] +pub struct DatasetItem { + /// The actual value of this item. + pub value: f64, + /// The string form of this value given from original input for re-saving. + pub str_value: String, +} + +impl DatasetItem { + /// Create a new dataset item with validation. + /// + /// # Errors + /// + /// Returns [`LcrConnError::InvalidDatasetValue`] if the value is not greater than 0. + /// Returns [`LcrConnError::EmptyDatasetItem`] if the string value is empty. + pub fn new(value: f64, str_value: String) -> Result { + if value <= 0.0 { + return Err(LcrConnError::InvalidDatasetValue(value)); + } + if str_value.is_empty() { + return Err(LcrConnError::EmptyDatasetItem); + } + Ok(Self { value, str_value }) + } +} + +/// A list holding available standard values for resistor, capacitor or inductor. +/// +/// Standard values is a collection of all possible values of specific device manufactured +/// by electronic factory. In reality, it also can be replaced by all possible values of +/// specific device provided by your laboratory. For example, your laboratory only provide +/// resistor with 100 Ohm and 4.7k Ohm. This list will only contain 100 and 4.7k. +pub struct Dataset { + /// A list of available device gauge values. + items: Vec, +} + +impl Dataset { + /// Create a dataset from an iterable of stringified values. + /// + /// # Errors + /// + /// Returns [`LcrConnError::DuplicateDatasetItem`] if duplicate values are found. + /// Returns [`LcrConnError::EmptyDataset`] if the iterable produces no items. + /// Returns [`LcrConnError::InvalidHumanReadableValue`] if a value cannot be parsed. + pub fn from_iterable(str_values: I) -> Result + where + I: IntoIterator, + S: Into, + { + // Check string form value one by one + let mut items: Vec = Vec::new(); + let mut seen: HashSet = HashSet::new(); + + for str_value_raw in str_values { + let str_value = str_value_raw.into(); + // Try parsing value + let value = from_human_readable_value(&str_value)?; + // Check and update set + if !seen.insert(value) { + return Err(LcrConnError::DuplicateDatasetItem(str_value)); + } + // Add into result + items.push(DatasetItem::new(value, str_value)?); + } + + // Check empty case + if items.is_empty() { + return Err(LcrConnError::EmptyDataset); + } + + // Ok, assign it + Ok(Self { items }) + } + + /// Load a dataset from a block of text. + /// + /// Each non-empty line (after trimming whitespace) is treated as a value. + /// + /// # Errors + /// + /// See [`Dataset::from_iterable`]. + pub fn from_text(text: &str) -> Result { + let lines: Vec = text + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect(); + Self::from_iterable(lines) + } + + /// Load a dataset from a file. + /// + /// # Errors + /// + /// Returns [`LcrConnError::Io`] if the file cannot be read. + /// See [`Dataset::from_iterable`] for other errors. + pub fn from_file(path: impl AsRef) -> Result { + let text = std::fs::read_to_string(path)?; + Self::from_text(&text) + } + + /// The preset dataset for resistors. + /// + /// # Errors + /// + /// See [`Dataset::from_iterable`]. + pub fn resistor_preset() -> Result { + Self::from_iterable([ + "100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", + "4.7k", "6.8k", "10k", "47k", "100k", "1M", + ]) + } + + /// The preset dataset for capacitors. + /// + /// # Errors + /// + /// See [`Dataset::from_iterable`]. + pub fn capacitor_preset() -> Result { + Self::from_iterable([ + "10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p", + "1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u", + ]) + } + + /// The preset dataset for inductors. + /// + /// # Errors + /// + /// See [`Dataset::from_iterable`]. + pub fn inductor_preset() -> Result { + Self::from_iterable([ + "0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u", + "8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u", + ]) + } + + /// Get the string form of all values joined by newlines. + pub fn save_text(&self) -> String { + self.items + .iter() + .map(|i| i.str_value.as_str()) + .collect::>() + .join("\n") + } + + /// Save all values to a file. + /// + /// # Errors + /// + /// Returns [`LcrConnError::Io`] if the file cannot be written. + pub fn save_file(&self, path: impl AsRef) -> Result<(), LcrConnError> { + std::fs::write(path, self.save_text())?; + Ok(()) + } + + /// Get the available standard values as an iterator of `f64`. + pub fn values(&self) -> impl Iterator + '_ { + self.items.iter().map(|i| i.value) + } + + /// Get the underlying dataset items as a slice. + pub fn items(&self) -> &[DatasetItem] { + &self.items + } +} + +/// The collection holding all standard values for resistor, capacitor and inductor respectively. +pub struct DatasetCollection { + /// A list of available device gauge values for resistor. + resistor: Dataset, + /// A list of available device gauge values for capacitor. + capacitor: Dataset, + /// A list of available device gauge values for inductor. + inductor: Dataset, +} + +impl DatasetCollection { + pub fn new(resistor: Dataset, capacitor: Dataset, inductor: Dataset) -> Self { + Self { + resistor, + capacitor, + inductor, + } + } + + /// Load the standard values for resistor, capacitor and inductor respectively from iterables. + /// + /// # Arguments + /// + /// * `resistor` - The iterable to load available standard values for resistor. + /// * `capacitor` - The iterable to load available standard values for capacitor. + /// * `inductor` - The iterable to load available standard values for inductor. + /// + /// # Errors + /// + /// See [`Dataset::from_iterable`]. + pub fn from_iterable( + resistor: I1, + capacitor: I2, + inductor: I3, + ) -> Result + where + I1: IntoIterator, + S1: Into, + I2: IntoIterator, + S2: Into, + I3: IntoIterator, + S3: Into, + { + Ok(Self { + resistor: Dataset::from_iterable(resistor)?, + capacitor: Dataset::from_iterable(capacitor)?, + inductor: Dataset::from_iterable(inductor)?, + }) + } + + /// Load the standard values from strings. + /// + /// # Arguments + /// + /// * `resistor` - The string to load available standard values for resistor. + /// * `capacitor` - The string to load available standard values for capacitor. + /// * `inductor` - The string to load available standard values for inductor. + /// + /// # Errors + /// + /// See [`Dataset::from_text`]. + pub fn from_text(resistor: &str, capacitor: &str, inductor: &str) -> Result { + Ok(Self { + resistor: Dataset::from_text(resistor)?, + capacitor: Dataset::from_text(capacitor)?, + inductor: Dataset::from_text(inductor)?, + }) + } + + /// Load the standard values from files. + /// + /// # Arguments + /// + /// * `resistor` - The file to load available standard values for resistor. + /// * `capacitor` - The file to load available standard values for capacitor. + /// * `inductor` - The file to load available standard values for inductor. + /// + /// # Errors + /// + /// See [`Dataset::from_file`]. + pub fn from_file( + resistor: impl AsRef, + capacitor: impl AsRef, + inductor: impl AsRef, + ) -> Result { + Ok(Self { + resistor: Dataset::from_file(resistor)?, + capacitor: Dataset::from_file(capacitor)?, + inductor: Dataset::from_file(inductor)?, + }) + } + + /// The preset dataset collection for all devices. + /// + /// # Errors + /// + /// See [`Dataset::from_iterable`]. + pub fn devices_preset() -> Result { + Ok(Self { + resistor: Dataset::resistor_preset()?, + capacitor: Dataset::capacitor_preset()?, + inductor: Dataset::inductor_preset()?, + }) + } + + /// Get the string form of all values. + /// + /// # Returns + /// + /// A tuple of strings for resistor, capacitor and inductor respectively. + pub fn save_text(&self) -> (String, String, String) { + ( + self.resistor.save_text(), + self.capacitor.save_text(), + self.inductor.save_text(), + ) + } + + /// Save all values to files. + /// + /// # Arguments + /// + /// * `resistor` - The file to save available standard values for resistor. + /// * `capacitor` - The file to save available standard values for capacitor. + /// * `inductor` - The file to save available standard values for inductor. + /// + /// # Errors + /// + /// Returns [`LcrConnError::Io`] if any file cannot be written. + pub fn save_file( + &self, + resistor: impl AsRef, + capacitor: impl AsRef, + inductor: impl AsRef, + ) -> Result<(), LcrConnError> { + self.resistor.save_file(resistor)?; + self.capacitor.save_file(capacitor)?; + self.inductor.save_file(inductor)?; + Ok(()) + } + + /// Get the dataset for resistor. + pub fn resistor(&self) -> &Dataset { + &self.resistor + } + + /// Get the dataset for capacitor. + pub fn capacitor(&self) -> &Dataset { + &self.capacitor + } + + /// Get the dataset for inductor. + pub fn inductor(&self) -> &Dataset { + &self.inductor + } +} + +/// Convert human readable value to float. +/// +/// # Arguments +/// +/// * `strl` - The human readable value. +/// +/// # Returns +/// +/// The parsed float value. +/// +/// # Errors +/// +/// Returns [`LcrConnError::InvalidHumanReadableValue`] if the input string is not a valid number. +pub fn from_human_readable_value(strl: &str) -> Result { + let strl = strl.trim(); + + let (num_part, multiplier) = if let Some(stripped) = strl.strip_suffix('n') { + (stripped, 1e-12) + } else if let Some(stripped) = strl.strip_suffix('p') { + (stripped, 1e-9) + } else if let Some(stripped) = strl.strip_suffix('u') { + (stripped, 1e-6) + } else if let Some(stripped) = strl.strip_suffix('m') { + (stripped, 1e-3) + } else if let Some(stripped) = strl.strip_suffix('k') { + (stripped, 1e3) + } else if let Some(stripped) = strl.strip_suffix('M') { + (stripped, 1e6) + } else if let Some(stripped) = strl.strip_suffix('G') { + (stripped, 1e9) + } else { + (strl, 1.0) + }; + + num_part + .parse::() + .map(|v| v * multiplier) + .map_err(|_| LcrConnError::InvalidHumanReadableValue(strl.to_string())) +} + +/// The unit scale for human readable value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UnitScale { + NanoLower, + Nano, + Micro, + Milli, + None, + Kilo, + Mega, + Giga, + GigaHigher, +} + +/// Get the unit scale of human readable value. +/// +/// # Arguments +/// +/// * `v` - The value. +/// +/// # Returns +/// +/// The unit scale. +pub fn get_human_readable_value_scale(v: f64) -> UnitScale { + let v = v.abs(); + if v < 1e-12 { + UnitScale::NanoLower + } else if v < 1e-9 { + UnitScale::Nano + } else if v < 1e-6 { + UnitScale::Micro + } else if v < 1e-3 { + UnitScale::Milli + } else if v < 1e3 { + UnitScale::None + } else if v < 1e6 { + UnitScale::Kilo + } else if v < 1e9 { + UnitScale::Mega + } else if v < 1e12 { + UnitScale::Giga + } else { + UnitScale::GigaHigher + } +} + +/// Convert float value to human readable value. +/// +/// # Arguments +/// +/// * `v` - The float value. +/// +/// # Returns +/// +/// The human readable value. +pub fn to_human_readable_value(v: f64) -> String { + let scale = get_human_readable_value_scale(v); + match scale { + UnitScale::NanoLower => format!("{:+.4e} n", v / 1e-12), + UnitScale::Nano => format!("{:+.4f} p", v / 1e-9), + UnitScale::Micro => format!("{:+.4f} u", v / 1e-6), + UnitScale::Milli => format!("{:+.4f} m", v / 1e-3), + UnitScale::None => { + // YYC MARK: + // The space of this format string is by design + // for keeping the same style with other format strings. + format!("{:+.4} ", v) + } + UnitScale::Kilo => format!("{:+.4f} k", v / 1e3), + UnitScale::Mega => format!("{:+.4f} M", v / 1e6), + UnitScale::Giga => format!("{:+.4f} G", v / 1e9), + UnitScale::GigaHigher => format!("{:+.4e} G", v / 1e9), + } +} diff --git a/kernel/lcrconn/src/lib.rs b/kernel/lcrconn/src/lib.rs index b93cf3f..cddbfb5 100644 --- a/kernel/lcrconn/src/lib.rs +++ b/kernel/lcrconn/src/lib.rs @@ -1,14 +1,14 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub mod common; +pub mod dataset; +pub mod query; +pub mod resolver; -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub use common::{ + Circuit, CircuitDeviceScale, CircuitValueTrait, DeviceKind, JointKind, LcrConnError, SubCircuit, +}; +pub use dataset::{ + from_human_readable_value, get_human_readable_value_scale, to_human_readable_value, Dataset, + DatasetCollection, DatasetItem, UnitScale, +}; +pub use query::{Request, Response, ResponseItem, ResponsePriority, MAX_RESPONSE_CNT}; +pub use resolver::{BfsResolver, LutResolver, Resolver}; diff --git a/kernel/lcrconn/src/query.rs b/kernel/lcrconn/src/query.rs new file mode 100644 index 0000000..20d3acb --- /dev/null +++ b/kernel/lcrconn/src/query.rs @@ -0,0 +1,256 @@ +use std::cmp::Ordering; +use std::ops::Index; + +use crate::common::{Circuit, CircuitValueTrait, DeviceKind, LcrConnError}; + +/// The priority of the result. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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; + +/// All request information for the resolver. +#[derive(Clone, Debug)] +pub struct Request { + /// The kind of device to resolve. + pub device_kind: DeviceKind, + /// The target value of the device. + pub target_value: f64, + /// The tolerance of the device in absolute value. + pub tolerance: f64, + /// The priority principle when sorting response items. + pub response_priority: ResponsePriority, + /// The limited count of results. + pub count_limit: usize, +} + +impl Request { + /// Create a new request with validation. + /// + /// # Errors + /// + /// Returns [`LcrConnError::InvalidTargetValue`] if the target value is not greater than 0. + /// Returns [`LcrConnError::InvalidTolerance`] if the tolerance is negative. + /// Returns [`LcrConnError::InvalidCountLimit`] if the count limit is 0 or exceeds + /// [`MAX_RESPONSE_CNT`]. + pub fn new( + device_kind: DeviceKind, + target_value: f64, + tolerance: f64, + response_priority: ResponsePriority, + count_limit: usize, + ) -> Result { + if target_value <= 0.0 { + return Err(LcrConnError::InvalidTargetValue(target_value)); + } + if tolerance < 0.0 { + return Err(LcrConnError::InvalidTolerance(tolerance)); + } + if count_limit == 0 || count_limit > MAX_RESPONSE_CNT { + return Err(LcrConnError::InvalidCountLimit(count_limit)); + } + Ok(Self { + device_kind, + target_value, + tolerance, + response_priority, + count_limit, + }) + } +} + +/// The possible solution given by the resolver. +#[derive(Clone, Debug)] +pub struct ResponseItem { + /// The circuit of this response item. + circuit: Circuit, + /// The device count of this circuit. + device_count: usize, + /// The value of this circuit. + value: f64, + /// The signed difference between the target value and the value of this circuit. + /// + /// Positive value indicates that the value of this circuit is greater than the target value. + /// Negative value indicates that the value of this circuit is less than the target value. + difference: f64, + /// The unsigned difference between the target value and the value of this circuit. + unsigned_difference: f64, + /// The signed relative difference between the target value and the value of this circuit. + /// + /// Positive value indicates that the value of this circuit is greater than the target value. + /// Negative value indicates that the value of this circuit is less than the target value. + relative_difference: f64, + /// The unsigned relative difference between the target value and the value of this circuit. + unsigned_relative_difference: f64, +} + +impl ResponseItem { + /// Create a new response item by computing all values eagerly. + /// + /// # Errors + /// + /// See [`CircuitValueTrait::value`]. + pub fn new(circuit: Circuit, cv_trait: &CircuitValueTrait) -> Result { + let value = cv_trait.value(&circuit)?; + let difference = cv_trait.difference(&circuit, Some(value))?; + let unsigned_difference = cv_trait.unsigned_difference(&circuit, None, Some(difference))?; + let relative_difference = cv_trait.relative_difference(&circuit, None, Some(difference))?; + let unsigned_relative_difference = + cv_trait.unsigned_relative_difference(&circuit, None, None, Some(relative_difference))?; + let device_count = circuit.device_scale().to_device_count(); + + Ok(Self { + circuit, + device_count, + value, + difference, + unsigned_difference, + relative_difference, + unsigned_relative_difference, + }) + } + + /// 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.device_count + } + + /// The value of this circuit. + pub fn value(&self) -> f64 { + self.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.difference + } + + /// The unsigned difference between the target value and the value of this circuit. + pub fn unsigned_difference(&self) -> f64 { + self.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.relative_difference + } + + /// The unsigned relative difference between the target value and the value of this circuit. + pub fn unsigned_relative_difference(&self) -> f64 { + self.unsigned_relative_difference + } +} + +/// 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, +} + +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( + request: &Request, + candidates: impl IntoIterator, + ) -> Result { + let cv_trait = CircuitValueTrait::new(request.device_kind, request.target_value); + + let mut items: Vec = candidates + .into_iter() + .map(|c| ResponseItem::new(c, &cv_trait)) + .collect::>()?; + + // Sort by different strategy + match request.response_priority { + ResponsePriority::LessDevices => { + items.sort_by(|a, b| { + a.device_count + .cmp(&b.device_count) + .then_with(|| { + a.unsigned_difference + .partial_cmp(&b.unsigned_difference) + .unwrap_or(Ordering::Equal) + }) + }); + } + ResponsePriority::MoreAccuracy => { + items.sort_by(|a, b| { + a.unsigned_difference + .partial_cmp(&b.unsigned_difference) + .unwrap_or(Ordering::Equal) + }); + } + } + + // 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 { + self.sorted_items.iter() + } +} + +impl Index for Response { + type Output = ResponseItem; + + fn index(&self, index: usize) -> &Self::Output { + &self.sorted_items[index] + } +} diff --git a/kernel/lcrconn/src/resolver/bfs.rs b/kernel/lcrconn/src/resolver/bfs.rs new file mode 100644 index 0000000..dbb368a --- /dev/null +++ b/kernel/lcrconn/src/resolver/bfs.rs @@ -0,0 +1,481 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; +use std::iter::FusedIterator; + +use super::Resolver; +use crate::common::{Circuit, CircuitValueTrait, DeviceKind, JointKind, LcrConnError}; +use crate::dataset::{Dataset, DatasetCollection, DatasetItem}; +use crate::query::{Request, Response}; + +// ============================================================================ +// Lazy iterator structs for circuit generation +// ============================================================================ + +// YYC MARK: +// Some circuit are equivalent in topology. +// If we deduplicate these equaivalent circuit in building result, +// there are too complex works. +// So we should deduplicated these equivalent circuit at the beginning, +// i.e. when generating them. +// So following iterator structs are taking this job. + +/// Iterator over all possible one-device circuits without repeating equivalent topology. +pub struct OneDeviceCircuitIter<'a> { + items: &'a [DatasetItem], + pos: usize, +} + +impl<'a> OneDeviceCircuitIter<'a> { + pub fn new(items: &'a [DatasetItem]) -> Self { + Self { items, pos: 0 } + } +} + +impl Iterator for OneDeviceCircuitIter<'_> { + type Item = Circuit; + + fn next(&mut self) -> Option { + if self.pos < self.items.len() { + // Every single device is unique so we directly output them. + // This feature is insured by dataset itself. + let circuit = Circuit::from_one_device(self.items[self.pos].value); + self.pos += 1; + Some(circuit) + } else { + None + } + } +} + +impl FusedIterator for OneDeviceCircuitIter<'_> {} + +/// Iterator over all possible two-device circuits without repeating equivalent topology. +pub struct TwoDeviceCircuitIter<'a> { + items: &'a [DatasetItem], + i: usize, + j: usize, + joint_idx: usize, +} + +impl<'a> TwoDeviceCircuitIter<'a> { + pub fn new(items: &'a [DatasetItem]) -> Self { + Self { + items, + i: 0, + j: 0, + joint_idx: 0, + } + } +} + +impl Iterator for TwoDeviceCircuitIter<'_> { + type Item = Circuit; + + fn next(&mut self) -> Option { + let n = self.items.len(); + if n == 0 { + return None; + } + + loop { + if self.joint_idx < JointKind::ALL.len() { + let jk = JointKind::ALL[self.joint_idx]; + self.joint_idx += 1; + // The two devices in this circuit is always swapable, + // so we iterate them without repeating. + return Some(Circuit::from_two_devices( + self.items[self.i].value, + self.items[self.j].value, + jk, + )); + } + + // Advance to next combination + self.joint_idx = 0; + self.j += 1; + if self.j >= n { + self.i += 1; + self.j = self.i; + if self.i >= n { + return None; + } + } + } + } +} + +impl FusedIterator for TwoDeviceCircuitIter<'_> {} + +/// Iterator over three-device circuits where both joints share the same type. +/// +/// In this case, all 3 devices are swapable and are iterated without repeating. +pub struct ThreeDeviceSameJointIter<'a> { + items: &'a [DatasetItem], + i: usize, + j: usize, + k: usize, + joint_idx: usize, +} + +impl<'a> ThreeDeviceSameJointIter<'a> { + pub fn new(items: &'a [DatasetItem]) -> Self { + Self { + items, + i: 0, + j: 0, + k: 0, + joint_idx: 0, + } + } +} + +impl Iterator for ThreeDeviceSameJointIter<'_> { + type Item = Circuit; + + fn next(&mut self) -> Option { + let n = self.items.len(); + if n == 0 { + return None; + } + + loop { + if self.joint_idx < JointKind::ALL.len() { + let jk = JointKind::ALL[self.joint_idx]; + self.joint_idx += 1; + return Some(Circuit::from_three_devices( + self.items[self.i].value, + self.items[self.j].value, + jk, + self.items[self.k].value, + jk, + )); + } + + self.joint_idx = 0; + self.k += 1; + if self.k >= n { + self.j += 1; + self.k = self.j; + if self.j >= n { + self.i += 1; + self.j = self.i; + self.k = self.i; + if self.i >= n { + return None; + } + } + } + } + } +} + +impl FusedIterator for ThreeDeviceSameJointIter<'_> {} + +/// Iterator over three-device circuits where the two joint types differ. +/// +/// In this case, the first 2 devices are swapable and are iterated without repeating, +/// while the third device iterates over all values independently. +pub struct ThreeDeviceDiffJointIter<'a> { + items: &'a [DatasetItem], + i: usize, + j: usize, + k: usize, + joint_idx: usize, +} + +impl<'a> ThreeDeviceDiffJointIter<'a> { + pub fn new(items: &'a [DatasetItem]) -> Self { + Self { + items, + i: 0, + j: 0, + k: 0, + joint_idx: 0, + } + } +} + +impl Iterator for ThreeDeviceDiffJointIter<'_> { + type Item = Circuit; + + fn next(&mut self) -> Option { + let n = self.items.len(); + if n == 0 { + return None; + } + + loop { + if self.joint_idx < JointKind::ALL.len() { + let j = JointKind::ALL[self.joint_idx]; + self.joint_idx += 1; + return Some(Circuit::from_three_devices( + self.items[self.i].value, + self.items[self.j].value, + j, + self.items[self.k].value, + j.flip(), + )); + } + + self.joint_idx = 0; + self.k += 1; + if self.k >= n { + self.j += 1; + self.k = 0; + if self.j >= n { + self.i += 1; + self.j = self.i; + self.k = 0; + if self.i >= n { + return None; + } + } + } + } + } +} + +impl FusedIterator for ThreeDeviceDiffJointIter<'_> {} + +/// Type alias for the chained three-device circuit iterator. +pub type ThreeDeviceCircuitIter<'a> = std::iter::Chain< + ThreeDeviceSameJointIter<'a>, + ThreeDeviceDiffJointIter<'a>, +>; + +// ============================================================================ +// BfsItem +// ============================================================================ + +/// The entry used in BFS iteration storing circuit and value. +pub struct BfsItem { + /// The circuit represented by this item. + circuit: Circuit, + /// The computed value of the circuit. + 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. + /// + /// # Errors + /// + /// See [`CircuitValueTrait::value`]. + pub fn new(circuit: Circuit, cv_trait: &CircuitValueTrait) -> Result { + let value = cv_trait.value(&circuit)?; + let unsigned_difference = cv_trait.unsigned_difference(&circuit, Some(value))?; + Ok(Self { + circuit, + value, + unsigned_difference, + }) + } + + /// The circuit represented by this item. + pub fn circuit(&self) -> &Circuit { + &self.circuit + } + + /// The computed 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 + } +} + +// ============================================================================ +// ResultBucket +// ============================================================================ + +/// An item stored in a [`ResultBucket`]. +struct ResultBucketItem { + /// The score associated with this item. + score: 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 { + fn new(score: f64, item: BfsItem, seq: usize) -> Self { + Self { score, item, seq } + } +} + +impl PartialEq for ResultBucketItem { + fn eq(&self, other: &Self) -> bool { + self.score == other.score && self.seq == other.seq + } +} + +impl Eq for ResultBucketItem {} + +impl PartialOrd for ResultBucketItem { + fn partial_cmp(&self, other: &Self) -> Option { + 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. + match self.score.partial_cmp(&other.score) { + Some(Ordering::Equal) | None => self.seq.cmp(&other.seq), + Some(ord) => ord, + } + } +} + +/// A bounded bucket that keeps up to N entries with the smallest scores. +/// +/// When the bucket is full, inserting a new item only succeeds if its score +/// is less than the current maximum; the maximum is then evicted. +pub struct ResultBucket { + /// 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, + /// 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) -> Self { + Self { + n, + heap: BinaryHeap::new(), + counter: 0, + } + } + + /// The number of items currently in the bucket. + pub fn len(&self) -> usize { + self.heap.len() + } + + /// Whether the bucket is empty. + pub fn is_empty(&self) -> bool { + self.heap.is_empty() + } + + /// 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 { + let entry = ResultBucketItem::new(score, item, self.counter); + if self.heap.len() < self.n { + self.heap.push(entry); + self.counter += 1; + true + } else if score >= self.heap.peek().unwrap().score { + false + } else { + *self.heap.peek_mut().unwrap() = entry; + self.counter += 1; + true + } + } + + /// Consume the bucket and return all stored items. + pub fn into_items(self) -> Vec { + self.heap.into_iter().map(|entry| entry.item).collect() + } +} + +// ============================================================================ +// BfsResolver +// ============================================================================ + +/// A resolver that uses brute-force search to find the best matching circuits. +pub struct BfsResolver { + /// The datasets for all device kinds. + datasets: DatasetCollection, +} + +impl BfsResolver { + /// Create a new BFS resolver with the given datasets. + pub fn new(datasets: DatasetCollection) -> Self { + Self { datasets } + } + + /// Iterate all possible circuits with one device without repeating equivalent topology. + pub fn iter_one_device_circuit(dataset: &Dataset) -> OneDeviceCircuitIter<'_> { + OneDeviceCircuitIter::new(dataset.items()) + } + + /// Iterate all possible circuits with two devices without repeating equivalent topology. + pub fn iter_two_devices_circuit(dataset: &Dataset) -> TwoDeviceCircuitIter<'_> { + TwoDeviceCircuitIter::new(dataset.items()) + } + + /// Iterate all possible circuits with three devices without repeating equivalent topology. + pub fn iter_three_devices_circuit(dataset: &Dataset) -> ThreeDeviceCircuitIter<'_> { + ThreeDeviceSameJointIter::new(dataset.items()) + .chain(ThreeDeviceDiffJointIter::new(dataset.items())) + } + + fn pick_dataset(&self, device_kind: DeviceKind) -> &Dataset { + match device_kind { + DeviceKind::Resistor => self.datasets.resistor(), + DeviceKind::Capacitor => self.datasets.capacitor(), + DeviceKind::Inductor => self.datasets.inductor(), + } + } +} + +impl Resolver for BfsResolver { + fn resolve(&self, request: &Request) -> Result { + // Pick dataset from collection + let dataset = self.pick_dataset(request.device_kind); + + // Iterate circuit item one by one + let mut bucket = ResultBucket::new(request.count_limit); + let cv_trait = CircuitValueTrait::new(request.device_kind, request.target_value); + + let circuits = Self::iter_one_device_circuit(dataset) + .chain(Self::iter_two_devices_circuit(dataset)) + .chain(Self::iter_three_devices_circuit(dataset)); + + for circuit in circuits { + let item = BfsItem::new(circuit, &cv_trait)?; + // If circuit absolute difference is out of tolerance, skip it directly. + if item.unsigned_difference() > request.tolerance { + continue; + } + // Put it into bucket + bucket.insert(item, item.unsigned_difference()); + } + + // Return result + let circuits: Vec = bucket + .into_items() + .into_iter() + .map(BfsItem::into_circuit) + .collect(); + Response::new(request, circuits) + } +} diff --git a/kernel/lcrconn/src/resolver/lut.rs b/kernel/lcrconn/src/resolver/lut.rs new file mode 100644 index 0000000..93a2bab --- /dev/null +++ b/kernel/lcrconn/src/resolver/lut.rs @@ -0,0 +1,156 @@ +use std::cmp::Ordering; + +use super::bfs::BfsResolver; +use super::Resolver; +use crate::common::{Circuit, CircuitValueTrait, DeviceKind, LcrConnError}; +use crate::dataset::{Dataset, DatasetCollection}; +use crate::query::{Request, Response}; + +/// An item in the lookup table. +pub struct LutItem { + /// The circuit represented by this item. + circuit: Circuit, + /// The value of this circuit. + value: f64, +} + +impl LutItem { + /// Create a new LUT item by computing the circuit value. + /// + /// # Errors + /// + /// See [`Circuit::compute`]. + pub fn new(circuit: Circuit, device_kind: DeviceKind) -> Result { + let value = circuit.compute(device_kind)?; + Ok(Self { circuit, 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 + } +} + +/// A resolver that uses a lookup table to find the best matching circuit. +pub struct LutResolver { + /// The lookup table for resistors. + resistor_lut: Vec, + /// The lookup table for capacitors. + capacitor_lut: Vec, + /// The lookup table for inductors. + inductor_lut: Vec, +} + +impl LutResolver { + /// Create a new LUT resolver by building lookup tables from the given datasets. + /// + /// # Errors + /// + /// See [`LutItem::new`]. + pub fn new(datasets: &DatasetCollection) -> Result { + Ok(Self { + resistor_lut: Self::build_lut(datasets.resistor(), DeviceKind::Resistor)?, + capacitor_lut: Self::build_lut(datasets.capacitor(), DeviceKind::Capacitor)?, + inductor_lut: Self::build_lut(datasets.inductor(), DeviceKind::Inductor)?, + }) + } + + fn build_lut(dataset: &Dataset, device_kind: DeviceKind) -> Result, LcrConnError> { + let mut lut: Vec = Vec::new(); + + let circuits = BfsResolver::iter_one_device_circuit(dataset) + .chain(BfsResolver::iter_two_devices_circuit(dataset)) + .chain(BfsResolver::iter_three_devices_circuit(dataset)); + + for circuit in circuits { + lut.push(LutItem::new(circuit, device_kind)?); + } + + lut.sort_by(|a, b| a.value.partial_cmp(&b.value).unwrap_or(Ordering::Equal)); + 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, + } + } +} + +impl Resolver for LutResolver { + fn resolve(&self, request: &Request) -> Result { + let lut = self.pick_lut(request.device_kind); + let target = request.target_value; + let count_limit = request.count_limit; + let mut bucket: Vec = Vec::new(); + + // Locate the insertion point of target in the sorted LUT. + // left/right start at the two nearest neighbours and expand outward. + let idx = lut.partition_point(|item| item.value < target); + + // 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. + let mut left = idx as isize - 1; + let mut right = idx as isize; + let lut_len = lut.len() as isize; + + let cv_trait = CircuitValueTrait::new(request.device_kind, target); + + while left >= 0 || right < lut_len { + if bucket.len() >= count_limit { + break; + } + + let go_left = if left < 0 { + false + } else if right >= lut_len { + true + } else { + let left_item = &lut[left as usize]; + let left_diff = + cv_trait.unsigned_difference(left_item.circuit(), Some(left_item.value()))?; + let right_item = &lut[right as usize]; + let right_diff = cv_trait + .unsigned_difference(right_item.circuit(), Some(right_item.value()))?; + left_diff <= right_diff + }; + + let item = if go_left { + let item = &lut[left as usize]; + left -= 1; + item + } else { + let item = &lut[right as usize]; + right += 1; + item + }; + + let diff = cv_trait.unsigned_difference(item.circuit(), Some(item.value()))?; + // Since the LUT is sorted, values on each side only move further + // from target as we advance. Once one side exceeds tolerance, + // the rest of that side is guaranteed out of range — disable it. + if diff > request.tolerance { + if go_left { + left = -1; + } else { + right = lut_len; + } + continue; + } + + bucket.push(item.circuit().clone()); + } + + Response::new(request, bucket) + } +} diff --git a/kernel/lcrconn/src/resolver/mod.rs b/kernel/lcrconn/src/resolver/mod.rs new file mode 100644 index 0000000..3fb082a --- /dev/null +++ b/kernel/lcrconn/src/resolver/mod.rs @@ -0,0 +1,26 @@ +pub mod bfs; +pub mod lut; + +use crate::common::LcrConnError; +use crate::query::{Request, Response}; + +/// Abstract base trait for all resolvers. +pub trait Resolver { + /// Resolve the request and return the response. + /// + /// # Arguments + /// + /// * `request` - The request to resolve. + /// + /// # Returns + /// + /// The response containing the best matching circuits. + /// + /// # Errors + /// + /// See [`Circuit::compute`](crate::common::Circuit::compute). + fn resolve(&self, request: &Request) -> Result; +} + +pub use bfs::BfsResolver; +pub use lut::LutResolver;