Evaluator

Evaluator

Evaluator()

An optimized evaluator of an expression.

Methods

Name Description
__copy__ Copy the evaluator.
compile Compile the evaluator to a shared library using C++ and optionally inline assembly and load it.
dualize Dualize the evaluator to support hyper-dual numbers with the given shape, indicating the number of derivatives in every variable per term
evaluate Evaluate the expression for multiple inputs and return the result
evaluate_complex Evaluate the expression for multiple inputs and return the result
evaluate_complex_with_prec Evaluate the expression for a single complex input, represented as a tuple of real and imaginary parts
evaluate_with_prec Evaluate the expression for a single input
get_instructions Return the instructions for efficiently evaluating the expression, the length of the list of temporary variables, and the list of constants
jit_compile JIT compile the evaluator for faster evaluation
load Load the evaluator into memory, preparing it for evaluation.
merge Merge evaluator other into self
save Save the evaluator to a byte string.
set_real_params Set which parameters are fully real

__copy__

Evaluator.__copy__() -> Evaluator

Copy the evaluator.

compile

compile has 6 variants:

compile returning CompiledRealEvaluator

Evaluator.compile(
    function_name: str,
    filename: str,
    library_name: str,
    number_type: Literal['real'],
    inline_asm: str = 'default',
    optimization_level: int = 3,
    native: bool = True,
    compiler_path: str | None = None,
    compiler_flags: Sequence[str] | None = None,
    custom_header: str | None = None,
) -> CompiledRealEvaluator

Compile the evaluator to a shared library using C++ and optionally inline assembly and load it.

Parameters

  • function_name (str) The name of the function to generate and compile.
  • filename (str) The name of the file to generate.
  • library_name (str) The name of the shared library to generate.
  • number_type (Literal[‘real’] | Literal[‘complex’] | Literal[‘real_4x’] | Literal[‘complex_4x’] | Literal[‘cuda_real’] | Literal[‘cuda_complex’]) The numeric backend to generate. Use ‘real’ for double precision or ‘complex’ for complex double. For 4x SIMD runs, use ‘real_4x’ or ‘complex_4x’. For GPU runs with CUDA, use ‘cuda_real’ or ‘cuda_complex’.
  • inline_asm (str) The inline ASM option can be set to ‘default’, ‘x64’, ‘avx2’, ‘aarch64’ or ‘none’.
  • optimization_level (int) The compiler optimization level. This can be set to 0, 1, 2 or 3.
  • native (bool) If True, compile for the native architecture. This may produce faster code, but is less portable.
  • compiler_path (str | None) The custom path to the compiler executable.
  • compiler_flags (Sequence[str] | None) The custom flags to pass to the compiler.
  • custom_header (str | None) The custom header to include in the generated code.

compile returning CompiledComplexEvaluator

Evaluator.compile(
    function_name: str,
    filename: str,
    library_name: str,
    number_type: Literal['complex'],
    inline_asm: str = 'default',
    optimization_level: int = 3,
    native: bool = True,
    compiler_path: str | None = None,
    compiler_flags: Sequence[str] | None = None,
    custom_header: str | None = None,
) -> CompiledComplexEvaluator

Compile the evaluator to a shared library using C++ and optionally inline assembly and load it.

Parameters

  • function_name (str) The name of the function to generate and compile.
  • filename (str) The name of the file to generate.
  • library_name (str) The name of the shared library to generate.
  • number_type (Literal[‘real’] | Literal[‘complex’] | Literal[‘real_4x’] | Literal[‘complex_4x’] | Literal[‘cuda_real’] | Literal[‘cuda_complex’]) The numeric backend to generate. Use ‘real’ for double precision or ‘complex’ for complex double. For 4x SIMD runs, use ‘real_4x’ or ‘complex_4x’. For GPU runs with CUDA, use ‘cuda_real’ or ‘cuda_complex’.
  • inline_asm (str) The inline ASM option can be set to ‘default’, ‘x64’, ‘avx2’, ‘aarch64’ or ‘none’.
  • optimization_level (int) The compiler optimization level. This can be set to 0, 1, 2 or 3.
  • native (bool) If True, compile for the native architecture. This may produce faster code, but is less portable.
  • compiler_path (str | None) The custom path to the compiler executable.
  • compiler_flags (Sequence[str] | None) The custom flags to pass to the compiler.
  • custom_header (str | None) The custom header to include in the generated code.

compile returning CompiledSimdRealEvaluator

Evaluator.compile(
    function_name: str,
    filename: str,
    library_name: str,
    number_type: Literal['real_4x'],
    inline_asm: str = 'default',
    optimization_level: int = 3,
    native: bool = True,
    compiler_path: str | None = None,
    compiler_flags: Sequence[str] | None = None,
    custom_header: str | None = None,
) -> CompiledSimdRealEvaluator

Compile the evaluator to a shared library with 4x SIMD using C++ and optionally inline assembly and load it.

Parameters

  • function_name (str) The name of the function to generate and compile.
  • filename (str) The name of the file to generate.
  • library_name (str) The name of the shared library to generate.
  • number_type (Literal[‘real’] | Literal[‘complex’] | Literal[‘real_4x’] | Literal[‘complex_4x’] | Literal[‘cuda_real’] | Literal[‘cuda_complex’]) The numeric backend to generate. Use ‘real’ for double precision or ‘complex’ for complex double. For 4x SIMD runs, use ‘real_4x’ or ‘complex_4x’. For GPU runs with CUDA, use ‘cuda_real’ or ‘cuda_complex’.
  • inline_asm (str) The inline ASM option can be set to ‘default’, ‘x64’, ‘avx2’, ‘aarch64’ or ‘none’.
  • optimization_level (int) The compiler optimization level. This can be set to 0, 1, 2 or 3.
  • native (bool) If True, compile for the native architecture. This may produce faster code, but is less portable.
  • compiler_path (str | None) The custom path to the compiler executable.
  • compiler_flags (Sequence[str] | None) The custom flags to pass to the compiler.
  • custom_header (str | None) The custom header to include in the generated code.

compile returning CompiledSimdComplexEvaluator

Evaluator.compile(
    function_name: str,
    filename: str,
    library_name: str,
    number_type: Literal['complex_4x'],
    inline_asm: str = 'default',
    optimization_level: int = 3,
    native: bool = True,
    compiler_path: str | None = None,
    compiler_flags: Sequence[str] | None = None,
    custom_header: str | None = None,
) -> CompiledSimdComplexEvaluator

Compile the evaluator to a shared library with 4x SIMD using C++ and optionally inline assembly and load it.

Parameters

  • function_name (str) The name of the function to generate and compile.
  • filename (str) The name of the file to generate.
  • library_name (str) The name of the shared library to generate.
  • number_type (Literal[‘real’] | Literal[‘complex’] | Literal[‘real_4x’] | Literal[‘complex_4x’] | Literal[‘cuda_real’] | Literal[‘cuda_complex’]) The numeric backend to generate. Use ‘real’ for double precision or ‘complex’ for complex double. For 4x SIMD runs, use ‘real_4x’ or ‘complex_4x’. For GPU runs with CUDA, use ‘cuda_real’ or ‘cuda_complex’.
  • inline_asm (str) The inline ASM option can be set to ‘default’, ‘x64’, ‘avx2’, ‘aarch64’ or ‘none’.
  • optimization_level (int) The compiler optimization level. This can be set to 0, 1, 2 or 3.
  • native (bool) If True, compile for the native architecture. This may produce faster code, but is less portable.
  • compiler_path (str | None) The custom path to the compiler executable.
  • compiler_flags (Sequence[str] | None) The custom flags to pass to the compiler.
  • custom_header (str | None) The custom header to include in the generated code.

compile returning CompiledCudaRealEvaluator

Evaluator.compile(
    function_name: str,
    filename: str,
    library_name: str,
    number_type: Literal['cuda_real'],
    inline_asm: str = 'default',
    optimization_level: int = 3,
    native: bool = True,
    compiler_path: str | None = None,
    compiler_flags: Sequence[str] | None = None,
    custom_header: str | None = None,
    cuda_number_of_evaluations: int | None = None,
    cuda_block_size: int | None = 256,
) -> CompiledCudaRealEvaluator

Compile the evaluator to a shared library using C++ and optionally inline assembly and load it.

You may have to specify -code=sm_XY for your architecture XY in the compiler flags to prevent a potentially long JIT compilation upon the first evaluation.

Parameters

  • function_name (str) The name of the function to generate and compile.
  • filename (str) The name of the file to generate.
  • library_name (str) The name of the shared library to generate.
  • number_type (Literal[‘real’] | Literal[‘complex’] | Literal[‘real_4x’] | Literal[‘complex_4x’] | Literal[‘cuda_real’] | Literal[‘cuda_complex’]) The numeric backend to generate. Use ‘real’ for double precision or ‘complex’ for complex double. For 4x SIMD runs, use ‘real_4x’ or ‘complex_4x’. For GPU runs with CUDA, use ‘cuda_real’ or ‘cuda_complex’.
  • inline_asm (str) The inline ASM option can be set to ‘default’, ‘x64’, ‘avx2’, ‘aarch64’ or ‘none’.
  • optimization_level (int) The compiler optimization level. This can be set to 0, 1, 2 or 3.
  • native (bool) If True, compile for the native architecture. This may produce faster code, but is less portable.
  • compiler_path (str | None) The custom path to the compiler executable.
  • compiler_flags (Sequence[str] | None) The custom flags to pass to the compiler.
  • custom_header (str | None) The custom header to include in the generated code.
  • cuda_number_of_evaluations (int | None) The number of parallel evaluations to perform on the CUDA device. The input to evaluate must have the length cuda_number_of_evaluations * arg_len.
  • cuda_block_size (int | None) The block size for CUDA kernel launches.

compile returning CompiledCudaComplexEvaluator

Evaluator.compile(
    function_name: str,
    filename: str,
    library_name: str,
    number_type: Literal['cuda_complex'],
    inline_asm: str = 'default',
    optimization_level: int = 3,
    native: bool = True,
    compiler_path: str | None = None,
    compiler_flags: Sequence[str] | None = None,
    custom_header: str | None = None,
    cuda_number_of_evaluations: int | None = None,
    cuda_block_size: int | None = 256,
) -> CompiledCudaComplexEvaluator

Compile the evaluator to a shared library using C++ and optionally inline assembly and load it.

You may have to specify -code=sm_XY for your architecture XY in the compiler flags to prevent a potentially long JIT compilation upon the first evaluation.

Parameters

  • function_name (str) The name of the function to generate and compile.
  • filename (str) The name of the file to generate.
  • library_name (str) The name of the shared library to generate.
  • number_type (Literal[‘real’] | Literal[‘complex’] | Literal[‘real_4x’] | Literal[‘complex_4x’] | Literal[‘cuda_real’] | Literal[‘cuda_complex’]) The numeric backend to generate. Use ‘real’ for double precision or ‘complex’ for complex double. For 4x SIMD runs, use ‘real_4x’ or ‘complex_4x’. For GPU runs with CUDA, use ‘cuda_real’ or ‘cuda_complex’.
  • inline_asm (str) The inline ASM option can be set to ‘default’, ‘x64’, ‘avx2’, ‘aarch64’ or ‘none’.
  • optimization_level (int) The compiler optimization level. This can be set to 0, 1, 2 or 3.
  • native (bool) If True, compile for the native architecture. This may produce faster code, but is less portable.
  • compiler_path (str | None) The custom path to the compiler executable.
  • compiler_flags (Sequence[str] | None) The custom flags to pass to the compiler.
  • custom_header (str | None) The custom header to include in the generated code.
  • cuda_number_of_evaluations (int | None) The number of parallel evaluations to perform on the CUDA device. The input to evaluate must have the length cuda_number_of_evaluations * arg_len.
  • cuda_block_size (int | None) The block size for CUDA kernel launches.

dualize

Evaluator.dualize(
    dual_shape: list[list[int]],
    external_functions: dict[tuple[str, str, int], Callable[[Sequence[float | complex]], float | complex]] | None = None,
    zero_components: list[tuple[int, int]] | None = None,
) -> None

Dualize the evaluator to support hyper-dual numbers with the given shape, indicating the number of derivatives in every variable per term. This allows for efficient computation of derivatives.

For example, to compute first derivatives in two variables x and y, use dual_shape = [[0, 0], [1, 0], [0, 1]].

External functions must be mapped to len(dual_shape) different functions that compute a single component each. The input to the functions is the flattened vector of all components of all parameters, followed by all previously computed output components.

Examples

from symbolica import *
e1 = E('x^2 + y*x').evaluator({}, {}, [S('x'), S('y')])
e1.dualize([[0, 0], [1, 0], [0, 1]])
r = e1.evaluate([[2., 1., 0., 3., 0., 1.]])
print(r)  # [10, 7, 2]

Mapping external functions:

ev = E('f(x + 1)').evaluator({}, {}, [S('x')], external_functions={(S('f'), 'f'): lambda args: args[0]})
ev.dualize([[0], [1]], {('f', 'f0', 0): lambda args: args[0], ('f', 'f1', 1): lambda args: args[1]})
print(ev.evaluate([[2., 1.]]))  # [[3. 1.]]

Parameters

  • dual_shape (list[list[int]]) The shape of the dual numbers, indicating the number of derivatives in every variable per term.
  • external_functions (dict[tuple[str, str, int], Callable[[Sequence[float | complex]], float | complex]] | None) A mapping from external function identifiers to functions that compute a single component each. The key is a tuple of function name, unique printable name, and component index. The value is a function that takes the flattened parameters and returns a component.
  • zero_components (list[tuple[int, int]] | None) A list of components that are known to be zero and can be skipped in the dualization. Each component is specified as a tuple of (parameter index, dual index).

evaluate

Evaluator.evaluate(inputs: npt.ArrayLike) -> npt.NDArray[np.float64]

Evaluate the expression for multiple inputs and return the result. For best performance, use numpy arrays instead of lists.

On the first call, the expression is JIT compiled using SymJIT.

Examples

Evaluate the function for three sets of inputs:

from symbolica import *
import numpy as np
ev = E('x * y + 2').evaluator({}, {}, [S('x'), S('y')])
print(ev.evaluate(np.array([1., 2., 3., 4., 5., 6.]).reshape((3, 2))))

Yields[[ 4.] [ 8.] [14.]]

Parameters

  • inputs (npt.ArrayLike) The input values or batches to evaluate.

evaluate_complex

Evaluator.evaluate_complex(inputs: npt.ArrayLike) -> npt.NDArray[np.complex128]

Evaluate the expression for multiple inputs and return the result. For best performance, use numpy arrays and np.complex128 instead of lists and complex.

On the first call, the expression is JIT compiled using SymJIT.

Examples

Evaluate the function for three sets of inputs:

from symbolica import *
import numpy as np
ev = E('x * y + 2').evaluator({}, {}, [S('x'), S('y')])
print(ev.evaluate(np.array([1.+2j, 2., 3., 4., 5., 6.]).reshape((3, 2))))

Yields[[ 4.+4.j] [14.+0.j] [32.+0.j]]

Parameters

  • inputs (npt.ArrayLike) The input values or batches to evaluate.

evaluate_complex_with_prec

Evaluator.evaluate_complex_with_prec(
    inputs: Sequence[tuple[float | str | Decimal, float | str | Decimal]],
    decimal_digit_precision: int,
) -> list[tuple[Decimal]]

Evaluate the expression for a single complex input, represented as a tuple of real and imaginary parts. The precision of the input parameters is honored, and all constants are converted to a float with a decimal precision set by decimal_digit_precision.

If decimal_digit_precision is set to 32, a much faster evaluation using double-float arithmetic is performed.

Examples

Evaluate the function for a single input with 50 digits of precision:

from symbolica import *
ev = E('x^2').evaluator({}, {}, [S('x')])
print(ev.evaluate_complex_with_prec(
    [(Decimal('1.234567890121223456789981273238947212312338947923'), Decimal('3.434567890121223356789981273238947212312338947923'))], 50))

Yields [(Decimal('-10.27209871653338252296233957800668637617803672307'), Decimal('8.480414467170121512062583245527383392798704790330'))]

Parameters

  • inputs (Sequence[tuple[float | str | Decimal, float | str | Decimal]]) The input values or batches to evaluate.
  • decimal_digit_precision (int) The decimal precision used for arbitrary-precision evaluation.

evaluate_with_prec

Evaluator.evaluate_with_prec(
    inputs: Sequence[float | str | Decimal],
    decimal_digit_precision: int,
) -> list[Decimal]

Evaluate the expression for a single input. The precision of the input parameters is honored, and all constants are converted to a float with a decimal precision set by decimal_digit_precision.

If decimal_digit_precision is set to 32, a much faster evaluation using double-float arithmetic is performed.

Examples

Evaluate the function for a single input with 50 digits of precision:

from symbolica import *
ev = E('x^2').evaluator({}, {}, [S('x')])
print(ev.evaluate_with_prec([Decimal('1.234567890121223456789981273238947212312338947923')], 50))

Yields 1.524157875318369274550121833760353508310334033629

Parameters

  • inputs (Sequence[float | str | Decimal]) The input values or batches to evaluate.
  • decimal_digit_precision (int) The decimal precision used for arbitrary-precision evaluation.

get_instructions

Evaluator.get_instructions() -> tuple[list[tuple[str, tuple[str, int], list[tuple[str, int]]]], int, list[Expression]]

Return the instructions for efficiently evaluating the expression, the length of the list of temporary variables, and the list of constants. This can be used to generate code for the expression evaluation in any programming language.

There are four lists that are used in the evaluation instructions:

  • param: the list of input parameters.
  • temp: the list of temporary slots. The size of it is provided as the second return value.
  • const: the list of constants.
  • out: the list of outputs.

The instructions are of the form:

  • ('add', ('out', 0), [('const', 1), ('param', 0)], 0) which means out[0] = const[1] + param[0] where the first 0 arguments are real.
  • ('mul', ('out', 0), [('temp', 0), ('param', 0)], 1) which means out[0] = temp[0] * param[0], where the first 1 arguments are real.
  • ('pow', ('out', 0), ('param', 0), -1, true) which means out[0] = param[0]^-1 and the output is real (true).
  • ('powf', ('out', 0), ('param', 0), ('param', 1), false) which means out[0] = param[0]^param[1].
  • ('fun', ('temp', 1), cos, ('param', 0), true) which means temp[1] = cos(param[0]) and the output is real (true).
  • ('external_fun', ('temp', 1), f, [('param', 0)]) which means temp[1] = f(param[0]).
  • ('if_else', ('temp', 0), 5) which means if temp[0] == 0 goto label 5 (false branch).
  • ('goto', 10) which means goto label 10.
  • ('label', 3) which means label 3.
  • ('join', ('out', 0), ('temp', 0), 3, 7) which means out[0] = (temp[0] != 0) ? label 3 : label 7.

Examples

from symbolica import *
(ins, m, c) = E('x^2+5/3+cos(x)').evaluator({}, {}, [S('x')]).get_instructions()

for x in ins:
    print(x)
print('temp list length:', m)
print('constants:', c)

yields

('mul', ('out', 0), [('param', 0), ('param', 0)], 0) ('fun', ('temp', 1), cos, ('param', 0), false) ('add', ('out', 0), [('const', 0), ('out', 0), ('temp', 1)]) temp list length: 2 constants: [5/3]

jit_compile

Evaluator.jit_compile(jit_compile: bool) -> None

JIT compile the evaluator for faster evaluation. This may take some time, but will speed up subsequent evaluations.

Parameters

  • jit_compile (bool) Whether JIT compilation should be enabled.

load

Evaluator.load(
    evaluator: bytes,
    external_functions: dict[tuple[Expression, str], Callable[[Sequence[float | complex]], float | complex]] = {},
) -> Evaluator

Load the evaluator into memory, preparing it for evaluation.

Parameters

  • evaluator (bytes) The serialized evaluator state.
  • external_functions (dict[tuple[Expression, str], Callable[[ Sequence[float | complex]], float | complex]]) The external functions to register.

merge

Evaluator.merge(other: Evaluator, cpe_iterations: int | None = None) -> None

Merge evaluator other into self. The parameters must be the same, and the outputs will be concatenated.

The optional cpe_iterations parameter can be used to limit the number of common pair elimination rounds after the merge.

Examples

from symbolica import *
e1 = E('x').evaluator({}, {}, [S('x')])
e2 = E('x+1').evaluator({}, {}, [S('x')])
e1.merge(e2)
e1.evaluate([[2.]])

yields [2, 3].

Parameters

  • other (Evaluator) The other operand to combine or compare with.
  • cpe_iterations (int | None) The number of common subexpression elimination iterations to perform.

save

Evaluator.save() -> bytes

Save the evaluator to a byte string.

set_real_params

Evaluator.set_real_params(
    real_params: list[int],
    sqrt_real = False,
    log_real = False,
    powf_real = False,
    verbose = False,
) -> None

Set which parameters are fully real. This allows for more optimal assembly output that uses real arithmetic instead of complex arithmetic where possible.

You can also set if all encountered sqrt, log, and powf operations with real arguments are expected to yield real results.

Must be called after all optimization functions and merging are performed on the evaluator, or the registration will be lost.

Parameters

  • real_params (list[int]) The parameter indices that should be treated as real.
  • sqrt_real (Any) Whether square roots should be assumed real.
  • log_real (Any) Whether logarithms should be assumed real.
  • powf_real (Any) Whether fractional powers should be assumed real.
  • verbose (Any) Whether verbose output should be enabled.