Fuse factors sharing a NON-NUMERIC exponent: aˣ·bˣ → (a·b)ˣ. Sound for the * Rubi verification regime (positive-real parameters), and needed so a product * of distinct-base exponentials (`aˣ·bˣ`, `aˣ/bˣ`) presents a single base to * FunctionOfExponential. Restricted to symbolic exponents so numer
( ce: ComputeEngine, flat: Expression[] )
| 78 | * factors (`√2·√3`, `x²·y²`) are left to CE's own canonicalization, keeping the |
| 79 | * blast radius to exponential integrands. */ |
| 80 | function collectSameExponent( |
| 81 | ce: ComputeEngine, |
| 82 | flat: Expression[] |
| 83 | ): Expression[] { |
| 84 | // Group powers by their SIGN-CANONICALIZED exponent (|exp|) so aˣ·bˣ → (ab)ˣ |
| 85 | // and aˣ/bˣ (= aˣ·b⁻ˣ) → (a/b)ˣ both fuse: positive-sign bases go to the |
| 86 | // numerator, negative-sign to the denominator. |
| 87 | const groups = new Map< |
| 88 | string, |
| 89 | { |
| 90 | exp: Expression; |
| 91 | items: { f: Expression; base: Expression; sign: 1 | -1 }[]; |
| 92 | } |
| 93 | >(); |
| 94 | const order: string[] = []; |
| 95 | const passthrough: Expression[] = []; |
| 96 | for (const f of flat) { |
| 97 | const isPow = f.operator === 'Power' && f.ops; |
| 98 | const exp = isPow ? f.ops![1] : ce.One; |
| 99 | // only fuse genuine (symbolic-exponent) powers; everything else is kept |
| 100 | if (!isPow || isNumber(exp)) { |
| 101 | passthrough.push(f); |
| 102 | continue; |
| 103 | } |
| 104 | const [canon, sign] = splitSign(ce, exp); |
| 105 | const key = canon.toString(); |
| 106 | if (!groups.has(key)) { |
| 107 | groups.set(key, { exp: canon, items: [] }); |
| 108 | order.push(key); |
| 109 | } |
| 110 | groups.get(key)!.items.push({ f, base: f.ops![0], sign }); |
| 111 | } |
| 112 | if ([...groups.values()].every((g) => g.items.length === 1)) return flat; |
| 113 | const out: Expression[] = [...passthrough]; |
| 114 | for (const key of order) { |
| 115 | const { exp, items } = groups.get(key)!; |
| 116 | if (items.length === 1) { |
| 117 | out.push(items[0].f); // singleton: keep the original factor verbatim |
| 118 | continue; |
| 119 | } |
| 120 | const num = items.filter((i) => i.sign === 1).map((i) => i.base); |
| 121 | const den = items.filter((i) => i.sign === -1).map((i) => i.base); |
| 122 | let base: Expression = num.length ? mul(ce, num) : ce.One; |
| 123 | if (den.length) base = ce._fn('Divide', [base, mul(ce, den)]); |
| 124 | out.push(pow(ce, base, exp)); // exp is the canonical (positive) exponent |
| 125 | } |
| 126 | return out; |
| 127 | } |
| 128 | |
| 129 | /** Split an exponent into (|exponent|, sign), canonicalizing through `mul` so |
| 130 | * `−1·x` keys identically to `x` (the raw `negate` leaves a `1·x` artifact). */ |