Skip to main content

symjit_bridge/
lib.rs

1//! # Introduction
2
3//! [Symjit](https://github.com/siravan/symjit) is a lightweight just-in-time (JIT)
4//! optimizer compiler for mathematical expressions written in Rust. It was originally
5//! designed to compile SymPy (Python’s symbolic algebra package) expressions
6//! into machine code and to serve as a bridge between SymPy and numerical routines
7//! provided by NumPy and SciPy libraries.
8
9//! Symjit emits AMD64 (x86-64), ARM64 (aarch64), and 64-bit RISC-V (riscv64) machine
10//! codes on Linux, Windows, and macOS platforms. SIMD is supported on x86-64
11//! CPUs with AVX instruction sets.
12
13//! Symbolica (<https://symbolica.io/>) is a fast Rust-based Computer Algebra System.
14//! Symbolica usually generate fast code using external compilers (e.g., using gcc to
15//! compile synthetic c++ code). Symjit accepts Symbolica expressions and can act as
16//! an optional code-generator for Symbolica.
17
18//! Symjit-bridge crate acts as a bridge between Symbolica and Symjit to ease generating
19//! JIT code for Symbolica expressions.
20
21//! # Workflow
22
23//! The main workflow is using different `Runner`s. A runner corresponds to a Symbolica
24//! `CompiledEvaluator` object. The main runners are:
25//!
26//! * `CompiledRealRunner`, corresponding to `CompiledRealEvaluator`.
27//! * `CompiledComplexRunner`, corresponding to `CompiledComplexEvaluator`.
28//! * `InterpretedRealRunner`, bytecode interpreter, generally similar to `ExpressionEvaluator`.
29//! * `InterpretedComplexRunner`, bytecode interpreter, generally similar to `ExpressionEvaluator`.
30//!
31//! Each runner has four main methods:
32//!
33//! * `compile(ev: &ExpressionEvaluator<T>, config: Config)`: the main constructor. `T` is either `f64`
34//!     or `Complex<f64>`, and `config` is an object of type `Config`. For most applications, the
35//!     default config suffices. However, `Config.use_threads(bool)` is useful to enable multi-threading.
36//! * `compile_with_funcs(ev: &ExpressionEvaluator<T>, config: Config, df: &Defuns, num_params: usize)`: Same as
37//!     `compile` but with the additional of external functions defined in a `Defuns` structure and `num_prams`.
38//! * `compile_string(model: String, config: Config)`: `model` is a string generated using `get_instruction` method
39//!     in Python, and `config` is an object of type `Config`.
40//! * `compile_string_with_funcs(model: String, config: Config, df: &Defuns, num_params: usize)`: Same as
41//!     `compile_string` but with the additional of external functions defined in a `Defuns` structure
42//!     and `num_params`.
43//! * `evaluate(args, outs)`: similar to the corresponding method of the `Evaluator`s.
44//! * `save(filename)`.
45//! * `load(filename)`.
46//!
47//! Both `CompiledRealRunner` and `CompiledComplexRunner` may use SIMD instructions if it is available
48//!     and the number of input rows is equal or more than the number of SIMD lanes (4 in AVX, 2 in aarch64).
49//!
50//! ```rust
51//! use anyhow::Result;
52//! use symjit_bridge::{compile, Config};
53
54//! use symbolica::{
55//!     atom::AtomCore,
56//!     evaluate::{FunctionMap, OptimizationSettings},
57//!     parse, symbol,
58//! };
59//!
60//! fn test_real_runner() -> Result<()> {
61//!     let params = vec![parse!("x"), parse!("y")];
62//!     let f = FunctionMap::new();
63//!     let ev = parse!("x + y^3")
64//!         .evaluator(&f, &params, OptimizationSettings::default())
65//!         .unwrap()
66//!         .map_coeff(&|x| x.re.to_f64());
67
68//!     let mut runner = CompiledRealRunner::compile(&ev, Config::default())?;
69//!     let mut outs: [f64; 1] = [0.0];
70//!     runner.evaluate(&[3.0, 5.0], &mut outs);
71//!     assert_eq!(outs[0], 128.0);
72//!     Ok(())
73//! }
74//! ```
75
76//! ## External Functions
77
78//! Symjit has a rich set of transcendental, conditional, and logical functions (refer to
79//! [Symjit](https://github.com/siravan/symjit) for details). It is possible to expose
80//! these functions to Symbolica by using `add_external_function`:
81
82//! ```rust
83//! fn test_external() -> Result<()> {
84//!     let params = vec![parse!("x"), parse!("y")];
85//!     let mut f = FunctionMap::new();
86//!     f.add_external_function(symbol!("sinh"), "sinh".to_string())
87//!         .unwrap();
88
89//!     let ev = parse!("sinh(x+y)")
90//!         .evaluator(&f, &params, OptimizationSettings::default())
91//!         .unwrap()
92//!         .map_coeff(&|x| Complex::new(x.re.to_f64(), x.im.to_f64()));
93
94//!     let mut runner = CompiledComplexRunner::compile(&ev, Config::default())?;
95//!     let args = [Complex::new(1.0, 2.0), Complex::new(2.0, -1.0)];
96//!     let mut outs = [Complex::<f64>::default(); 1];
97//!     runner.evaluate(&args, &mut outs);
98//!     assert_eq!(outs[0], Complex::new(3.0, 1.0).sinh());
99//!     Ok(())
100//! }
101//! ```
102//!
103
104use anyhow::Result;
105
106pub use runners::{
107    CompiledComplexRunner, CompiledRealRunner, InterpretedComplexRunner, InterpretedRealRunner,
108};
109use symjit::{compiler, instruction, Compiler, Translator};
110pub use symjit::{Application, Complex, ComplexFloat, Config, Defuns};
111
112use symbolica::evaluate::{BuiltinSymbol, ExpressionEvaluator, Instruction, Slot};
113
114mod runners;
115
116fn slot(s: Slot) -> compiler::Slot {
117    match s {
118        Slot::Param(id) => instruction::Slot::Param(id),
119        Slot::Out(id) => instruction::Slot::Out(id),
120        Slot::Const(id) => instruction::Slot::Const(id),
121        Slot::Temp(id) => instruction::Slot::Temp(id),
122    }
123}
124
125fn slot_list(v: &[Slot]) -> Vec<instruction::Slot> {
126    v.iter()
127        .map(|s| slot(*s))
128        .collect::<Vec<instruction::Slot>>()
129}
130
131fn builtin_symbol(s: BuiltinSymbol) -> instruction::BuiltinSymbol {
132    instruction::BuiltinSymbol(s.get_symbol().get_id())
133}
134
135fn translate(
136    instructions: Vec<Instruction>,
137    constants: Vec<Complex<f64>>,
138    mut config: Config,
139    df: Defuns,
140) -> Result<Translator> {
141    let mut translator = Translator::new(config, df);
142    // config.set_defuns(df);
143    // let mut translator = Translator::new(config);
144
145    for z in constants {
146        translator.append_constant(z)?;
147    }
148
149    for q in instructions {
150        match q {
151            Instruction::Add(lhs, args, num_reals) => {
152                translator.append_add(&slot(lhs), &slot_list(&args), num_reals)?
153            }
154            Instruction::Mul(lhs, args, num_reals) => {
155                translator.append_mul(&slot(lhs), &slot_list(&args), num_reals)?
156            }
157            Instruction::Pow(lhs, arg, p, is_real) => {
158                translator.append_pow(&slot(lhs), &slot(arg), p, is_real)?
159            }
160            Instruction::Powf(lhs, arg, p, is_real) => {
161                translator.append_powf(&slot(lhs), &slot(arg), &slot(p), is_real)?
162            }
163            Instruction::Assign(lhs, rhs) => translator.append_assign(&slot(lhs), &slot(rhs))?,
164            Instruction::Fun(lhs, fun, arg, is_real) => {
165                translator.append_fun(&slot(lhs), &builtin_symbol(fun), &slot(arg), is_real)?
166            }
167            Instruction::Join(lhs, cond, true_val, false_val) => translator.append_join(
168                &slot(lhs),
169                &slot(cond),
170                &slot(true_val),
171                &slot(false_val),
172            )?,
173            Instruction::Label(id) => translator.append_label(id)?,
174            Instruction::IfElse(cond, id) => translator.append_if_else(&slot(cond), id)?,
175            Instruction::Goto(id) => translator.append_goto(id)?,
176            Instruction::ExternalFun(lhs, op, args) => {
177                translator.append_external_fun(&slot(lhs), &op, &slot_list(&args))?
178            }
179        }
180    }
181
182    Ok(translator)
183}
184
185pub trait Number {
186    fn as_complex(&self) -> Complex<f64>;
187}
188
189impl Number for Complex<f64> {
190    fn as_complex(&self) -> Complex<f64> {
191        *self
192    }
193}
194
195impl Number for f64 {
196    fn as_complex(&self) -> Complex<f64> {
197        Complex::new(*self, 0.0)
198    }
199}
200
201pub fn compile<T: Clone + Number>(
202    ev: &ExpressionEvaluator<T>,
203    config: Config,
204    df: Defuns,
205    num_params: usize,
206) -> Result<Application> {
207    let (instructions, _, constants) = ev.export_instructions();
208    let constants: Vec<Complex<f64>> = constants.iter().map(|x| x.as_complex()).collect();
209    let mut translator = translate(instructions, constants, config, df).unwrap();
210    translator.set_num_params(num_params);
211    translator.compile()
212}
213
214pub fn compile_string(
215    model: String,
216    config: Config,
217    df: Defuns,
218    num_params: usize,
219) -> Result<Application> {
220    let mut comp = Compiler::with_config(config);
221    comp.translate(model, df, num_params)
222}