-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create a function library for the evaluator, bring back the ICCC voltage divider #16
Changes from all commits
8ee529a
4c21de7
f950a80
aeb3f3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[workspace] | ||
members = ["kicad_rs", "kicad_functions"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
[package] | ||
name = "kicad_functions" | ||
version = "0.1.0" | ||
authors = ["Dennis Marttinen <[email protected]>"] | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
evalexpr = "6.1" | ||
resistor-calc = { git = "https://github.com/racklet/resistor-calc.git", branch = "fix-no-expr_builder-compile", default-features = false } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
use crate::util::err; | ||
use evalexpr::{EvalexprError, EvalexprResult, Value}; | ||
|
||
/// `index` retrieves tuple values based on the given index. | ||
/// - Usage: idx(<tuple>, <i>) | ||
/// - Example: idx(("a", "b", "c"), 1) -> "b" | ||
/// - Output: i:th value in the tuple (zero-indexed) | ||
pub(crate) fn index(argument: &Value) -> EvalexprResult<Value> { | ||
let args = argument.as_tuple()?; | ||
if let [target, index] = &args[..] { | ||
let index = index.as_int()?; | ||
return target | ||
.as_tuple()? | ||
.get(index as usize) | ||
.ok_or(EvalexprError::CustomMessage(format!( | ||
"index out of bounds: {}", | ||
index | ||
))) | ||
.map(|v| v.clone()); | ||
} | ||
|
||
err(&format!("unsupported argument count: {}", args.len())) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
mod idx; | ||
pub mod util; | ||
mod vdiv; | ||
|
||
use evalexpr::{EvalexprError, EvalexprResult, Value}; | ||
|
||
// Match function for all custom functions available in the kicad_rs evaluator | ||
pub fn call_function(identifier: &str, argument: &Value) -> EvalexprResult<Value> { | ||
match identifier { | ||
"idx" => idx::index(argument), | ||
"vdiv" => vdiv::voltage_divider(argument), | ||
other => Err(EvalexprError::FunctionIdentifierNotFound(other.into())), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
use evalexpr::{EvalexprError, EvalexprResult}; | ||
|
||
/// Returns an `EvalexprResult` with a `EvalexprError::CustomMessage` error | ||
pub fn err<T>(msg: &str) -> EvalexprResult<T> { | ||
Err(EvalexprError::CustomMessage(msg.into())) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
use crate::util::err; | ||
use evalexpr::{ | ||
ContextWithMutableVariables, EvalexprError, EvalexprResult, HashMapContext, Node, Value, | ||
}; | ||
use resistor_calc::{RCalc, RRes, RSeries}; | ||
use std::collections::HashSet; | ||
use std::panic::panic_any; | ||
|
||
fn parse_series(str: &str) -> EvalexprResult<&'static RSeries> { | ||
match str.trim() { | ||
"E3" => Ok(&resistor_calc::E3), | ||
"E6" => Ok(&resistor_calc::E6), | ||
"E12" => Ok(&resistor_calc::E12), | ||
"E24" => Ok(&resistor_calc::E24), | ||
"E48" => Ok(&resistor_calc::E48), | ||
"E96" => Ok(&resistor_calc::E96), | ||
_ => err(&format!("unknown resistor series: {}", str)), | ||
} | ||
} | ||
|
||
// evalexpr is not smart enough to distinguish identifiers on its own | ||
fn unique_identifiers(e: &Node) -> usize { | ||
let mut set = HashSet::new(); | ||
e.iter_variable_identifiers().for_each(|i| { | ||
set.insert(i); | ||
}); | ||
set.len() | ||
} | ||
|
||
struct VoltageDividerConfig { | ||
target: f64, | ||
expression: Node, | ||
count: usize, | ||
series: &'static RSeries, | ||
resistance_min: Option<f64>, | ||
resistance_max: Option<f64>, | ||
} | ||
|
||
impl VoltageDividerConfig { | ||
fn parse(v: &Value) -> EvalexprResult<Self> { | ||
let tuple = v.as_tuple()?; | ||
|
||
let resistance_min = tuple.get(3).map(|v| v.as_number()).transpose()?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL about transpose, nice |
||
let resistance_max = tuple.get(4).map(|v| v.as_number()).transpose()?; | ||
|
||
// TODO: Support external constants | ||
if let [target, expression, series] = &tuple[..3] { | ||
let expression = evalexpr::build_operator_tree(&expression.as_string()?)?; | ||
let count = unique_identifiers(&expression); | ||
|
||
Ok(Self { | ||
target: target.as_number()?, | ||
expression, | ||
count, | ||
series: parse_series(&series.as_string()?)?, | ||
resistance_min, | ||
resistance_max, | ||
}) | ||
} else { | ||
err(&format!("unsupported argument count: {}", tuple.len())) | ||
} | ||
} | ||
} | ||
|
||
fn calculate(config: &VoltageDividerConfig) -> Option<RRes> { | ||
let calc = RCalc::new(vec![config.series; config.count]); | ||
|
||
calc.calc(|set| { | ||
if let Some(true) = config.resistance_min.map(|r| set.sum() < r) { | ||
return None; // Sum of resistance less than minimum | ||
} | ||
|
||
if let Some(true) = config.resistance_max.map(|r| set.sum() > r) { | ||
return None; // Sum of resistance larger than maximum | ||
} | ||
|
||
// TODO: Storing this externally and using interior mutability | ||
// could be helpful to avoid reallocating on every invocation. | ||
let mut context = HashMapContext::new(); | ||
for i in 1..=config.count { | ||
context | ||
.set_value(format!("R{}", i).into(), Value::Float(set.r(i))) | ||
.unwrap(); | ||
} | ||
|
||
match config.expression.eval_with_context(&context) { | ||
Ok(v) => Some((config.target - v.as_number().unwrap()).abs()), | ||
Err(e) => match &e { | ||
EvalexprError::DivisionError { divisor: d, .. } => { | ||
if let Ok(n) = d.as_number() { | ||
if n == 0.0 { | ||
// This soft-catch may be a bit redundant. Based on some testing the | ||
// internal conversions in evalexpr cause zero values to deviate | ||
// slightly from zero, thus avoiding division by zero even if you | ||
// explicitly write a zero division into the voltage divider equation. | ||
return None; | ||
} | ||
} | ||
panic_any(e) // No graceful way to handle this from the closure | ||
} | ||
_ => panic_any(e), | ||
}, | ||
} | ||
}) | ||
} | ||
|
||
/// `voltage_divider` computes values for resistor-based voltage dividers. | ||
/// - Usage: vdiv(<target voltage>, <divider expression>, <resistor series>, (min resistance), (max resistance)) | ||
/// - Example: vdiv(5.1, "(R1+R2)/R2*0.8", "E96", 500e3, 700e3) | ||
/// - Output: (<closest voltage>, <R1 value>, <R2 value>, ...) | ||
/// There can be arbitrary many resistors in the divider, but they must be named "R1", "R2", etc. | ||
/// The computed optimal resistance values are also presented in this order. The minimal and maximal | ||
/// resistance limits are optional parameters, and only consider the sum of the resistances of all | ||
/// resistors defined in the expression. | ||
pub(crate) fn voltage_divider(argument: &Value) -> EvalexprResult<Value> { | ||
let config = VoltageDividerConfig::parse(argument)?; | ||
if let Some(res) = calculate(&config) { | ||
// Take the first result, these are ordered by increasing error | ||
if let Some((v, set)) = res.iter().next() { | ||
let voltage = config.target + fixed_to_floating(*v); | ||
let mut tuple = vec![Value::from(voltage)]; | ||
for i in 1..=config.count { | ||
tuple.push(Value::from(set.r(i))); | ||
} | ||
return Ok(Value::from(tuple)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does evalexpr support tuples natively, or are they arrays? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "tuple" is just a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, thanks for the info |
||
} | ||
} | ||
|
||
err(&format!("no solution found: {}", argument)) | ||
} | ||
|
||
// resistor_calc outputs error quantities as "fixed point" numbers by multiplying | ||
// a float by 1e9, rounding the result and converting to u64. We need to undo | ||
// that procedure here. | ||
fn fixed_to_floating(value: u64) -> f64 { | ||
(value as f64) / 1e9 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -210,8 +210,10 @@ pub fn parse_components(kicad_sch: &SchematicFile) -> DynamicResult<Vec<Componen | |
}, | ||
// Get the main key value. It is ok if it's empty, too. | ||
value: Value::parse(get_component_attr_mapped(&comp, main_key, &m).or_empty_str()), | ||
// As this field corresponds to the main key expression attribute, we can get the expression directly | ||
expression: f.value.clone(), | ||
// As this field corresponds to the main key expression attribute, we can get the | ||
// expression directly. The Eeschema file escapes the field though, so it needs to | ||
// be unescaped here first. | ||
expression: unescape(&f.value), | ||
// Optionally, get the unit and a comment | ||
unit: get_component_attr_mapped(&comp, &unit_key, &m), | ||
comment: get_component_attr_mapped(&comp, &comment_key, &m), | ||
|
@@ -360,3 +362,41 @@ impl<T: AsRef<str>> SplitCharN for Option<T> { | |
} | ||
} | ||
} | ||
|
||
fn unescape(s: &str) -> String { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a "custom" unescaping function, or is it a "conventional" like done for e.g. URL escaping? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Its custom, I just cobbled it together since |
||
let mut res = String::new(); | ||
let mut prev = 0 as char; | ||
|
||
let mut iter = s.chars().peekable(); | ||
while let Some(c) = iter.next() { | ||
let mut next_prev = c; | ||
match c { | ||
'\\' => { | ||
if prev == '\\' { | ||
res.push('\\'); | ||
next_prev = 0 as char; // Remove duplicate backslashes | ||
} else if iter.peek().map(|next| next != &'\\').unwrap_or(true) { | ||
res.push('"'); // A lone backslash is actually a missing double quote | ||
} | ||
} | ||
c => res.push(c), | ||
} | ||
prev = next_prev; | ||
} | ||
|
||
res | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_unescape() { | ||
assert_eq!(r#"\""#, unescape(r"\\\")); | ||
assert_eq!( | ||
r#"vdiv(5.1, "(R1+\R2\\)/R2*0.8", 'E96', 500e3, 700e3)"#, | ||
unescape(r"vdiv(5.1, \(R1+\\R2\\\\)/R2*0.8\, 'E96', 500e3, 700e3)") | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific reason to make this a new crate compared to build into kicad_rs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly just for easy expandability, one does not need to touch core application logic to extend its functionality (via adding these functions). IIRC the "external library" approach was also preferred when I asked about it.