Skip to content

Commit 870fefb

Browse files
committed
feat: implement solving for equations with two sqrt terms and add corresponding tests
1 parent bccae5c commit 870fefb

5 files changed

Lines changed: 441 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@
1313
ce.parse('\\sqrt{x} = x').solve('x'); // → [0, 1]
1414
```
1515

16+
- **Two Sqrt Equation Solving**: The equation solver now handles equations
17+
with two sqrt terms of the form `√(f(x)) + √(g(x)) = e` using double squaring.
18+
Both addition and subtraction forms are supported, and extraneous roots are
19+
automatically filtered.
20+
21+
```javascript
22+
ce.parse('\\sqrt{x+1} + \\sqrt{x+4} = 3').solve('x'); // → [0]
23+
ce.parse('\\sqrt{x} + \\sqrt{x+7} = 7').solve('x'); // → [9]
24+
ce.parse('\\sqrt{x+5} - \\sqrt{x-3} = 2').solve('x'); // → [4]
25+
ce.parse('\\sqrt{2x+1} + \\sqrt{x-1} = 4').solve('x'); // → [46 - 8√29] ≈ 2.919
26+
```
27+
1628
- **Nested Sqrt Equation Solving**: The equation solver now handles nested
1729
sqrt equations of the form `√(x + √x) = a` using substitution. These patterns
1830
have √x inside the argument of an outer sqrt. The solver uses u = √x

requirements/DONE.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,3 +1440,147 @@ ce.assume(ce.box(['Less', 'x', 0])); // → 'contradiction' (x > 4 contradi
14401440

14411441
- `test/compute-engine/assumptions.test.ts` - Enabled "TAUTOLOGY AND
14421442
CONTRADICTION DETECTION" describe block (4 tests)
1443+
1444+
---
1445+
1446+
### 23. Replace Method Auto-Wildcards Fix ✅
1447+
1448+
**FIXED:** The `.replace()` method no longer auto-converts single-character
1449+
symbols to wildcards when using object rules. This fixes issue #23.
1450+
1451+
**Problem:** When using `.replace({match: 'a', replace: 2})`, the symbol `'a'`
1452+
was being auto-converted to wildcard `'_a'`, causing it to match ANY expression
1453+
instead of just the literal symbol `a`.
1454+
1455+
**Solution:** Added `autoWildcard` parameter to `parseRulePart()`. Auto-wildcarding
1456+
is now only enabled when parsing string rules (like `"a*x -> 2*x"`), not when
1457+
parsing object rules.
1458+
1459+
**Examples that now work correctly:**
1460+
1461+
```typescript
1462+
const expr = ce.box(['Add', ['Multiply', 'a', 'x'], 'b']);
1463+
1464+
// Object rules: literal matching
1465+
expr.replace({match: 'a', replace: 2}, {recursive: true});
1466+
// → 2x + b (was: 2 - incorrectly matched entire expression)
1467+
1468+
// String rules: still auto-wildcard
1469+
expr.replace('a*x -> 5*x', {recursive: true});
1470+
// → 5x + b (works as before)
1471+
1472+
// Explicit wildcards in object rules still work
1473+
expr.replace({match: ['Multiply', '_a', 'x'], replace: 10}, {recursive: true});
1474+
// → 10 + b
1475+
```
1476+
1477+
**Files modified:**
1478+
1479+
- `src/compute-engine/boxed-expression/rules.ts` - Added `autoWildcard` parameter
1480+
to `parseRulePart()`, defaults to `false` for object rules
1481+
- `test/compute-engine/rules.test.ts` - Added 5 new tests in "OBJECT RULES
1482+
LITERAL MATCHING" describe block
1483+
1484+
---
1485+
1486+
### 15. Extended Sqrt Pattern 4: Nested Sqrt Equations ✅
1487+
1488+
**IMPLEMENTED:** The solver now handles nested sqrt equations of the form
1489+
`√(x + √x) = a` and similar patterns where √x appears inside the argument of an
1490+
outer sqrt.
1491+
1492+
**Problem:** Equations like `√(x + 2√x) = 3` or `√(x - √x) = 1` were not being
1493+
solved because they require a substitution approach.
1494+
1495+
**Solution:** Added `solveNestedSqrtEquation()` function that:
1496+
1497+
1. Detects patterns where √x appears inside the argument of an outer √
1498+
2. Uses substitution u = √x, so x = u²
1499+
3. Transforms the equation: `√(u² + ku) = a``u² + ku = a²`
1500+
4. Solves the resulting quadratic for u
1501+
5. Filters out negative u values (since u = √x ≥ 0)
1502+
6. Returns x = u² for valid u values
1503+
1504+
**Key implementation details:**
1505+
1506+
- Uses a unique internal symbol `__internalU` to avoid wildcard interpretation
1507+
- Must replace √x with u BEFORE replacing x with u², otherwise √x becomes √(u²)
1508+
- Uses `isNegative` property on the evaluated u value for filtering
1509+
- Integrates with `validateRoots()` for additional extraneous root filtering
1510+
1511+
**Examples that now work:**
1512+
1513+
```typescript
1514+
ce.parse("\sqrt{x + 2\sqrt{x}} = 3").solve("x") // → [11 - 2√10] ≈ 4.675
1515+
// u² + 2u - 9 = 0 → u = -1 ± √10
1516+
// u = -1 + √10 ≈ 2.16 (valid), u = -1 - √10 ≈ -4.16 (filtered)
1517+
// x = (−1 + √10)² = 11 - 2√10
1518+
1519+
ce.parse("\sqrt{x + \sqrt{x}} = 2").solve("x") // → [9/2 - √17/2] ≈ 2.438
1520+
// u² + u - 4 = 0 → u = (-1 ± √17)/2
1521+
// Only u = (-1 + √17)/2 ≈ 1.56 is valid
1522+
1523+
ce.parse("\sqrt{x - \sqrt{x}} = 1").solve("x") // → [φ²] ≈ 2.618
1524+
// u² - u - 1 = 0 → u = (1 ± √5)/2
1525+
// Only u = φ (golden ratio) ≈ 1.618 is valid
1526+
```
1527+
1528+
**Files modified:**
1529+
1530+
- `src/compute-engine/boxed-expression/solve.ts` - Added `solveNestedSqrtEquation()`
1531+
function and integration in `findUnivariateRoots()`
1532+
- `test/compute-engine/solve.test.ts` - Added 3 new tests in "NESTED SQRT
1533+
EQUATIONS (Pattern 4)" describe block
1534+
- `requirements/TODO.md` - Updated Pattern 4 as implemented
1535+
1536+
---
1537+
1538+
### 15. Extended Sqrt Pattern 3: Two Sqrt Terms ✅
1539+
1540+
**IMPLEMENTED:** The solver now handles equations with two sqrt terms of the
1541+
form `√(f(x)) + √(g(x)) = e` using double squaring.
1542+
1543+
**Problem:** Equations like `√(x+1) + √(x+4) = 3` or `√(x+5) - √(x-3) = 2` were
1544+
not being solved because they require squaring twice to eliminate both sqrts.
1545+
1546+
**Solution:** Added `solveTwoSqrtEquation()` function that:
1547+
1548+
1. Detects equations with exactly two sqrt terms and a constant
1549+
2. Isolates one sqrt: `√(f(x)) = e - √(g(x))`
1550+
3. Squares both sides: `f(x) = e² - 2e√(g(x)) + g(x)`
1551+
4. Isolates remaining sqrt: `f(x) - e² - g(x) = -2e√(g(x))`
1552+
5. Squares again: `(f(x) - e² - g(x))² = 4e²·g(x)`
1553+
6. Solves the resulting polynomial equation
1554+
7. Validates each solution by checking:
1555+
- `f(x) ≥ 0` and `g(x) ≥ 0` (for real sqrts)
1556+
- The original equation holds numerically
1557+
1558+
**Key implementation details:**
1559+
1560+
- Handles both addition (`√f + √g = e`) and subtraction (`√f - √g = e`)
1561+
- Handles negated sqrt terms (`-√f + √g = e`)
1562+
- Validates solutions numerically with tolerance of 1e-9
1563+
- Filters out extraneous roots introduced by squaring
1564+
1565+
**Examples that now work:**
1566+
1567+
```typescript
1568+
ce.parse("\sqrt{x+1} + \sqrt{x+4} = 3").solve("x") // → [0]
1569+
// Verify: √1 + √4 = 1 + 2 = 3 ✓
1570+
1571+
ce.parse("\sqrt{x} + \sqrt{x+7} = 7").solve("x") // → [9]
1572+
// Verify: √9 + √16 = 3 + 4 = 7 ✓
1573+
1574+
ce.parse("\sqrt{x+5} - \sqrt{x-3} = 2").solve("x") // → [4]
1575+
// Verify: √9 - √1 = 3 - 1 = 2 ✓
1576+
1577+
ce.parse("\sqrt{2x+1} + \sqrt{x-1} = 4").solve("x") // → [46 - 8√29] ≈ 2.919
1578+
```
1579+
1580+
**Files modified:**
1581+
1582+
- `src/compute-engine/boxed-expression/solve.ts` - Added `solveTwoSqrtEquation()`
1583+
and `solveTwoSqrtEquationCore()` functions, integrated into `findUnivariateRoots()`
1584+
- `test/compute-engine/solve.test.ts` - Added 5 new tests in "TWO SQRT EQUATIONS
1585+
(Pattern 3)" describe block
1586+
- `requirements/TODO.md` - Updated Pattern 3 as implemented

requirements/TODO.md

Lines changed: 27 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# TODO - Compute Engine Improvements
22

3-
Next: #14, #15, #3, #23 (option 1), #18, #19, #20, #21
3+
Next: (all sqrt patterns complete)
44

55
## Lower Priority
66

@@ -358,88 +358,42 @@ patterns aren't supported.
358358

359359
**Patterns to add:**
360360

361-
1. **Sqrt of linear expression:** `√(ax + b) + c = 0`
361+
1. **Sqrt of linear expression:** `√(ax + b) + c = 0` ✅ IMPLEMENTED (in prior work)
362362
- Solution: `ax + b = c²` when `c ≤ 0`, so `x = (c² - b)/a`
363363

364-
```typescript
365-
{
366-
match: ['Add', ['Sqrt', ['Add', ['Multiply', '__a', '_x'], '__b']], '__c'],
367-
replace: ['Divide', ['Subtract', ['Square', '__c'], '__b'], '__a'],
368-
condition: (sub) => filter(sub) && (sub.__c.isNonPositive ?? false),
369-
}
370-
```
371-
372-
2. **Sqrt equals linear:** `√(ax + b) = cx + d`
373-
- Square both sides: `ax + b = (cx + d)²`
374-
- Expand: `ax + b = c²x² + 2cdx + d²`
375-
- Rearrange: `c²x² + (2cd - a)x + (d² - b) = 0`
376-
- Use quadratic formula, then validate (may have extraneous roots)
377-
378-
3. **Sum of two sqrt terms:** `√(ax + b) + √(cx + d) = e`
379-
- Isolate one sqrt: `√(ax + b) = e - √(cx + d)`
380-
- Square: `ax + b = e² - 2e√(cx + d) + cx + d`
381-
- Isolate remaining sqrt and square again
382-
- Results in polynomial equation
383-
384-
4. **Nested sqrt:** `√(x + √x) = a`
385-
- Use substitution u = √x
364+
2. **Sqrt equals linear:** `√(ax + b) = cx + d` ✅ IMPLEMENTED
365+
- Pre-processing in `transformSqrtLinearEquation()` squares both sides
366+
- Transforms to: `c²x² + (2cd - a)x + (d² - b) = 0`
367+
- Quadratic formula solves, `validateRoots()` filters extraneous roots
368+
- Examples: `√(x+1) = x``1.618`, `√(3x-2) = x``[1, 2]`
369+
370+
3. **Sum of two sqrt terms:** `√(ax + b) + √(cx + d) = e` ✅ IMPLEMENTED
371+
- Isolate one sqrt: `√(f(x)) = e - √(g(x))`
372+
- Square: `f(x) = e² - 2e√(g(x)) + g(x)`
373+
- Isolate remaining sqrt: `f(x) - e² - g(x) = -2e√(g(x))`
374+
- Square again: `(f(x) - e² - g(x))² = 4e²·g(x)`
375+
- Solve polynomial and validate against original equation
376+
- Implemented in `solveTwoSqrtEquation()` function
377+
- Examples: `√(x+1) + √(x+4) = 3``0`, `√(x+5) - √(x-3) = 2``4`
378+
379+
4. **Nested sqrt:** `√(x + √x) = a` ✅ IMPLEMENTED
380+
- Uses substitution u = √x, so x = u²
386381
- Becomes `√(u² + u) = a`, then `u² + u = a²`
387-
- Solve quadratic for u, then x = u²
382+
- Solves quadratic for u, filters u < 0 (since u = √x ≥ 0), then x = u²
383+
- Implemented in `solveNestedSqrtEquation()` function
384+
- Examples: `√(x + 2√x) = 3``11 - 2√10`, `√(x - √x) = 1``φ² ≈ 2.618`
388385

389-
**Complexity note:** Patterns 2-4 can produce extraneous roots and require
390-
careful validation.
386+
**Also implemented:** Quadratic without constant term: `ax² + bx = 0`
387+
- Factor: `x(ax + b) = 0``x = 0` or `x = -b/a`
388+
- This enables Pattern 2 cases like `√x = x``x² - x = 0`
391389

392390
**File:** `src/compute-engine/boxed-expression/solve.ts`
393391

394392
---
395393

396-
### 23. Replace Method Auto-Wildcards Single-Char Symbols
394+
### ~~23. Replace Method Auto-Wildcards Single-Char Symbols~~ ✅ FIXED
397395

398-
**Problem:** `.replace({match: 'a', replace: 2})` unexpectedly converts `'a'` to
399-
a wildcard `'_a'`, causing it to match ANY expression instead of just the
400-
literal symbol `a`.
401-
402-
**Current behavior:**
403-
404-
```typescript
405-
const expr = ce.box(['Add', ['Multiply', 'a', 'x'], 'b']);
406-
expr.replace({match: 'a', replace: 2}, {recursive: true})
407-
// Returns: 2 (wrong!)
408-
// Expected: 2*x + b
409-
```
410-
411-
**Root cause:** In `parseRulePart` (rules.ts:350), all single-character symbols
412-
are auto-converted to wildcards:
413-
414-
```typescript
415-
if (x.symbol && x.symbol.length === 1) return ce.symbol('_' + x.symbol);
416-
```
417-
418-
This makes sense when parsing rule strings like `"a*x -> 2*x"` where `a`, `x`
419-
should be wildcards, but NOT when the user explicitly provides
420-
`{match: 'a', replace: 2}` where they likely want literal matching.
421-
422-
**Solution options:**
423-
424-
1. Only auto-wildcard when parsing a rule string, not when rule is provided as
425-
an object
426-
2. Add an option like `{literal: true}` to disable auto-wildcarding
427-
3. Require explicit wildcard syntax `'_a'` and never auto-wildcard
428-
429-
[*] Option 1 is preferred.
430-
431-
**Workaround:** Use `.subs()` for simple variable substitution:
432-
433-
```typescript
434-
expr.subs({a: 2}) // Returns: 2*x + b (correct)
435-
```
436-
437-
**Files to modify:**
438-
439-
- `src/compute-engine/boxed-expression/rules.ts` - `parseRulePart` function
440-
441-
**Tests:** See `test/playground.ts` lines 61-72 and PLAYGROUND.md Evaluation
442-
section
396+
See `requirements/DONE.md` for implementation details
443397

444398
---
445399

0 commit comments

Comments
 (0)