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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
//! Risch–Norman ("parallel Risch") heuristic integration.
//!
//! # What this is
//!
//! Instead of building a differential-field tower and recursing on it — what
//! [`crate::integrate::risch`] does — the parallel-Risch heuristic *posits* the
//! shape of the answer,
//!
//! ```text
//! F = Σᵢ cᵢ·mᵢ + Σⱼ dⱼ·log(pⱼ)
//! ```
//!
//! differentiates it, and equates the result against the integrand. The
//! unknown constants `cᵢ`, `dⱼ` enter linearly, so the whole problem collapses
//! to **one linear system over `ℚ`**. The method is due to Norman & Moore
//! (1977); Davenport, *The parallel Risch algorithm (I)* (1982) is the standard
//! write-up, and Geddes, Czapor & Labahn ch. 12 the textbook treatment.
//!
//! It is the default integrator in Maple (`int`), Axiom/FriCAS (Norman–Moore)
//! and Reduce, with full Risch behind it, because it is cheap and it closes a
//! large fraction of everyday integrands without any tower construction.
//!
//! # The contract — a decline is not a verdict
//!
//! [`integrate_parallel_risch`] returns [`ParallelRischOutcome`], which has
//! exactly two shapes: [`ParallelRischOutcome::Solved`] carries a
//! **verification-gated** antiderivative, and
//! [`ParallelRischOutcome::Declined`] carries a [`DeclineReason`].
//!
//! **There is deliberately no variant that a caller could read as a proof of
//! non-elementarity.** A Risch–Norman failure means "my ansatz did not
//! close" — the monomial box was too small, the logarithm candidate set did
//! not contain the right argument, the constant field was too small (`ℚ`
//! only — see the limitations below), or the structure-theorem precondition
//! could not be certified. `∫exp(x²)dx` and `∫dx/(x²+1)` both decline here;
//! the first has no elementary antiderivative and the second is `arctan x`.
//! Nothing in the return type distinguishes them, because this module cannot.
//! [`DeclineReason::into_integration_error`] therefore maps *every* decline to
//! [`IntegrationError::NotImplemented`], never to `NonElementary`.
//!
//! (This is the pattern that `solve_rational_rde_generalized` got wrong: its
//! `None` was documented as certifying non-elementarity and was converted
//! straight into a certificate. A heuristic's silence is not evidence.)
//!
//! # Soundness
//!
//! Two independent guards:
//!
//! 1. **Algebraic independence up front.** Equating coefficients of monomials
//! in the generators is only valid when those generators are algebraically
//! independent over `ℚ(x)` (Bronstein, *Structure theorems for parallel
//! integration*, JSC 42(7):757–769, 2007). The `ring` submodule reduces
//! exponential arguments to a `ℤ`-lattice basis and checks logarithm
//! arguments for
//! multiplicative independence modulo constants, and declines when it
//! cannot certify the condition. See that module for the details.
//! 2. **`d/dx F = f` before return**, in two stages. First the *rebuilt*
//! candidate is differentiated with the general-purpose `diff` — not with
//! the ring's own derivation table, so a bug in that table cannot hide —
//! read back into `ℚ(x, θ)`, and required to equal `f` exactly as a
//! rational function. A candidate that cannot be re-read into the ring
//! falls through to [`crate::integrate::verify_antiderivative_status`],
//! the engine's own gate, which also accepts agreement at several numeric
//! sample points; [`ParallelRischOutcome::Solved`] records which of the two
//! established the identity. A candidate that passes neither is discarded
//! and the call declines. The construction is sound by design; the gates
//! are there because construction arguments have bugs.
//!
//! A singular or inconsistent linear system declines
//! ([`DeclineReason::NoSolution`]); it never produces a partial answer. The
//! solver keeps "this system has no solution" and "I gave up"
//! ([`DeclineReason::LinearSolver`]) apart, and separately substitutes its
//! answer back into the untouched system before returning it — so a solver bug
//! costs a decline rather than a wrong antiderivative.
//!
//! # Coverage
//!
//! Integrands that are **rational functions of `x` and of exp/log
//! generators**, whose antiderivative is a rational function of the same
//! generators plus `ℚ`-multiples of logarithms of ring elements. In practice
//! that means:
//!
//! * everything the rational-function integrator gets without needing
//! `RootSum` or `arctan`;
//! * mixed exp/log integrands where tower construction is the hard part but
//! the answer's shape is simple — `∫exp(x)/(exp(x)+1)`,
//! `∫exp(2x)/(exp(x)+1)`, `∫dx/(1+exp(−x))`, `∫dx/(x·log x)`;
//! * polynomial-times-exponential and polynomial-times-logarithm families.
//!
//! # Limitations
//!
//! In the register of [`crate::holonomic`]: this list is what the module
//! *cannot* do, stated so that a decline is never mistaken for a verdict.
//!
//! * **The constant field is `ℚ`.** Answers needing `ℚ(i)` (`arctan`,
//! `∫dx/(x²+1)`), a real quadratic field (`∫√2`-scaled logarithms, the
//! classical `∫√(tan x)`), or algebraic residues (`RootSum`, which the
//! existing rational integrator does produce) are out of reach. This is the
//! single biggest source of declines on textbook input.
//! * **No trigonometric, inverse-trigonometric, hyperbolic or algebraic
//! generators.** The ring is `ℚ(x, exp …, log …)` only. `sin`, `cos`,
//! `tan`, `atan`, `sqrt` and any non-integer power decline immediately.
//! Trigonometric coverage would need either a `ℚ(i)` constant field with
//! complex exponentials or explicit `tan` monomials; neither is implemented.
//! * **The ansatz is a box, not a bound.** The monomial degrees are bounded
//! heuristically by `deg(N) + deg(Q) + 1` per variable, and the logarithm
//! arguments are drawn from the irreducible factors of the denominator
//! together with `x` and the tower's logarithm generators. An elementary
//! antiderivative outside that set is not found. No claim of completeness
//! is made or intended — that is what the full Risch algorithm is for.
//! * **The independence checks are conservative.** A tower whose generators
//! *are* independent but which the checks cannot certify is declined rather
//! than attempted. `∫f(log 2x, log x)` is the canonical example.
//! * **Size caps.** The expression's nesting depth, the tower's generator and
//! atom count, the ansatz box, the common denominator, the linear system and
//! the solver's fill-in are all capped (`ring::MAX_DEPTH`,
//! `ring::MAX_GENERATORS`, `ring::MAX_ATOMS`, the constants in `ansatz`, and
//! `solve::MAX_CELLS`). Every one of them carries the measurement it was set
//! against; exceeding any declines with [`DeclineReason::TooLarge`] rather
//! than running to exhaustion. What is *not* capped is wall-clock time:
//! there is no budget hook here, so the ceilings are all structural proxies.
//! * **It is not wired into [`crate::integrate::integrate`].** This is a
//! separate entry point on purpose, so its coverage can be measured against
//! the existing engine before any routing change is made.
use fmt;
use crate;
use crate;
pub
pub
pub
pub
/// Why the heuristic declined.
///
/// **None of these is a statement about the integrand's integrability.** See
/// the module documentation.
/// The result of a Risch–Norman attempt.
///
/// Note what is *absent*: there is no variant meaning "no elementary
/// antiderivative exists". A [`ParallelRischOutcome::Declined`] can only ever
/// be read as "this heuristic did not find one".
/// Integrate `expr` with respect to `var` using the Risch–Norman heuristic.
///
/// Returns a verification-gated antiderivative or a [`DeclineReason`]. Read
/// the module documentation before wiring this into anything: a decline is not
/// a non-elementarity verdict.
///
/// ```
/// use alkahest_cas::kernel::{Domain, ExprPool};
/// use alkahest_cas::integrate::norman::{integrate_parallel_risch, ParallelRischOutcome};
///
/// let pool = ExprPool::new();
/// let x = pool.symbol("x", Domain::Real);
/// // ∫ exp(x)/(exp(x) + 1) dx = log(exp(x) + 1)
/// let e = pool.func("exp", vec![x]);
/// let den = pool.add(vec![e, pool.integer(1_i32)]);
/// let f = pool.mul(vec![e, pool.pow(den, pool.integer(-1_i32))]);
/// assert!(matches!(
/// integrate_parallel_risch(f, x, &pool),
/// ParallelRischOutcome::Solved { .. }
/// ));
/// ```
/// `d/dx candidate == f` as an exact identity in the ring.
///
/// Returns `false` — never an error — when the derivative falls outside the
/// ring, so a candidate that cannot be re-read simply falls through to the
/// engine's gate rather than being accepted on trust.
/// The unverified half of [`integrate_parallel_risch`].