| 237 | const hi = numAt(ce, f, c.varName, a0 + H), lo = numAt(ce, f, c.varName, a0 - H); |
| 238 | if (hi == null || lo == null || Math.abs(hi - lo) > 1e-3 * (1 + Math.abs(hi))) return [null]; |
| 239 | return [(hi + lo) / 2]; |
| 240 | } |
| 241 | if (c.op === 'defint') { // composite Simpson |
| 242 | const a0 = Number(c.a), b0 = Number(c.b), n = 200, h = (b0 - a0) / n; |
| 243 | let sum = 0; |
| 244 | for (let i = 0; i <= n; i++) { |
| 245 | const y = numAt(ce, f, c.varName, a0 + i * h); |
| 246 | if (y == null) return [null]; |
| 247 | sum += (i === 0 || i === n ? 1 : i % 2 ? 4 : 2) * y; |
| 248 | } |
| 249 | return [(h / 3) * sum]; |
| 250 | } |
| 251 | return POINTS.map((p) => numAt(ce, f, c.varName, p)); // factor/expand/simplify |
| 252 | } |
| 253 | |
| 254 | // --- run one configuration -------------------------------------------------- |
| 255 | // When `engine` has the Rubi rules loaded (the CE+R/F engine), `Integrate` |
| 256 | // consults them automatically, so every op runs through the same code path on |
| 257 | // every engine. |
| 258 | function runOn(engine: any, c: Case) { |
| 259 | try { |
| 260 | if (c.op === 'integrate') { |
| 261 | const build = () => engine.expr(['Integrate', c.arg, ['Tuple', c.varName]]).evaluate(); |
| 262 | const timeMs = timeit(build); |
| 263 | const F = build(); |
| 264 | if (F == null || F.operator === 'Integrate' || /\bint\(/.test(F.toString())) return { status: 'unsolved', text: F ? F.toString() : 'null', values: [], timeMs }; |
| 265 | const dF = engine.expr(['D', F, c.varName]).evaluate(); |
| 266 | return { status: 'ok', text: F.toString(), values: POINTS.map((p) => numAt(engine, dF, c.varName, p)), timeMs }; |
| 267 | } |
| 268 | if (c.op === 'defint') { |
| 269 | const build = () => engine.expr(['Integrate', c.arg, ['Tuple', c.varName, c.a, c.b]]); |
| 270 | const timeMs = timeit(() => build().N()); |
| 271 | const v = numOf(build()); |
| 272 | return v == null ? { status: 'unsolved', text: build().toString(), values: [], timeMs } : { status: 'ok', text: build().N().toString(), values: [v], timeMs }; |
| 273 | } |
| 274 | if (c.op === 'diff') { |
| 275 | const build = () => engine.expr(['D', c.arg, c.varName]).evaluate(); |
| 276 | const timeMs = timeit(build); const r = build(); |
| 277 | return { status: 'ok', text: r.toString(), values: POINTS.map((p) => numAt(engine, r, c.varName, p)), timeMs }; |
| 278 | } |
| 279 | if (c.op === 'limit') { |
| 280 | const build = () => engine.expr(['Limit', ['Function', c.arg, c.varName], c.point]); |
| 281 | const timeMs = timeit(() => build().N()); |
| 282 | const v = numOf(build()); |
| 283 | return v == null ? { status: 'unsolved', text: build().evaluate().toString(), values: [], timeMs } : { status: 'ok', text: build().N().toString(), values: [v], timeMs }; |
| 284 | } |
| 285 | if (c.op === 'solve') { |
| 286 | // The public API is the `.solve()` method (the `Solve` operator doesn't |
| 287 | // auto-evaluate). It returns the *real* roots. |
| 288 | const build = () => engine.expr(c.arg).solve(c.varName); |
| 289 | const timeMs = timeit(() => build()); |
| 290 | const roots: any[] = build() || []; |
| 291 | if (!roots.length) return { status: 'unsolved', text: '[]', values: [], roots: [], timeMs }; |
| 292 | const resid = engine.expr(c.arg); |
| 293 | const realRoots: number[] = [], resid_mag: number[] = []; |
| 294 | for (const root of roots) { |
| 295 | try { |
| 296 | const rv = root.N(); const im = typeof rv.im === 'number' ? rv.im : 0; |