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__() -> EvaluatorCopy 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,
) -> CompiledRealEvaluatorCompile 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) IfTrue, 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,
) -> CompiledComplexEvaluatorCompile 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) IfTrue, 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,
) -> CompiledSimdRealEvaluatorCompile 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) IfTrue, 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,
) -> CompiledSimdComplexEvaluatorCompile 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) IfTrue, 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,
) -> CompiledCudaRealEvaluatorCompile 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) IfTrue, 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 lengthcuda_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,
) -> CompiledCudaComplexEvaluatorCompile 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) IfTrue, 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 lengthcuda_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,
) -> NoneDualize 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 meansout[0] = const[1] + param[0]where the first0arguments are real.('mul', ('out', 0), [('temp', 0), ('param', 0)], 1)which meansout[0] = temp[0] * param[0], where the first1arguments are real.('pow', ('out', 0), ('param', 0), -1, true)which meansout[0] = param[0]^-1and the output is real (true).('powf', ('out', 0), ('param', 0), ('param', 1), false)which meansout[0] = param[0]^param[1].('fun', ('temp', 1), cos, ('param', 0), true)which meanstemp[1] = cos(param[0])and the output is real (true).('external_fun', ('temp', 1), f, [('param', 0)])which meanstemp[1] = f(param[0]).('if_else', ('temp', 0), 5)which meansif temp[0] == 0 goto label 5(false branch).('goto', 10)which meansgoto label 10.('label', 3)which meanslabel 3.('join', ('out', 0), ('temp', 0), 3, 7)which meansout[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) -> NoneJIT 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]] = {},
) -> EvaluatorLoad 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) -> NoneMerge 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() -> bytesSave 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,
) -> NoneSet 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.