Skip to content

Commit 53c457b

Browse files
committed
feat: enhance handling of non-enumerable domains to remain symbolic in evaluations
1 parent e612a22 commit 53c457b

5 files changed

Lines changed: 295 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@
9191
- `["Interval", ["Open", 0], 5]` → iterates 1, 2, 3, 4, 5 (excludes 0)
9292
- `["Interval", 1, ["Open", 6]]` → iterates 1, 2, 3, 4, 5 (excludes 6)
9393

94+
- **Non-enumerable domains stay symbolic**: When the domain cannot be enumerated
95+
(unknown symbol, infinite set, or symbolic bounds), the expression now stays
96+
symbolic instead of returning NaN:
97+
- `\sum_{n \in S} n` with unknown `S` → stays as `["Sum", "n", ["Element", "n", "S"]]`
98+
- `\sum_{n \in \N} n` with infinite set → stays symbolic
99+
- `\sum_{n \in [1,a]} n` with symbolic bound → stays symbolic
100+
- Previously these would all return `NaN` with no explanation
101+
94102
- **Linear Algebra Enhancements**: Improved tensor and matrix operations with
95103
better scalar handling, new functionality, and clearer error messages:
96104

@@ -117,6 +125,14 @@
117125
- `ZeroMatrix(m, n?)`: Creates an m×n matrix of zeros (square if n omitted)
118126
- `OnesMatrix(m, n?)`: Creates an m×n matrix of ones (square if n omitted)
119127

128+
- **Matrix and Vector Norms**: Added `Norm` function for computing various
129+
norms:
130+
- **Vector norms**: L1 (sum of absolute values), L2 (Euclidean, default),
131+
L-infinity (max absolute value), and general Lp norms
132+
- **Matrix norms**: Frobenius (default, sqrt of sum of squared elements),
133+
L1 (max column sum), L-infinity (max row sum)
134+
- Scalar norms return the absolute value
135+
120136
- **Diagonal function**: Now fully implemented with bidirectional behavior:
121137
- Vector → Matrix: Creates a diagonal matrix from a vector
122138
(`Diagonal([1,2,3])` → 3×3 diagonal matrix)

src/compute-engine/library/arithmetic.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import {
5050
mulN,
5151
canonicalDivide,
5252
} from '../boxed-expression/arithmetic-mul-div';
53-
import { canonicalBigop, reduceBigOp } from './utils';
53+
import { canonicalBigop, reduceBigOp, NON_ENUMERABLE_DOMAIN } from './utils';
5454
import {
5555
canonicalPower,
5656
canonicalRoot,
@@ -1516,31 +1516,41 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
15161516
canonicalBigop('Product', body, bounds, scope),
15171517

15181518
evaluate: (ops, options) => {
1519+
const ce = options.engine;
15191520
const result = run(
15201521
reduceBigOp(
15211522
ops[0],
15221523
ops.slice(1),
15231524
(acc: BoxedExpression, x) => acc.mul(x.evaluate(options)),
1524-
options.engine.One
1525+
ce.One
15251526
),
1526-
options.engine._timeRemaining
1527+
ce._timeRemaining
15271528
);
1529+
// If domain is non-enumerable, keep expression unevaluated (symbolic)
1530+
if (result === NON_ENUMERABLE_DOMAIN) {
1531+
return undefined; // Return undefined to keep expression symbolic
1532+
}
15281533
// Evaluate the accumulated result to combine numeric factors
1529-
return result?.evaluate() ?? options.engine.NaN;
1534+
return result?.evaluate() ?? ce.NaN;
15301535
},
15311536

15321537
evaluateAsync: async (ops, options) => {
1538+
const ce = options.engine;
15331539
const result = await runAsync(
15341540
reduceBigOp(
15351541
ops[0],
15361542
ops.slice(1),
15371543
(acc: BoxedExpression, x) => acc.mul(x.evaluate(options)),
1538-
options.engine.One
1544+
ce.One
15391545
),
1540-
options.engine._timeRemaining,
1546+
ce._timeRemaining,
15411547
options.signal
15421548
);
1543-
return result?.evaluate() ?? options.engine.NaN;
1549+
// If domain is non-enumerable, keep expression unevaluated (symbolic)
1550+
if (result === NON_ENUMERABLE_DOMAIN) {
1551+
return undefined; // Return undefined to keep expression symbolic
1552+
}
1553+
return result?.evaluate() ?? ce.NaN;
15441554
},
15451555
},
15461556

@@ -1567,6 +1577,10 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
15671577
),
15681578
engine._timeRemaining
15691579
);
1580+
// If domain is non-enumerable, keep expression unevaluated (symbolic)
1581+
if (result === NON_ENUMERABLE_DOMAIN) {
1582+
return undefined; // Return undefined to keep expression symbolic
1583+
}
15701584
// Evaluate the accumulated result to combine numeric terms
15711585
// e.g., 3x + 1 + 2 + 3 → 3x + 6
15721586
return result?.evaluate() ?? engine.NaN;
@@ -1583,6 +1597,10 @@ export const ARITHMETIC_LIBRARY: SymbolDefinitions[] = [
15831597
engine._timeRemaining,
15841598
signal
15851599
);
1600+
// If domain is non-enumerable, keep expression unevaluated (symbolic)
1601+
if (result === NON_ENUMERABLE_DOMAIN) {
1602+
return undefined; // Return undefined to keep expression symbolic
1603+
}
15861604
return result?.evaluate() ?? engine.NaN;
15871605
},
15881606
},

src/compute-engine/library/logic-analysis.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,43 @@ import {
1111
* Extracted from logic.ts for better code organization.
1212
*/
1313

14+
/**
15+
* Result of extracting a finite domain from an Element expression.
16+
* - `status: 'success'` - Domain was successfully extracted
17+
* - `status: 'non-enumerable'` - Domain exists but cannot be enumerated (e.g., infinite set, unknown symbol)
18+
* - `status: 'error'` - Invalid Element expression (missing variable, malformed domain)
19+
*/
20+
export type ExtractDomainResult =
21+
| { status: 'success'; variable: string; values: BoxedExpression[] }
22+
| { status: 'non-enumerable'; variable: string; domain: BoxedExpression; reason: string }
23+
| { status: 'error'; reason: string };
24+
1425
/**
1526
* Extract the finite domain from a quantifier's condition.
1627
* Supports:
1728
* - ["Element", "x", ["Set", 1, 2, 3]] → [1, 2, 3]
1829
* - ["Element", "x", ["Range", 1, 5]] → [1, 2, 3, 4, 5]
1930
* - ["Element", "x", ["Interval", 1, 5]] → [1, 2, 3, 4, 5] (integers only)
20-
* Returns null if the domain is not finite or not recognized.
31+
* Returns detailed result indicating success, non-enumerable domain, or error.
2132
*/
22-
export function extractFiniteDomain(
33+
export function extractFiniteDomainWithReason(
2334
condition: BoxedExpression,
2435
ce: ComputeEngine
25-
): { variable: string; values: BoxedExpression[] } | null {
36+
): ExtractDomainResult {
2637
// Check for ["Element", var, set] pattern
27-
if (condition.operator !== 'Element') return null;
38+
if (condition.operator !== 'Element') {
39+
return { status: 'error', reason: 'expected-element-expression' };
40+
}
2841

2942
const variable = condition.op1?.symbol;
30-
if (!variable) return null;
43+
if (!variable) {
44+
return { status: 'error', reason: 'expected-index-variable' };
45+
}
3146

3247
const domain = condition.op2;
33-
if (!domain) return null;
48+
if (!domain) {
49+
return { status: 'error', reason: 'expected-domain' };
50+
}
3451

3552
// Handle explicit sets: ["Set", 1, 2, 3]
3653
if (domain.operator === 'Set' || domain.operator === 'List') {
@@ -49,13 +66,19 @@ export function extractFiniteDomain(
4966
for (let i = start; i <= end; i++) {
5067
rangeValues.push(ce.number(i));
5168
}
52-
return { variable, values: rangeValues };
69+
return { status: 'success', variable, values: rangeValues };
70+
}
71+
if (count > 1000) {
72+
return { status: 'non-enumerable', variable, domain, reason: 'domain-too-large' };
5373
}
5474
}
5575
}
56-
return { variable, values: [...values] };
76+
return { status: 'success', variable, values: [...values] };
5777
}
58-
return null;
78+
if (values && values.length > 1000) {
79+
return { status: 'non-enumerable', variable, domain, reason: 'domain-too-large' };
80+
}
81+
return { status: 'error', reason: 'empty-domain' };
5982
}
6083

6184
// Handle Range: ["Range", start, end] or ["Range", start, end, step]
@@ -75,10 +98,14 @@ export function extractFiniteDomain(
7598
for (let i = start; step > 0 ? i <= end : i >= end; i += step) {
7699
values.push(ce.number(i));
77100
}
78-
return { variable, values };
101+
return { status: 'success', variable, values };
102+
}
103+
if (count > 1000) {
104+
return { status: 'non-enumerable', variable, domain, reason: 'domain-too-large' };
79105
}
80106
}
81-
return null;
107+
// Range with non-integer or symbolic bounds
108+
return { status: 'non-enumerable', variable, domain, reason: 'non-integer-bounds' };
82109
}
83110

84111
// Handle finite integer Interval: ["Interval", start, end]
@@ -120,12 +147,52 @@ export function extractFiniteDomain(
120147
for (let i = start; i <= end; i++) {
121148
values.push(ce.number(i));
122149
}
123-
return { variable, values };
150+
return { status: 'success', variable, values };
124151
}
152+
if (count > 1000) {
153+
return { status: 'non-enumerable', variable, domain, reason: 'domain-too-large' };
154+
}
155+
}
156+
// Interval with non-integer or symbolic bounds
157+
return { status: 'non-enumerable', variable, domain, reason: 'non-integer-bounds' };
158+
}
159+
160+
// Check for known infinite sets (e.g., NonNegativeIntegers, Integers, Reals, etc.)
161+
if (domain.symbol) {
162+
const knownInfiniteSets = [
163+
'Integers', 'NonNegativeIntegers', 'PositiveIntegers', 'NegativeIntegers',
164+
'Rationals', 'Reals', 'PositiveReals', 'NonNegativeReals', 'NegativeReals',
165+
'NonPositiveReals', 'ExtendedReals', 'Complexes', 'ImaginaryNumbers',
166+
'Numbers', 'ExtendedComplexes', 'AlgebraicNumbers', 'TranscendentalNumbers',
167+
];
168+
if (knownInfiniteSets.includes(domain.symbol)) {
169+
return { status: 'non-enumerable', variable, domain, reason: 'infinite-domain' };
125170
}
126-
return null;
171+
// Unknown symbol - could be a finite set, but we can't determine
172+
return { status: 'non-enumerable', variable, domain, reason: 'unknown-domain' };
127173
}
128174

175+
// Unknown domain structure
176+
return { status: 'non-enumerable', variable, domain, reason: 'unrecognized-domain-type' };
177+
}
178+
179+
/**
180+
* Extract the finite domain from a quantifier's condition.
181+
* Supports:
182+
* - ["Element", "x", ["Set", 1, 2, 3]] → [1, 2, 3]
183+
* - ["Element", "x", ["Range", 1, 5]] → [1, 2, 3, 4, 5]
184+
* - ["Element", "x", ["Interval", 1, 5]] → [1, 2, 3, 4, 5] (integers only)
185+
* Returns null if the domain is not finite or not recognized.
186+
* @deprecated Use extractFiniteDomainWithReason for better error handling
187+
*/
188+
export function extractFiniteDomain(
189+
condition: BoxedExpression,
190+
ce: ComputeEngine
191+
): { variable: string; values: BoxedExpression[] } | null {
192+
const result = extractFiniteDomainWithReason(condition, ce);
193+
if (result.status === 'success') {
194+
return { variable: result.variable, values: result.values };
195+
}
129196
return null;
130197
}
131198

0 commit comments

Comments
 (0)