Skip to content

expressions ¤

Evaluate NetlistParse expression nodes with explicit, scoped name lookup.

Classes:

Name Description
Scope

Lazy lexical parameter environment, with overrides and cycle detection.

Functions:

Name Description
evaluate

Interpret only supported CST operations; never execute source as Python.

evaluate_source

Parse a scalar expression without the optional NetlistParse extension.

parse_sine_waveform

Resolve SPICE-style SIN(offset amplitude frequency [delay damping phase]).

Scope ¤

Scope(parent: Scope | None = None)

Lazy lexical parameter environment, with overrides and cycle detection.

Methods:

Name Description
lookup

Resolve a parameter, evaluating its definition in the declaring scope.

Source code in circulax/netlist_io/expressions.py
def __init__(self, parent: Scope | None = None) -> None:
    """Create a lazy parameter scope with an optional lexical parent."""
    self.parent = parent
    self.dialect = parent.dialect if parent else "ngspice"
    self.statistical_mode = parent.statistical_mode if parent else "reject"
    self.bindings: dict[str, Any] = {}
    self._active: set[str] = set()

lookup ¤

lookup(name: str) -> float | str

Resolve a parameter, evaluating its definition in the declaring scope.

SPICE identifiers are case-insensitive, so a name is matched exactly first and falls back to a case-insensitive scan of this scope only.

Source code in circulax/netlist_io/expressions.py
def lookup(self, name: str) -> float | str:
    """Resolve a parameter, evaluating its definition in the declaring scope.

    SPICE identifiers are case-insensitive, so a name is matched exactly
    first and falls back to a case-insensitive scan of this scope only.
    """
    if name not in self.bindings:
        folded = next((k for k in self.bindings if k.lower() == name.lower()), None)
        if folded is not None:
            name = folded
        elif self.parent is not None:
            return self.parent.lookup(name)
        else:
            msg = f"unresolved parameter {name!r}"
            raise NetlistError(msg)
    value = self.bindings[name]
    if isinstance(value, (float, int)):
        return value
    if name in self._active:
        msg = f"parameter cycle involving {name!r}"
        raise NetlistError(msg)
    self._active.add(name)
    try:
        return evaluate(value, self)
    finally:
        self._active.remove(name)

evaluate ¤

evaluate(node: Any, scope: Scope) -> float | str

Interpret only supported CST operations; never execute source as Python.

Source code in circulax/netlist_io/expressions.py
def evaluate(node: Any, scope: Scope) -> float | str:  # noqa: C901, PLR0911, PLR0912 -- CST operation dispatch
    """Interpret only supported CST operations; never execute source as Python."""
    c = children(node)
    kind = node.kind
    if kind in {"Brace", "Parens", "ParenthesizedExpression", "LiteralExpr", "FunctionArgs", "Condition", "Quote", "Prime"}:
        expressions = [p for p in c if p.kind != "Notation"]
        if len(expressions) != 1:
            msg = f"invalid expression {node.text!r}"
            raise NetlistError(msg)
        return evaluate(expressions[0], scope)
    if kind in {"Literal", "StringLiteral"} and node.text.startswith('"'):
        return node.text[1:-1]
    if kind == "NumberLiteral":
        match = re.fullmatch(r"([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)([A-Za-z]*)", node.text)
        if not match:
            msg = f"invalid number {node.text!r}"
            raise NetlistError(msg)
        suffix = match[2].lower()
        if match[2] == "M" and scope.dialect not in {"spice", "ngspice"}:
            return float(match[1]) * 1e6
        if suffix and suffix not in _SCALE:
            msg = f"unsupported unit suffix {suffix!r}"
            raise NetlistError(msg)
        return float(match[1]) * _SCALE.get(suffix, 1)
    if kind in {"NameRef", "Identifier"}:
        return scope.lookup(node.text)
    if kind == "BinaryExpression":
        left, op, right = c
        a = evaluate(left, scope)
        if op.text == "&&":
            return float(bool(a) and bool(evaluate(right, scope)))
        if op.text == "||":
            return float(bool(a) or bool(evaluate(right, scope)))
        if op.text not in _BINARY:
            msg = f"unsupported operator {op.text!r}"
            raise NetlistError(msg)
        return float(_BINARY[op.text](a, evaluate(right, scope)))
    if kind in {"UnaryExpression", "UnaryOp"}:
        op, expression = c
        value = evaluate(expression, scope)
        if op.text == "-":
            return -value
        if op.text == "+":
            return value
        if op.text == "!":
            return float(not value)
        msg = f"unsupported unary operator {op.text!r}"
        raise NetlistError(msg)
    if kind == "TernaryExpr":
        return evaluate(c[2] if evaluate(c[0], scope) else c[4], scope)
    if kind == "FunctionCall":
        function = c[0].text
        if function.lower() == "agauss" and scope.statistical_mode == "nominal":
            arguments = children(node, "FunctionArgs")
            if len(arguments) != 3:
                msg = "agauss requires nominal, variation and sigma arguments"
                raise NetlistError(msg)
            values = [evaluate(argument, scope) for argument in arguments]
            return float(values[0])
        if function not in _FUNCTIONS:
            msg = f"unsupported function {function!r}"
            raise NetlistError(msg)
        return float(_FUNCTIONS[function](*(evaluate(a, scope) for a in children(node, "FunctionArgs"))))
    msg = f"unsupported expression {kind}: {node.text!r}"
    raise NetlistError(msg)

evaluate_source ¤

evaluate_source(
    expression: str, settings: dict[str, float], dialect: str = "ngspice"
) -> float

Parse a scalar expression without the optional NetlistParse extension.

dialect="spice" and "ngspice" read an uppercase M suffix as milli, as SPICE source cards do; other dialects read it as mega.

@tags circulax-simulation

Source code in circulax/netlist_io/expressions.py
def evaluate_source(expression: str, settings: dict[str, float], dialect: str = "ngspice") -> float:
    """Parse a scalar expression without the optional NetlistParse extension.

    ``dialect="spice"`` and ``"ngspice"`` read an uppercase ``M`` suffix as
    milli, as SPICE source cards do; other dialects read it as mega.

    @tags circulax-simulation
    """
    scope = Scope()
    scope.dialect = dialect
    scope.bindings.update(settings)
    return float(evaluate(parse_expression(expression), scope))

parse_sine_waveform ¤

parse_sine_waveform(waveform: str, dialect: str = 'spice') -> dict[str, float]

Resolve SPICE-style SIN(offset amplitude frequency [delay damping phase]).

Phase is supplied in degrees and returned in radians. Numeric arguments use the same safe expression/unit interpreter as model cards, read with SPICE suffix semantics by default (M is milli). Other waveform kinds are rejected so callers cannot silently substitute a DC source.

@tags circulax-simulation

Source code in circulax/netlist_io/expressions.py
def parse_sine_waveform(waveform: str, dialect: str = "spice") -> dict[str, float]:
    """Resolve SPICE-style SIN(offset amplitude frequency [delay damping phase]).

    Phase is supplied in degrees and returned in radians. Numeric arguments use
    the same safe expression/unit interpreter as model cards, read with SPICE
    suffix semantics by default (``M`` is milli). Other waveform kinds are
    rejected so callers cannot silently substitute a DC source.

    @tags circulax-simulation
    """
    # Keep the original import path without mixing waveform parsing into the
    # expression evaluator. Import lazily because sources uses evaluate_source.
    from circulax.netlist_io.sources import parse_sine_waveform as parse_sine

    return parse_sine(waveform, dialect)