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
//! Integration tests for the LambertW function and LambertW-based equation solving.
//!
//! The LambertW function W(x) satisfies W(x)·exp(W(x)) = x.
//!
//! # Note on solve tests
//!
//! The public `Ex::solve()` / `Ex::solve_or_empty()` API currently gates on
//! polynomial convertibility before dispatching to the internal solver.
//! Transcendental solvers (inversion peeling, change-of-variable, LambertW)
//! are exercised through the internal `solve::solve` function, which is
//! tested via unit tests in `src/solve.rs`. The integration tests here
//! focus on LambertW *evaluation* and non-panic robustness through the
//! public API.
// ═══════════════════════════════════════════════════════════════════════════
// LambertW evaluation at known values
// ═══════════════════════════════════════════════════════════════════════════
use symplex::prelude::*;
#[test]
fn lambertw_eval_at_zero() {
let ctx = Context::new();
// W(0) = 0 because 0·exp(0) = 0
let result = ctx.int(0).lambertw().eval();
assert_eq!(format!("{result}"), "0");
}
#[test]
fn lambertw_eval_at_e() {
let ctx = Context::new();
// W(e) = 1 because 1·exp(1) = e
let result = ctx.e().lambertw().eval();
assert_eq!(format!("{result}"), "1");
}
#[test]
fn lambertw_symbolic_stays_symbolic() {
let ctx = Context::new();
// W(5) has no closed form — should remain as lambertw(5)
let result = ctx.int(5).lambertw().eval();
let s = format!("{result}");
assert!(s.contains("W("), "W(5) should stay symbolic, got: {s}");
}
#[test]
fn lambertw_of_negative_stays_symbolic() {
let ctx = Context::new();
// W(-1) has no simple closed form on the principal branch
let result = ctx.int(-1).lambertw().eval();
let s = format!("{result}");
assert!(s.contains("W("), "W(-1) should stay symbolic, got: {s}");
}
// ═══════════════════════════════════════════════════════════════════════════
// LambertW symbolic construction and display
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn lambertw_display_format() {
let ctx = Context::new();
let expr = ctx.int(3).lambertw();
let s = format!("{expr}");
assert!(
s.contains("W(") && s.contains("3"),
"display should show W(3), got: {s}"
);
}
#[test]
fn lambertw_of_expression() {
let ctx = Context::new();
// W(x + 1) should display sensibly and not panic
let x = ctx.symbol("x");
let expr = (&x + 1).lambertw();
let s = format!("{expr}");
assert!(s.contains("W("), "W(x+1) should display with W, got: {s}");
}
#[test]
fn lambertw_nested_eval() {
let ctx = Context::new();
// W(W(e)) = W(1) — since W(e)=1, W(W(e)) = W(1) which stays symbolic
let inner = ctx.e().lambertw().eval(); // = 1
let outer = inner.lambertw().eval(); // = W(1) ... but 1·exp(1) = e ≠ 1, so W(1) ≠ 1
// W(1) ≈ 0.5671; stays symbolic since no closed form
// But inner evaluated to 1, so this is W(1)
let s = format!("{outer}");
// W(1) should either stay as lambertw(1) or evaluate — either is fine
assert!(
s.contains("W(") || s.parse::<f64>().is_ok(),
"W(W(e)) = W(1) should be representable, got: {s}"
);
}
// ═══════════════════════════════════════════════════════════════════════════
// Robustness: public solve_or_empty should not panic on transcendental eqs
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn solve_or_empty_no_panic_on_x_exp_x() {
let ctx = Context::new();
// x·exp(x) - 1 is not polynomial, so public solve_or_empty returns []
// (the internal solver handles it — see unit tests in solve.rs).
// Key assertion: it must not panic.
let x = ctx.symbol("x");
let eq = &x * &x.exp() - 1;
let _roots = eq.solve_or_empty(&x);
// No panic = success
}
#[test]
fn solve_or_empty_no_panic_on_exp_plus_linear() {
let ctx = Context::new();
// exp(x) + x - 2 is not polynomial
let x = ctx.symbol("x");
let eq = x.exp() + &x - 2;
let _roots = eq.solve_or_empty(&x);
// No panic = success
}
#[test]
fn solve_or_empty_no_panic_on_x2_exp_x() {
let ctx = Context::new();
// x²·exp(x) - 1 is not polynomial and not a LambertW pattern either
let x = ctx.symbol("x");
let eq = x.powi(2) * &x.exp() - 1;
let _roots = eq.solve_or_empty(&x);
// No panic = success
}
// ═══════════════════════════════════════════════════════════════════════════
// Polynomial solve still works (regression guard)
// ═══════════════════════════════════════════════════════════════════════════
#[test]
fn polynomial_solve_unaffected() {
let ctx = Context::new();
// x² - 1 = 0 → x = ±1 (polynomial path, must still work)
let x = ctx.symbol("x");
let eq = x.powi(2) - 1;
let roots = eq.solve_or_empty(&x);
assert_eq!(roots.len(), 2, "x²-1 should still yield 2 roots");
let mut vals: Vec<String> = roots.iter().map(|r| format!("{r}")).collect();
vals.sort();
assert_eq!(vals, vec!["-1", "1"], "roots of x²-1: {vals:?}");
}