#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2021 Jan Sebastian Götte from dataclasses import dataclass import operator import re import ast import math from ..utils import LengthUnit, MM, Inch, MILLIMETERS_PER_INCH def expr(obj): return obj if isinstance(obj, Expression) else ConstantExpression(obj) _make_expr = expr @dataclass(frozen=True, slots=True) class Expression: def optimized(self, variable_binding={}): return self def __str__(self): return f'<{self.to_gerber()}>' def __repr__(self): return f'' def converted(self, unit): return self def replace_mixed_subexpressions(self, unit): return self def calculate(self, variable_binding={}, unit=None): expr = self.converted(unit).optimized(variable_binding) if not isinstance(expr, ConstantExpression): raise IndexError(f'Cannot fully resolve expression due to unresolved parameters: residual expression {expr} under parameters {variable_binding}') return expr.value def __add__(self, other): return OperatorExpression(operator.add, self, expr(other)).optimized() def __radd__(self, other): return expr(other) + self def __sub__(self, other): return OperatorExpression(operator.sub, self, expr(other)).optimized() def __rsub__(self, other): return expr(other) - self def __mul__(self, other): return OperatorExpression(operator.mul, self, expr(other)).optimized() def __rmul__(self, other): return expr(other) * self def __truediv__(self, other): return OperatorExpression(operator.truediv, self, expr(other)).optimized() def __rtruediv__(self, other): return expr(other) / self def __neg__(self): return NegatedExpression(self) def __pos__(self): return self def parameters(self): return tuple() @property def _operator(self): return None @dataclass(frozen=True, slots=True) class UnitExpression(Expression): expr: Expression unit: LengthUnit def __init__(self, expr, unit): expr = _make_expr(expr) if isinstance(expr, UnitExpression): expr = expr.converted(unit) object.__setattr__(self, 'expr', expr) object.__setattr__(self, 'unit', unit) def to_gerber(self, register_variable=None, unit=None): return self.converted(unit).optimized().to_gerber(register_variable) def __eq__(self, other): return type(other) == type(self) and \ self.unit == other.unit and\ self.expr == other.expr def __str__(self): return f'<{self.expr.to_gerber()} {self.unit}>' def __repr__(self): return f'' def replace_mixed_subexpressions(self, unit): return self.converted(unit).replace_mixed_subexpressions(unit) def converted(self, unit): if self.unit is None or unit is None or self.unit == unit: return self.expr elif MM == unit: return self.expr * MILLIMETERS_PER_INCH elif Inch == unit: return self.expr / MILLIMETERS_PER_INCH else: raise ValueError(f'invalid unit {unit}, must be "inch" or "mm".') def __add__(self, other): if not isinstance(other, UnitExpression): raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') if self.unit == other.unit or self.unit is None or other.unit is None: return UnitExpression(self.expr + other.expr, self.unit) if other.unit == 'mm': # -> and self.unit == 'inch' return UnitExpression(self.expr + (other.expr / MILLIMETERS_PER_INCH), self.unit) else: # other.unit == 'inch' and self.unit == 'mm' return UnitExpression(self.expr + (other.expr * MILLIMETERS_PER_INCH), self.unit) def __radd__(self, other): # left hand side cannot have been an UnitExpression or __radd__ would not have been called raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') def __sub__(self, other): return (self + (-other)).optimized() def __rsub__(self, other): # see __radd__ above raise ValueError('Unit mismatch: Can only add/subtract UnitExpression from UnitExpression, not scalar.') def __mul__(self, other): return UnitExpression(self.expr * other, self.unit) def __rmul__(self, other): return UnitExpression(other * self.expr, self.unit) def __truediv__(self, other): return UnitExpression(self.expr / other, self.unit) def __rtruediv__(self, other): return UnitExpression(other / self.expr, self.unit) def __neg__(self): return UnitExpression(-self.expr, self.unit) def __pos__(self): return self def parameters(self): return self.expr.parameters() @dataclass(frozen=True, slots=True) class ConstantExpression(Expression): value: float def __float__(self): return float(self.value) def __eq__(self, other): try: return math.isclose(self.value, float(other), abs_tol=1e-9) except TypeError: return False def to_gerber(self, register_variable=None, unit=None): if self == 0: # Avoid producing "-0" for negative floating point zeros return '0' return f'{self.value:.6f}'.rstrip('0').rstrip('.') @dataclass(frozen=True, slots=True) class VariableExpression(Expression): expr: Expression def optimized(self, variable_binding={}): opt = self.expr.optimized(variable_binding) if isinstance(opt, OperatorExpression): return self else: return opt def __eq__(self, other): return type(self) == type(other) and self.expr == other.expr def replace_mixed_subexpressions(self, unit): return VariableExpression(self.expr.replace_mixed_subexpressions(unit)) def to_gerber(self, register_variable=None, unit=None): if register_variable is None: return self.expr.to_gerber(None, unit) else: num = register_variable(self.expr.converted(unit).optimized()) return f'${num}' @dataclass(frozen=True, slots=True) class ParameterExpression(Expression): number: int def optimized(self, variable_binding={}): if self.number in variable_binding: return expr(variable_binding[self.number]).optimized(variable_binding) return self def __eq__(self, other): return type(self) == type(other) and \ self.number == other.number def to_gerber(self, register_variable=None, unit=None): return f'${self.number}' def parameters(self): yield self @dataclass(frozen=True, slots=True) class NegatedExpression(Expression): value: Expression def optimized(self, variable_binding={}): match self.value.optimized(variable_binding): # -(-x) == x case NegatedExpression(inner_value): return inner_value # -(x) == -x case ConstantExpression(inner_value): return ConstantExpression(-inner_value) # -(x-y) == y-x case OperatorExpression(operator.sub, l, r): return OperatorExpression(operator.sub, r, l) # Round very small values and negative floating point zeros to a (positive) zero case 0: return expr(0) # Default case case x: return NegatedExpression(x) @property def _operator(self): return self.value._operator def __eq__(self, other): return type(self) == type(other) and \ self.value == other.value def to_gerber(self, register_variable=None, unit=None): val_str = self.value.to_gerber(register_variable, unit) if isinstance(self.value, (VariableExpression, ParameterExpression)): return f'-{val_str}' else: return f'-({val_str})' @dataclass(frozen=True, slots=True) class OperatorExpression(Expression): op: str l: Expression r: Expression def __init__(self, op, l, r): object.__setattr__(self, 'op', op) object.__setattr__(self, 'l', expr(l)) object.__setattr__(self, 'r', expr(r)) def __eq__(self, other): return type(self) == type(other) and \ self.op == other.op and \ self.l == other.l and \ self.r == other.r @property def _operator(self): return self.op def optimized(self, variable_binding={}): l = self.l.optimized(variable_binding) r = self.r.optimized(variable_binding) match (l, self.op, r): case (ConstantExpression(), op, ConstantExpression()): return ConstantExpression(self.op(float(l), float(r))) # Minimize operations with neutral elements and zeros # 0 + x == x case (0, operator.add, r): return r # x + 0 == x case (l, operator.add, 0): return l # 0 * x == 0 case (0, operator.mul, r): return expr(0) # x * 0 == 0 case (l, operator.mul, 0): return expr(0) # x * 1 == x case (l, operator.mul, 1): return l # 1 * x == x case (1, operator.mul, r): return r # x * -1 == -x case (l, operator.mul, -1): rv = -l # -1 * x == -x case (-1, operator.mul, r): rv = -r # x - 0 == x case (l, operator.sub, 0): return l # 0 - x == -x (unary minus) case (0, operator.sub, r): rv = -r # x - x == 0 case (l, operator.sub, r) if l == r: return expr(0) # x - -y == x + y case (l, operator.sub, NegatedExpression(r)): rv = (l + r) # x / 1 == x case (l, operator.truediv, 1): return l # x / -1 == -x case (l, operator.truediv, -1): rv = -l # x / x == 1 case (l, operator.truediv, r) if l == r: return expr(1) # -x [*/] -y == x [*/] y case (NegatedExpression(l), (operator.truediv | operator.mul) as op, NegatedExpression(r)): rv = op(l, r) # x + -y == x - y case (l, operator.add, NegatedExpression(r)): rv = l-r # -x + y == y - x case (NegatedExpression(l), operator.add, r): rv = r-l case _: # default return OperatorExpression(self.op, l, r) return expr(rv).optimized(variable_binding) def replace_mixed_subexpressions(self, unit): l = self.l.replace_mixed_subexpressions(unit) if l._operator not in (None, self.op): l = VariableExpression(self.l) r = self.r.replace_mixed_subexpressions(unit) if r._operator not in (None, self.op): r = VariableExpression(self.r) return OperatorExpression(self.op, l, r) def to_gerber(self, register_variable=None, unit=None): lval = self.l.to_gerber(register_variable, unit) rval = self.r.to_gerber(register_variable, unit) if isinstance(self.l, OperatorExpression): lval = f'({lval})' if isinstance(self.r, OperatorExpression): rval = f'({rval})' op = {operator.add: '+', operator.sub: '-', operator.mul: 'x', operator.truediv: '/'} [self.op] return f'{lval}{op}{rval}' def parameters(self): yield from self.l.parameters() yield from self.r.parameters()