1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
//! Robotics Code Generation — DH parameters to optimized Rust code.
//!
//! This example demonstrates the complete symplex robotics pipeline:
//!
//! 1. Define joint angles and link lengths as exact rationals
//! 2. Specify DH parameters for a 3-DOF planar arm
//! 3. Compute forward kinematics (symbolic end-effector position)
//! 4. Print the symbolic FK expressions
//! 5. Compute the analytical Jacobian (2×3)
//! 6. Generate optimized Rust code with cross-entry CSE
//! 7. Print the generated code
//! 8. Verify numerically at a specific joint configuration
//!
//! Run with: cargo run --example robotics_codegen
use std::time::Instant;
use symplex::matrix::jacobian;
use symplex::prelude::*;
use symplex::robotics::*;
fn main() {
println!("=== Symplex Robotics Code Generation ===\n");
// ── 1. Define joint variables and link lengths ─────────────────
//
// Joint angles are symbolic variables — they will become function
// parameters in the generated code.
//
// Link lengths are exact rationals. Using ctx.rational()
// instead of f64 keeps the entire derivation exact: no IEEE 754
// rounding until the very end when we evaluate numerically.
let ctx = Context::new();
symplex::syms!(ctx; theta1, theta2, theta3);
let l1 = ctx.rational(3, 10); // 0.3 m
let l2 = ctx.rational(1, 4); // 0.25 m
let l3 = ctx.rational(1, 5); // 0.2 m
println!("Link lengths: L1 = {l1}, L2 = {l2}, L3 = {l3}");
println!("Total reach: {} m", &(&l1 + &l2) + &l3);
// ── 2. DH parameters ───────────────────────────────────────────
//
// Standard DH convention: each joint is a `DhLink` with four named
// parameters: theta (joint angle), d (link offset), a (link length),
// alpha (link twist).
//
// For a planar arm, d = 0 and alpha = 0 for every joint.
// Only theta (joint angle) and a (link length) vary.
let zero = ctx.int(0);
let dh: [DhLink<'_>; 3] = [
DhLink {
theta: &theta1,
d: &zero,
a: &l1,
alpha: &zero,
}, // Joint 1
DhLink {
theta: &theta2,
d: &zero,
a: &l2,
alpha: &zero,
}, // Joint 2
DhLink {
theta: &theta3,
d: &zero,
a: &l3,
alpha: &zero,
}, // Joint 3
];
println!("\nDH parameters (theta, d, a, alpha):");
for (i, link) in dh.iter().enumerate() {
println!(
" Joint {}: ({}, {}, {}, {})",
i + 1,
link.theta,
link.d,
link.a,
link.alpha
);
}
// ── 3. Forward kinematics ──────────────────────────────────────
//
// Chain-multiply the DH transformation matrices to get the
// end-effector position as symbolic expressions of the joint angles.
//
// fk_position() returns (px, py, pz). For a planar arm, pz = 0.
println!("\n--- Forward Kinematics ---");
let t0 = Instant::now();
let (px, py, _pz) = fk_position(&dh);
let fk_time = t0.elapsed();
println!("FK computed in {fk_time:?}");
// ── 4. Print symbolic FK expressions ───────────────────────────
//
// These are trigonometric sums of the form:
// px = L1·cos(θ1) + L2·cos(θ1+θ2) + L3·cos(θ1+θ2+θ3)
// py = L1·sin(θ1) + L2·sin(θ1+θ2) + L3·sin(θ1+θ2+θ3)
//
// The exact form depends on how symplex canonicalizes the
// expanded trig expressions.
println!("\nEnd-effector position (symbolic):");
println!(" px = {px}");
println!(" py = {py}");
// Also show the LaTeX form for documentation
println!("\nLaTeX:");
println!(" p_x = {}", px.to_latex());
println!(" p_y = {}", py.to_latex());
// ── 5. Compute the Jacobian ────────────────────────────────────
//
// The Jacobian J maps joint velocities to end-effector velocities:
// [ẋ] [∂px/∂θ1 ∂px/∂θ2 ∂px/∂θ3] [θ̇1]
// [ẏ] = J · [θ̇2] where J = [∂py/∂θ1 ∂py/∂θ2 ∂py/∂θ3]
//
// This is a 2×3 matrix (2 task-space DOFs, 3 joint-space DOFs).
//
// The jacobian() function computes each ∂fᵢ/∂θⱼ symbolically.
println!("\n--- Jacobian (2×3) ---");
let t1 = Instant::now();
let jac = jacobian(&[&px, &py], &[&theta1, &theta2, &theta3]);
let jac_time = t1.elapsed();
println!("Jacobian computed in {jac_time:?}");
println!("\n{jac}");
// Show individual entries
println!("\nJacobian entries:");
for i in 0..2 {
for j in 0..3 {
let label = if i == 0 { "x" } else { "y" };
println!(" ∂p{label}/∂θ{} = {}", j + 1, jac.get(i, j));
}
}
// ── 6. Generate optimized Rust code ────────────────────────────
//
// to_rust_fn() performs:
// - Common Subexpression Elimination (CSE) across ALL matrix
// entries simultaneously — shared trig calls like sin(θ1+θ2)
// are computed once
// - Constant folding — sin(0)→0, cos(0)→1 at codegen time
// - Clean decimal output — 0.3, not 0.30000000000000004
// - Proper subtraction — "x - 0.3", not "x + (-0.3)"
//
// The result is a complete pub fn returning [f64; 6] in row-major order.
println!("\n--- Code Generation ---");
let t2 = Instant::now();
let code = jac
.to_rust_fn("robot_jacobian", &["theta1", "theta2", "theta3"])
.expect("codegen failed");
let codegen_time = t2.elapsed();
println!(
"Code generated in {codegen_time:?} ({} bytes)\n",
code.len()
);
println!("{code}");
// Also generate code for the FK position itself
println!("--- FK Position Code ---");
let px_code = px
.to_rust_fn("fk_x", &["theta1", "theta2", "theta3"])
.expect("codegen failed");
println!("{px_code}");
let py_code = py
.to_rust_fn("fk_y", &["theta1", "theta2", "theta3"])
.expect("codegen failed");
println!("{py_code}");
// ── 7. Generate with different options ─────────────────────────
println!("--- Embedded f32 Variant ---");
let embedded_code = jac
.to_rust_fn_with_options(
"robot_jacobian_f32",
&["theta1", "theta2", "theta3"],
&symplex::matrix::CodegenOptions::embedded_f32(),
)
.expect("codegen failed");
println!("{embedded_code}");
// ── 8. Numerical verification ──────────────────────────────────
//
// Verify the symbolic FK at a specific configuration by:
// (a) evaluating the symbolic expression
// (b) computing FK from first principles using f64 trig
// (c) comparing the results
//
// This is the verification step you should always perform after
// code generation to catch any derivation or codegen bugs.
println!("--- Numerical Verification ---");
let test_configs: &[(f64, f64, f64)] = &[
(0.0, 0.0, 0.0), // fully extended along +x
(std::f64::consts::FRAC_PI_4, 0.0, 0.0), // 45° first joint
(0.5, 0.3, 0.1), // arbitrary configuration
];
for (t1, t2, t3) in test_configs {
println!("\n Config: θ = ({t1:.4}, {t2:.4}, {t3:.4})");
// Ground truth: direct f64 computation
let l1_f = 0.3;
let l2_f = 0.25;
let l3_f = 0.2;
let gt_x = l1_f * t1.cos() + l2_f * (t1 + t2).cos() + l3_f * (t1 + t2 + t3).cos();
let gt_y = l1_f * t1.sin() + l2_f * (t1 + t2).sin() + l3_f * (t1 + t2 + t3).sin();
println!(" Ground truth: px = {gt_x:.6}, py = {gt_y:.6}");
// Symbolic evaluation (substitute integer approximations for display)
// For exact comparison we use compile()
if let Ok(px_fn) = px.compile(&["theta1", "theta2", "theta3"]) {
let sym_x = px_fn(&[*t1, *t2, *t3]);
let sym_y = py
.compile(&["theta1", "theta2", "theta3"])
.map(|f| f(&[*t1, *t2, *t3]))
.unwrap_or(f64::NAN);
println!(" Symbolic eval: px = {sym_x:.6}, py = {sym_y:.6}");
let err_x = (sym_x - gt_x).abs();
let err_y = (sym_y - gt_y).abs();
println!(" Error: Δx = {err_x:.2e}, Δy = {err_y:.2e}");
if err_x < 1e-10 && err_y < 1e-10 {
println!(" ✓ Match!");
} else {
println!(" ✗ MISMATCH — investigate!");
}
}
// Also evaluate the Jacobian numerically
// For the fully extended config, ∂px/∂θ1 should be
// -(L1·sin(θ1) + L2·sin(θ1+θ2) + L3·sin(θ1+θ2+θ3))
// At θ=0,0,0 that's 0 (since sin(0)=0).
}
// ── 9. Timing summary ──────────────────────────────────────────
let total = fk_time + jac_time + codegen_time;
println!("\n--- Timing Summary ---");
println!(" FK derivation: {fk_time:?}");
println!(" Jacobian: {jac_time:?}");
println!(" Code generation: {codegen_time:?}");
println!(" Total: {total:?}");
println!("\n (This runs once at design time or in build.rs.)");
println!(" (The generated code runs at >1 MHz in your control loop.)");
// ── 10. Full transformation matrix ─────────────────────────────
//
// For reference, you can also get the full 4×4 homogeneous
// transformation matrix and extract the rotation submatrix.
println!("\n--- Full FK Chain ---");
let t_full = fk_chain(&dh);
println!("T(4×4) shape: {:?}", t_full.shape());
// The rotation submatrix gives end-effector orientation
let rot = fk_rotation(&dh);
println!("Rotation (3×3):");
println!("{rot}");
println!("\n✓ Done!");
}