Source code for finesse.cyexpr

# distutils: sources = src/finesse/tinyexpr.c
# distutils: include_dirs = src/finesse/


"""Compiled symbolic expressions used internally via parameters and element workspaces.

This sub-module only exposes C code so can only be used by other Cython
extensions. The symbolic expression struct ``cy_expr`` is used in workspaces
(see :class:`.ElementWorkspace`) and parameter code (see :class:`.Parameter`)
for quick evaluation of changing symbolic expressions.

The ``cy_expr`` struct, and associated functions, are wrappers around the C
based math parsing and evaluation engine, `tinyexpr <https://github.com/codeplea/tinyexpr>`_.
"""

from libc.stdlib cimport calloc, free
from cpython.ref cimport Py_XINCREF, Py_XDECREF

cdef cy_expr* cy_expr_new() except NULL nogil:
    cdef cy_expr* ce_p = <cy_expr*> calloc(1, sizeof(cy_expr))
    if not ce_p:
        with gil:
            raise MemoryError()
    ce_p.expr = NULL
    ce_p.variables = NULL
    ce_p.byte_op_str = NULL
    return ce_p

cdef int cy_expr_init(cy_expr* ex, object operation) except -1:
    """Initialise the te_expr and te_variable type fields using the
    operation object (should be an instance of Function)."""
    if ex == NULL:
        raise MemoryError()

    cdef str op_str = str(operation)
    cdef list params = operation.parameters() # get the dependent parameter-refs
    cdef int Nparams = len(params)

    ex.variables = <te_variable*> calloc(Nparams, sizeof(te_variable))
    if not ex.variables:
        raise MemoryError()

    cdef Py_ssize_t i
    cdef Parameter p
    for i in range(Nparams):
        pref = params[i] # the ParameterRef object

        # Replace the corresponding parameter name in the operation string with
        # the tinyexpr compatible name version (see ParameterRef.cyexpr_name)
        op_str = op_str.replace(pref.name, pref.cyexpr_name.decode())
        # Also need to replace "quantity**n" with "quantity^n" as te expects powers in this form
        op_str = op_str.replace("**", "^")

        p = pref.parameter
        ex.variables[i] = te_variable(pref.cyexpr_name, &p.__cvalue, 0, NULL)

    cdef int err # position of parsing error in operation expression
    # Make byte str of operation expression, and store it in
    # cy_expr instance for info / debugging purposes later
    byte_op_str = op_str.encode("UTF-8")
    ex.byte_op_str = <PyObject*>byte_op_str
    Py_XINCREF(ex.byte_op_str)
    ex.expression = byte_op_str
    ex.expr = te_compile(byte_op_str, ex.variables, Nparams, &err)

    if not ex.expr:
        error_loc_str = "    {m: <{pos}}^".format(m="", pos=str(err - 1))
        raise RuntimeError(
            "Bug encountered! Internal cy_expr parsing error on:\n"
           f"    {op_str}\n" +
            error_loc_str +
           "\nError near here"
        )

    return 0

cdef void cy_expr_free(cy_expr* ex) noexcept:
    if ex != NULL:
        if ex.expr != NULL:
            te_free(ex.expr)
        if ex.variables != NULL:
            free(ex.variables)

        Py_XDECREF(ex.byte_op_str)

        free(ex)
        ex = NULL

cdef double cy_expr_eval(cy_expr* ex) noexcept nogil:
    """Evaluate the cythonised symbolic expression."""
    return te_eval(ex.expr)

def test_expr(op_str, value=None):
    """Python accessible function to test the parsing of strings.

    Parameters
    ----------
    op_str : str
        String containing operation, e.g., 0.0+1

    Returns
    -------
    ex.expr : boolean
        False on parse fail
    """
    cdef int err
    cdef cy_expr* ex = cy_expr_new()

    try:
        byte_op_str = op_str.encode("UTF-8")

        ex.expr = te_compile(byte_op_str, ex.variables, 0, &err)

        # test if expression compiled
        if not ex.expr:
            return False

        if value:
            # test if expression evaluates
            return te_eval(ex.expr) == value

        return True
    finally:
        cy_expr_free(ex)