Skip to content
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

Merged
merged 4 commits into from
Jun 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tools/kicad/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[workspace]
members = ["kicad_rs", "kicad_functions"]
11 changes: 11 additions & 0 deletions tools/kicad/kicad_functions/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "kicad_functions"
Copy link
Member

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?

Copy link
Member Author

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.

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 }
23 changes: 23 additions & 0 deletions tools/kicad/kicad_functions/src/idx.rs
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()))
}
14 changes: 14 additions & 0 deletions tools/kicad/kicad_functions/src/lib.rs
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())),
}
}
6 changes: 6 additions & 0 deletions tools/kicad/kicad_functions/src/util.rs
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()))
}
137 changes: 137 additions & 0 deletions tools/kicad/kicad_functions/src/vdiv.rs
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()?;
Copy link
Member

Choose a reason for hiding this comment

The 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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does evalexpr support tuples natively, or are they arrays?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "tuple" is just a Vec with any amount of elements. evalexpr has a an enum variant Value(TupleType), where TupleType is an alias for Vec<Value>. I haven't checked what is supported for those though, i.e. indexing is pretty much required for this use case. Will check in a bit if that's implemented.

Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Up @@ -8,6 +8,7 @@ edition = "2018"

[dependencies]
evalexpr = "6.1"
kicad_functions = { path = "../kicad_functions" }
kicad_parse_gen = { git = "https://github.com/productize/kicad-parse-gen", branch = "kicad5" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,8 @@ impl<'a> Context for SheetIndex<'a> {
.flatten()
}

fn call_function(&self, _identifier: &str, _argument: &Value) -> EvalexprResult<Value> {
// TODO: Fixed function set (voltage divider etc.)
unimplemented!("functions are currently unsupported");
fn call_function(&self, identifier: &str, argument: &Value) -> EvalexprResult<Value> {
kicad_functions::call_function(identifier, argument)
}
}

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -360,3 +362,41 @@ impl<T: AsRef<str>> SplitCharN for Option<T> {
}
}
}

fn unescape(s: &str) -> String {
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its custom, I just cobbled it together since kicad_parse_gen messes up escaping so badly.

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)")
)
}
}
File renamed without changes.