diff --git a/kernel/lcrconn/src/query.rs b/kernel/lcrconn/src/query.rs index 3ec313c..ec5a213 100644 --- a/kernel/lcrconn/src/query.rs +++ b/kernel/lcrconn/src/query.rs @@ -1,12 +1,11 @@ use crate::common::{ - Circuit, CircuitCalculator, CircuitCalculatorError, DeviceKind, DeviceValueError, - validate_device_value, + 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, PartialEq, Eq)] +#[derive(Debug, Clone, Copy)] pub enum ResponsePriority { /// Less devices is the first priority. LessDevices, @@ -76,11 +75,15 @@ impl Request { } /// 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 } @@ -91,6 +94,8 @@ impl Request { } /// 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 } @@ -99,8 +104,8 @@ impl Request { /// Error occurs when building [Response] and [ResponseItem]. #[derive(Debug, TeError)] pub enum ResponseError { - #[error("failed on computing circuit properties: {0}")] - CircuitCalculator(#[from] CircuitCalculatorError), + #[error("failed on evaluating circuit: {0}")] + EvaluateCircuit(#[from] CircuitError), } /// The possible solution given by the resolver. @@ -108,39 +113,23 @@ pub enum ResponseError { pub struct ResponseItem { /// The circuit of this response item. circuit: Circuit, - /// The value of this circuit. - value: f64, - /// The signed difference. - difference: f64, - /// The unsigned difference. - unsigned_difference: f64, - /// The signed relative difference. - relative_difference: f64, - /// The unsigned relative difference. - unsigned_relative_difference: f64, + /// The evaluation result of this circuit. + circuit_evaluation: CircuitEvaluation, } impl ResponseItem { /// Create a new response item by computing all values eagerly. - pub fn new(circuit: Circuit, ccalc: &CircuitCalculator) -> Result { + fn new(circuit: Circuit, request: &Request) -> Result { // 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 value = ccalc.value(&circuit)?; - let difference = ccalc.difference(&circuit, Some(value))?; - let unsigned_difference = ccalc.unsigned_difference(&circuit, None, Some(difference))?; - let relative_difference = ccalc.relative_difference(&circuit, None, Some(difference))?; - let unsigned_relative_difference = - ccalc.unsigned_relative_difference(&circuit, None, None, Some(relative_difference))?; - + let circuit_evaluation = + circuit.evaluate_with_target(request.get_target_value(), request.get_device_kind())?; + // Build self and return Ok(Self { circuit, - value, - difference, - unsigned_difference, - relative_difference, - unsigned_relative_difference, + circuit_evaluation, }) } @@ -156,7 +145,7 @@ impl ResponseItem { /// The value of this circuit. pub fn value(&self) -> f64 { - self.value + self.circuit_evaluation.value } /// The signed difference between the target value and the value of this circuit. @@ -164,12 +153,12 @@ impl ResponseItem { /// Positive value indicates that the value of this circuit is greater than the target value. /// Negative value indicates that the value of this circuit is less than the target value. pub fn difference(&self) -> f64 { - self.difference + self.circuit_evaluation.difference } /// The unsigned difference between the target value and the value of this circuit. pub fn unsigned_difference(&self) -> f64 { - self.unsigned_difference + self.circuit_evaluation.unsigned_difference } /// The signed relative difference between the target value and the value of this circuit. @@ -177,12 +166,12 @@ impl ResponseItem { /// Positive value indicates that the value of this circuit is greater than the target value. /// Negative value indicates that the value of this circuit is less than the target value. pub fn relative_difference(&self) -> f64 { - self.relative_difference + self.circuit_evaluation.relative_difference } /// The unsigned relative difference between the target value and the value of this circuit. pub fn unsigned_relative_difference(&self) -> f64 { - self.unsigned_relative_difference + self.circuit_evaluation.unsigned_relative_difference } } @@ -211,10 +200,9 @@ impl Response { where I: Iterator, { - let ccalc = CircuitCalculator::new(request.device_kind, request.target_value)?; let mut items: Vec = candidates .into_iter() - .map(|c| ResponseItem::new(c, &ccalc)) + .map(|c| ResponseItem::new(c, request)) .collect::>()?; // Sort by different strategy @@ -222,14 +210,14 @@ impl Response { 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)) + 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)) + OrderedFloat(a.unsigned_difference()).cmp(&OrderedFloat(b.unsigned_difference())) }); } } diff --git a/kernel/lcrconn/src/resolver/bfs.rs b/kernel/lcrconn/src/resolver/bfs.rs index 7adf9d6..4671965 100644 --- a/kernel/lcrconn/src/resolver/bfs.rs +++ b/kernel/lcrconn/src/resolver/bfs.rs @@ -1,9 +1,7 @@ use super::{Resolver, ResolverError}; -use crate::common::{ - Circuit, CircuitCalculator, CircuitCalculatorError, CircuitError, DeviceKind, JointKind, -}; -use crate::spec::{SpecGroup, SpecCatalog}; +use crate::common::{Circuit, CircuitError, DeviceKind, JointKind}; use crate::query::{Request, Response, ResponseError}; +use crate::spec::{SpecCatalog, SpecGroup}; use itertools::Itertools; use ordered_float::OrderedFloat; use std::cmp::Ordering; @@ -11,15 +9,13 @@ 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 to build circuit: {0}")] - Circuit(#[from] CircuitError), - #[error("failed on computing circuit properties: {0}")] - CircuitCalculator(#[from] CircuitCalculatorError), - #[error("the size of binary heap {0} is invalid")] - BadBinHeapSize(usize), + #[error("failed on evaluating circuit: {0}")] + EvaluateCircuit(#[from] CircuitError), #[error("fail to build response: {0}")] Response(#[from] ResponseError), } @@ -38,15 +34,16 @@ pub struct BfsItem { impl BfsItem { /// Create a new BFS item by computing values eagerly. - pub fn new(circuit: Circuit, ccalc: &CircuitCalculator) -> Result { + pub fn new(circuit: Circuit, request: &Request) -> Result { // YYC MARK: // The same reason for replacing cached_property like I done in `ResponseItem`. - let value = ccalc.value(&circuit)?; - let unsigned_difference = ccalc.unsigned_difference(&circuit, Some(value), None)?; + let eval = + circuit.evaluate_with_target(request.get_target_value(), request.get_device_kind())?; + Ok(Self { circuit, - value, - unsigned_difference, + value: eval.value, + unsigned_difference: eval.unsigned_difference, }) } @@ -73,7 +70,153 @@ impl BfsItem { // endregion -// region Result Bucket +// region: BFS Resolver + +/// A resolver that uses breadth first search to find the best matching circuits. +pub struct BfsResolver { + /// The datasets for all device kinds. + datasets: 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 { + // Every single device is unique so we directly output them. + // This feature is insured by dataset 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 { + // 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 { + // 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 datasets. + pub fn new(datasets: SpecCatalog) -> Self { + Self { datasets } + } + + fn pick_specs(&self, device_kind: DeviceKind) -> &SpecGroup { + match device_kind { + DeviceKind::Resistor => self.datasets.resistor_specs(), + DeviceKind::Capacitor => self.datasets.capacitor_specs(), + DeviceKind::Inductor => self.datasets.inductor_specs(), + } + } + + fn bfs_iteration( + specs: &SpecGroup, + request: &Request, + ) -> impl Iterator> { + 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 { + // Pick dataset from collection + 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 { + Ok(self.intern_resolve(request)?) + } +} + +// 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 { @@ -128,6 +271,10 @@ impl Ord for ResultBucketItem { } } +// 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 @@ -145,10 +292,10 @@ pub struct ResultBucket { impl ResultBucket { /// Create a new bucket that holds at most `n` items. - pub fn new(n: usize) -> Result { + pub fn new(n: usize) -> Result { // Check heap size if n == 0 { - Err(BfsResolverError::BadBinHeapSize(n)) + Err(ResultBucketError::BadBinHeapSize(n)) } else { Ok(Self { n, @@ -182,6 +329,11 @@ impl ResultBucket { /// /// 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); @@ -205,129 +357,4 @@ impl ResultBucket { // endregion -/// A resolver that uses brute-force search to find the best matching circuits. -pub struct BfsResolver { - /// The datasets for all device kinds. - datasets: 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 structs are taking this job. - - /// Iterate all possible circuits with one device without repeating equivalent topology. - pub fn iter_one_device_circuit( - dataset: &SpecGroup, - ) -> impl Iterator> { - // Every single device is unique so we directly output them. - // This feature is insured by dataset itself. - dataset.specs().map(|v1| Circuit::from_one_device(v1)) - } - - /// Iterate all possible circuits with two devices without repeating equivalent topology. - pub fn iter_two_devices_circuit( - dataset: &SpecGroup, - ) -> impl Iterator> { - // The two devices in this circuit is always swapable, - // so we iterate them without repeating. - itertools::iproduct!( - dataset.specs().array_combinations_with_replacement::<2>(), - JointKind::iter() - ) - .map(|([v1, v2], j2)| Circuit::from_two_devices(v1, v2, j2)) - } - - /// Iterate all possible circuits with three devices without repeating equivalent topology. - pub fn iter_three_devices_circuit( - dataset: &SpecGroup, - ) -> impl Iterator> { - // 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!( - dataset.specs().array_combinations_with_replacement::<3>(), - JointKind::iter() - ) - .map(|([v1, v2, v3], j)| Circuit::from_three_devices(v1, v2, j, v3, j)), - // Second, if the joint type is different, then the first 2 devices are swapable. - // So we need iterate them without repeating. - itertools::iproduct!( - dataset.specs().array_combinations_with_replacement::<2>(), - dataset.specs(), - JointKind::iter() - ) - .map(|([v1, v2], v3, j)| Circuit::from_three_devices( - v1, - v2, - j, - v3, - j.flip() - )), - ) - } -} - -impl BfsResolver { - /// Create a new BFS resolver with the given datasets. - pub fn new(datasets: SpecCatalog) -> Self { - Self { datasets } - } - - fn pick_dataset(&self, device_kind: DeviceKind) -> &SpecGroup { - match device_kind { - DeviceKind::Resistor => self.datasets.resistor_specs(), - DeviceKind::Capacitor => self.datasets.capacitor_specs(), - DeviceKind::Inductor => self.datasets.inductor_specs(), - } - } - - fn bfs_iteration( - dataset: &SpecGroup, - ccalc: &CircuitCalculator, - ) -> impl Iterator> { - itertools::chain!( - BfsResolver::iter_one_device_circuit(&dataset), - BfsResolver::iter_two_devices_circuit(&dataset), - BfsResolver::iter_three_devices_circuit(&dataset) - ) - .map(|circuit| -> Result { BfsItem::new(circuit?, ccalc) }) - } - - fn intern_resolve(&self, request: &Request) -> Result { - // Pick dataset from collection - let dataset = self.pick_dataset(request.get_device_kind()); - - // Iterate circuit item one by one - let mut bucket = ResultBucket::new(request.get_count_limit())?; - let ccalc = CircuitCalculator::new(request.get_device_kind(), request.get_target_value())?; - - for item in BfsResolver::bfs_iteration(dataset, &ccalc) { - 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 { - Ok(self.intern_resolve(request)?) - } -} +// endregion diff --git a/kernel/lcrconn/src/spec.rs b/kernel/lcrconn/src/spec.rs index a413ee3..b5bf878 100644 --- a/kernel/lcrconn/src/spec.rs +++ b/kernel/lcrconn/src/spec.rs @@ -156,7 +156,7 @@ impl SpecGroup { Self::from_iterator([ "100", "220", "270", "390", "470", "680", "1k", "1.2k", "1.5k", "2.2k", "3.3k", "4.7k", "6.8k", "10k", "47k", "100k", "1M", - ]).expect("unexpected bad rated values set") + ]).expect("unexpected bad rated values preset") } /// A commonly used set of capacitor rated values. @@ -164,7 +164,7 @@ impl SpecGroup { Self::from_iterator([ "10p", "22p", "33p", "47p", "68p", "100p", "150p", "220p", "330p", "470p", "560p", "1u", "2.2u", "3.3u", "4.7u", "10u", "22u", "47u", "100u", "220u", "470u", - ]).expect("unexpected bad rated values set") + ]).expect("unexpected bad rated values preset") } /// A commonly used set of inductor rated values. @@ -172,7 +172,7 @@ impl SpecGroup { Self::from_iterator([ "0.1u", "0.15u", "0.47u", "0.68u", "1u", "1.5u", "2.2u", "3.3u", "4.7u", "6.8u", "8.2u", "10u", "15u", "22u", "33u", "47u", "68u", "100u", - ]).expect("unexpected bad rated values set") + ]).expect("unexpected bad rated values preset") } fn save(&self) -> impl Iterator { @@ -218,7 +218,7 @@ impl SpecGroup { } /// Iterate over all numeric rated values in this group. - pub fn specs(&self) -> impl Iterator + Clone { + pub fn iter(&self) -> impl Iterator + Clone { self.specs.iter().map(|i| i.value) } } diff --git a/kernel/lcrconn/tests/spec.rs b/kernel/lcrconn/tests/spec.rs new file mode 100644 index 0000000..c1518a3 --- /dev/null +++ b/kernel/lcrconn/tests/spec.rs @@ -0,0 +1,11 @@ +use lcrconn::spec; + +#[test] +fn test_spec_preset() { + // All individual preset and catalog preset should nit panic + let specs = spec::SpecGroup::resistor_preset(); + let specs = spec::SpecGroup::capacitor_preset(); + let specs = spec::SpecGroup::inductor_preset(); + + let specs = spec::SpecCatalog::devices_preset(); +}