Skip to content

Commit

Permalink
Merge pull request #16 from racklet/evaluator-functions
Browse files Browse the repository at this point in the history
Create a function library for the evaluator, bring back the ICCC voltage divider
  • Loading branch information
luxas authored Jun 24, 2021
2 parents 9821be1 + aeb3f3a commit 90a4982
Show file tree
Hide file tree
Showing 22 changed files with 238 additions and 5 deletions.
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"
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()?;
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));
}
}

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.
File renamed without changes.
File renamed without changes.
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 {
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.
File renamed without changes.

0 comments on commit 90a4982

Please sign in to comment.