document-compute.js 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/index.cjs +221 -9
- package/dist/index.d.cts +64 -2
- package/dist/index.d.ts +64 -2
- package/dist/index.js +218 -10
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -102,9 +102,20 @@ solveFor(xSquared, 4, "x", {}, { method: "newton", initialGuess: 3 }); // 2, via
|
|
|
102
102
|
|
|
103
103
|
## Deviations from the issue
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
One thing #573 asks for was closed at adoption rather than built here: `Quantity` and `FormulaBindings` (with `Interval` and `EvaluationValue`) are typed contracts in `document-schema.js`'s `src/math.ts`, beside `MathExpression` itself, exactly as the issue proposes — evaluation inputs are validated schemas like everything else in that package, and this package imports them from there the same way it imports `MathExpression`, `DimensionVector`, and `ExactRational`, rather than carrying package-local definitions.
|
|
106
106
|
|
|
107
|
-
The
|
|
107
|
+
## The worked-example differential harness
|
|
108
|
+
|
|
109
|
+
[ExaDev/documents.js#794](https://github.com/ExaDev/documents.js/issues/794) split #573's own stated differentiator — measuring the fraction of a real document's formulae whose evaluation reproduces the document's own stated answer — into its own package once `evaluate`/`solveFor` themselves existed to measure against. `src/harness/worked-example.ts` and `src/harness/corpus.ts` are that harness:
|
|
110
|
+
|
|
111
|
+
- `runWorkedExampleSequence(formulas, symbolTable?, options?)` walks a document-ordered sequence of already-lowered `ContentFormula` values and recognises the "givens, a formula, a stated result" shape a worked example actually has: a **definition** (`F = m \times a` — the right-hand side still mentions a symbol), a **binding** (`m = 2 kg` — a fully closed "given"), and a **stated result** (`F = 6 N` — structurally identical to a binding, but restating a symbol a definition is waiting on). A definition's own right-hand side is evaluated against whatever bindings are current when its stated result is reached, not a snapshot taken when the definition line first appeared — the common real document states the general law first, then the specific numbers, then the answer. Every outcome is one of `match`, `mismatch`, `gap` (naming a specific `WorkedExampleGap` — `unbound-symbol`, `unknown-unit`, `incompatible-dimensions`, `division-by-zero`, `unsupported-construct`, `numeric-domain`, `non-convergent-solve` — one per `compute/errors.ts` class), or `unresolved` (a definition the document never restated an answer for). Comparison is by relative tolerance (`1e-3` default), not exact equality, since a worked example's own stated answer is conventionally rounded.
|
|
112
|
+
- `collectFormulas(document)`/`runCorpus(documents, options?)` extract the formula sequence out of a wordprocessing `ContentDocument`'s block flow (table cells included) and run the harness over a whole corpus at once, aggregating one combined coverage fraction plus every document's own outcomes; `formatCorpusReport` renders the result as plain text for a CLI/console caller.
|
|
113
|
+
|
|
114
|
+
Scoped to point-valued (`Quantity`) answers: every value this harness computes comes from evaluating a closed statement with no bindings, which `evaluate` cannot turn into an `Interval` (an `Interval` only ever arises by binding a symbol to one) — a genuinely interval-valued worked example (`0.87 <= cos(phi) <= 1`, #573's own illustration of interval arithmetic) has no representation in this "symbol = expression" equality grammar at all, since neither `MathExpression` nor `documents.js`'s LaTeX lowering has a compound-inequality-to-range reading, and is out of scope for this pass rather than silently mishandled.
|
|
115
|
+
|
|
116
|
+
`src/harness/corpus.test.ts` proves the whole pipeline end to end — markdown text through `markdown-codec`'s `$$` block recognition and `documents.js`'s `lowerMarkdownMath` (the "LaTeX lowering" #794 names as the natural source of worked examples) into this harness — against a small, hand-authored starter corpus. `markdown-codec` and `documents.js` are **devDependencies only**: both sit above this package in the family's own dependency order (see the monorepo root README's package table), so neither can be a runtime dependency here without a cycle, which is exactly why this package remains "not wired into the conversion pipeline" at runtime even though its own test suite now exercises that pipeline. A large real-world corpus (the issue's own stated differentiator at scale) is not included — gathering one is a data-curation task, not a code one — but is a straightforward local addition: point a `test/corpus/` directory (gitignored, matching `pdf-codec`'s own `test:corpus` convention) at real markdown documents with worked examples and feed `readMarkdownContent` → `lowerMarkdownMath` → `runCorpus` the same way `corpus.test.ts` does.
|
|
117
|
+
|
|
118
|
+
While building this harness's own fixtures, a real bug surfaced in `documents.js`'s LaTeX lowering: `F = m \times a` (the textbook-standard way to write almost any formula) lowers to `(F = m) \times a` rather than `F = (m \times a)`, because the lowering folds relational and arithmetic operators at the same precedence with no notion that `=` should bind loosest — filed as [ExaDev/documents.js#812](https://github.com/ExaDev/documents.js/issues/812). This package's own fixtures work around it with an explicit braced right-hand side (`F = {m \times a}`, which lowers correctly), since fixing the lowering itself is out of scope for this package.
|
|
108
119
|
|
|
109
120
|
## Out of scope
|
|
110
121
|
|
|
@@ -112,7 +123,6 @@ Quoting the issue's own scope line directly: **this is deliberately not a CAS in
|
|
|
112
123
|
|
|
113
124
|
- **Symbolic algebra** — exact rearrangement of an expression emitted back out as LaTeX, simplification, integration. `solveFor` finds a root numerically; it never isolates the unknown algebraically.
|
|
114
125
|
- **A SymPy sidecar or any other symbolic-engine adapter.** The issue names this as the eventual home for symbolic work, behind an evaluator interface this package does not define or stub.
|
|
115
|
-
- **The worked-example test harness** — see Deviations from the issue above.
|
|
116
126
|
- **Matrix-valued evaluation.** `MathExpression`'s `'matrix'` node exists in the grammar `document-schema.js` defines, but this evaluator only ever produces scalar `Quantity`/`Interval` values; a `'matrix'` node throws `UnsupportedExpressionError` rather than being silently misevaluated.
|
|
117
127
|
- **A general interval rule for `pow`/`sqrt`/the trigonometric operators.** These are implemented for `Quantity` only; applied to an `Interval` operand they throw `UnsupportedExpressionError` rather than guessing at a range a non-monotonic or sign-dependent function would need real analysis to get right.
|
|
118
128
|
|
package/dist/index.cjs
CHANGED
|
@@ -277,7 +277,7 @@ function absInterval(a) {
|
|
|
277
277
|
}
|
|
278
278
|
//#endregion
|
|
279
279
|
//#region src/compute/evaluate.ts
|
|
280
|
-
const EMPTY_SYMBOL_TABLE$
|
|
280
|
+
const EMPTY_SYMBOL_TABLE$2 = {
|
|
281
281
|
symbols: [],
|
|
282
282
|
units: []
|
|
283
283
|
};
|
|
@@ -287,7 +287,7 @@ function isInterval(value) {
|
|
|
287
287
|
function toInterval(value) {
|
|
288
288
|
return isInterval(value) ? value : pointInterval(value.magnitude, value.dimension);
|
|
289
289
|
}
|
|
290
|
-
function asQuantity(value, context) {
|
|
290
|
+
function asQuantity$1(value, context) {
|
|
291
291
|
if (isInterval(value)) throw new UnsupportedExpressionError(context, "this position requires a plain Quantity, not an Interval");
|
|
292
292
|
return value;
|
|
293
293
|
}
|
|
@@ -323,7 +323,7 @@ const UNARY_OPERATORS = {
|
|
|
323
323
|
"math:cos": { quantity: cosQuantity },
|
|
324
324
|
"math:tan": { quantity: tanQuantity }
|
|
325
325
|
};
|
|
326
|
-
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$
|
|
326
|
+
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$2) {
|
|
327
327
|
switch (expression.kind) {
|
|
328
328
|
case "num": return quantity(rationalToNumber(toRational(expression)), {});
|
|
329
329
|
case "qty": return evaluateQty(expression, context);
|
|
@@ -375,13 +375,13 @@ function evaluateApp(node, bindings, context) {
|
|
|
375
375
|
}
|
|
376
376
|
if (node.operator === "math:pow") {
|
|
377
377
|
const [base, exponent] = expectTwoArgs(args, "'math:pow'");
|
|
378
|
-
return powQuantity(asQuantity(base, "evaluate"), asQuantity(exponent, "evaluate"));
|
|
378
|
+
return powQuantity(asQuantity$1(base, "evaluate"), asQuantity$1(exponent, "evaluate"));
|
|
379
379
|
}
|
|
380
380
|
throw new UnsupportedExpressionError("evaluate", `unknown operator '${node.operator}'`);
|
|
381
381
|
}
|
|
382
382
|
function evaluateBinder(node, bindings, context) {
|
|
383
|
-
const lower = asQuantity(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
384
|
-
const upper = asQuantity(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
383
|
+
const lower = asQuantity$1(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
384
|
+
const upper = asQuantity$1(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
385
385
|
if (!isDimensionless(lower.dimension) || !isDimensionless(upper.dimension)) throw new IncompatibleDimensionsError(`math:${node.kind}`, lower.dimension, upper.dimension, "binder bounds must be dimensionless");
|
|
386
386
|
if (!Number.isInteger(lower.magnitude) || !Number.isInteger(upper.magnitude)) throw new UnsupportedExpressionError(`evaluate:${node.kind}`, "binder bounds must evaluate to integers");
|
|
387
387
|
let accumulator = node.kind === "sum" ? quantity(0, {}) : quantity(1, {});
|
|
@@ -390,7 +390,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
390
390
|
...bindings,
|
|
391
391
|
[node.binder]: quantity(i, {})
|
|
392
392
|
};
|
|
393
|
-
const bodyValue = asQuantity(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
393
|
+
const bodyValue = asQuantity$1(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
394
394
|
accumulator = node.kind === "sum" ? addQuantities(accumulator, bodyValue) : multiplyQuantities(accumulator, bodyValue);
|
|
395
395
|
}
|
|
396
396
|
return accumulator;
|
|
@@ -400,7 +400,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
400
400
|
const DEFAULT_TOLERANCE = 1e-9;
|
|
401
401
|
const DEFAULT_MAX_ITERATIONS = 100;
|
|
402
402
|
const DEFAULT_DERIVATIVE_STEP = 1e-6;
|
|
403
|
-
const EMPTY_SYMBOL_TABLE = {
|
|
403
|
+
const EMPTY_SYMBOL_TABLE$1 = {
|
|
404
404
|
symbols: [],
|
|
405
405
|
units: []
|
|
406
406
|
};
|
|
@@ -414,7 +414,7 @@ function residualFn(expression, targetValue, unknownSymbol, bindings, context, d
|
|
|
414
414
|
return result.magnitude - targetValue;
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
|
-
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE) {
|
|
417
|
+
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE$1) {
|
|
418
418
|
const method = options.method ?? "bisection";
|
|
419
419
|
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
|
|
420
420
|
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
@@ -456,6 +456,214 @@ function newton(f, initialGuess, tolerance, maxIterations, h) {
|
|
|
456
456
|
throw new NonConvergentSolveError("newton", maxIterations, `residual still exceeds tolerance ${tolerance} after ${maxIterations} iterations`);
|
|
457
457
|
}
|
|
458
458
|
//#endregion
|
|
459
|
+
//#region src/harness/worked-example.ts
|
|
460
|
+
const DEFAULT_RELATIVE_TOLERANCE = .001;
|
|
461
|
+
const EMPTY_BINDINGS = {};
|
|
462
|
+
const EMPTY_SYMBOL_TABLE = {
|
|
463
|
+
symbols: [],
|
|
464
|
+
units: []
|
|
465
|
+
};
|
|
466
|
+
function asEquality(expression) {
|
|
467
|
+
if (expression.kind !== "app" || expression.operator !== "math:eq") return;
|
|
468
|
+
const [lhs, rhs] = expression.args;
|
|
469
|
+
if (lhs === void 0 || rhs === void 0 || lhs.kind !== "sym") return;
|
|
470
|
+
return {
|
|
471
|
+
targetSymbol: lhs.id,
|
|
472
|
+
rhs
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function containsSymbol(expression) {
|
|
476
|
+
switch (expression.kind) {
|
|
477
|
+
case "sym": return true;
|
|
478
|
+
case "num":
|
|
479
|
+
case "qty":
|
|
480
|
+
case "unparsed": return false;
|
|
481
|
+
case "app": return expression.args.some(containsSymbol);
|
|
482
|
+
case "sum":
|
|
483
|
+
case "prod": return containsSymbol(expression.lower) || containsSymbol(expression.upper) || containsSymbol(expression.body);
|
|
484
|
+
case "matrix": return expression.rows.some((row) => row.some(containsSymbol));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function asQuantity(value) {
|
|
488
|
+
if (isInterval(value)) throw new UnsupportedExpressionError("runWorkedExampleSequence", "this harness compares point-valued Quantity answers only; a symbol resolving to a range (Interval) has no stated-answer comparison defined yet");
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
function gapFromError(error) {
|
|
492
|
+
if (error instanceof UnboundSymbolError) return "unbound-symbol";
|
|
493
|
+
if (error instanceof UnknownUnitError) return "unknown-unit";
|
|
494
|
+
if (error instanceof IncompatibleDimensionsError) return "incompatible-dimensions";
|
|
495
|
+
if (error instanceof DivisionByZeroError) return "division-by-zero";
|
|
496
|
+
if (error instanceof UnsupportedExpressionError) return "unsupported-construct";
|
|
497
|
+
if (error instanceof NumericDomainError) return "numeric-domain";
|
|
498
|
+
if (error instanceof NonConvergentSolveError) return "non-convergent-solve";
|
|
499
|
+
return "other-evaluation-error";
|
|
500
|
+
}
|
|
501
|
+
function errorMessage(error) {
|
|
502
|
+
return error instanceof Error ? error.message : String(error);
|
|
503
|
+
}
|
|
504
|
+
function withinTolerance(actual, expected, relativeTolerance) {
|
|
505
|
+
if (expected === 0) return Math.abs(actual) <= relativeTolerance;
|
|
506
|
+
return Math.abs(actual - expected) / Math.abs(expected) <= relativeTolerance;
|
|
507
|
+
}
|
|
508
|
+
function resultsMatch(actual, expected, relativeTolerance) {
|
|
509
|
+
return dimensionsEqual(actual.dimension, expected.dimension) && withinTolerance(actual.magnitude, expected.magnitude, relativeTolerance);
|
|
510
|
+
}
|
|
511
|
+
function runWorkedExampleSequence(formulas, symbolTable = EMPTY_SYMBOL_TABLE, options) {
|
|
512
|
+
const relativeTolerance = options?.relativeTolerance ?? DEFAULT_RELATIVE_TOLERANCE;
|
|
513
|
+
const bindings = {};
|
|
514
|
+
const outcomes = [];
|
|
515
|
+
let pending;
|
|
516
|
+
const closeUnresolved = () => {
|
|
517
|
+
if (pending === void 0) return;
|
|
518
|
+
outcomes.push({
|
|
519
|
+
outcome: "unresolved",
|
|
520
|
+
targetSymbol: pending.targetSymbol,
|
|
521
|
+
message: `"${pending.targetSymbol}" was defined but the sequence never restated it as a closed numeric result before ending or being superseded by another definition`
|
|
522
|
+
});
|
|
523
|
+
pending = void 0;
|
|
524
|
+
};
|
|
525
|
+
for (const formula of formulas) {
|
|
526
|
+
if (formula.content === void 0) continue;
|
|
527
|
+
const equality = asEquality(formula.content);
|
|
528
|
+
if (equality === void 0) continue;
|
|
529
|
+
const { targetSymbol, rhs } = equality;
|
|
530
|
+
if (containsSymbol(rhs)) {
|
|
531
|
+
closeUnresolved();
|
|
532
|
+
pending = {
|
|
533
|
+
targetSymbol,
|
|
534
|
+
rhs
|
|
535
|
+
};
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
let closedValue;
|
|
539
|
+
try {
|
|
540
|
+
closedValue = asQuantity(evaluate(rhs, EMPTY_BINDINGS, symbolTable));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
outcomes.push({
|
|
543
|
+
outcome: "gap",
|
|
544
|
+
gap: gapFromError(error),
|
|
545
|
+
targetSymbol,
|
|
546
|
+
message: errorMessage(error)
|
|
547
|
+
});
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (pending?.targetSymbol === targetSymbol) {
|
|
551
|
+
const { rhs: definitionRhs } = pending;
|
|
552
|
+
pending = void 0;
|
|
553
|
+
let actual;
|
|
554
|
+
try {
|
|
555
|
+
actual = asQuantity(evaluate(definitionRhs, bindings, symbolTable));
|
|
556
|
+
} catch (error) {
|
|
557
|
+
outcomes.push({
|
|
558
|
+
outcome: "gap",
|
|
559
|
+
gap: gapFromError(error),
|
|
560
|
+
targetSymbol,
|
|
561
|
+
message: errorMessage(error)
|
|
562
|
+
});
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
outcomes.push(resultsMatch(actual, closedValue, relativeTolerance) ? {
|
|
566
|
+
outcome: "match",
|
|
567
|
+
targetSymbol,
|
|
568
|
+
expected: closedValue,
|
|
569
|
+
actual
|
|
570
|
+
} : {
|
|
571
|
+
outcome: "mismatch",
|
|
572
|
+
targetSymbol,
|
|
573
|
+
expected: closedValue,
|
|
574
|
+
actual
|
|
575
|
+
});
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
bindings[targetSymbol] = closedValue;
|
|
579
|
+
}
|
|
580
|
+
closeUnresolved();
|
|
581
|
+
const matched = outcomes.filter((o) => o.outcome === "match").length;
|
|
582
|
+
const mismatched = outcomes.filter((o) => o.outcome === "mismatch").length;
|
|
583
|
+
const gaps = outcomes.filter((o) => o.outcome === "gap").length;
|
|
584
|
+
const unresolved = outcomes.filter((o) => o.outcome === "unresolved").length;
|
|
585
|
+
return {
|
|
586
|
+
outcomes,
|
|
587
|
+
total: outcomes.length,
|
|
588
|
+
matched,
|
|
589
|
+
mismatched,
|
|
590
|
+
gaps,
|
|
591
|
+
unresolved,
|
|
592
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/harness/corpus.ts
|
|
597
|
+
function collectFormulasFromBlocks(blocks, out) {
|
|
598
|
+
for (const block of blocks) {
|
|
599
|
+
if (block.kind === "table") {
|
|
600
|
+
for (const row of block.rows) for (const cell of row.cells) collectFormulasFromBlocks(cell.blocks, out);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (block.kind === "embeddedObject" && block.objectKind === "formula") {
|
|
604
|
+
const embedded = block.document;
|
|
605
|
+
if (embedded.kind === "formula") out.push(embedded.formula);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function collectFormulas(document) {
|
|
610
|
+
if (document.kind !== "wordprocessing") return [];
|
|
611
|
+
const out = [];
|
|
612
|
+
for (const section of document.sections) collectFormulasFromBlocks(section.blocks, out);
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
function runCorpus(documents, options) {
|
|
616
|
+
const reports = documents.map(({ label, document }) => {
|
|
617
|
+
const symbolTable = document.symbolTable ?? {
|
|
618
|
+
symbols: [],
|
|
619
|
+
units: []
|
|
620
|
+
};
|
|
621
|
+
return {
|
|
622
|
+
label,
|
|
623
|
+
report: runWorkedExampleSequence(collectFormulas(document), symbolTable, options)
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
const matched = sumBy(reports, (r) => r.report.matched);
|
|
627
|
+
const mismatched = sumBy(reports, (r) => r.report.mismatched);
|
|
628
|
+
const gaps = sumBy(reports, (r) => r.report.gaps);
|
|
629
|
+
const unresolved = sumBy(reports, (r) => r.report.unresolved);
|
|
630
|
+
return {
|
|
631
|
+
documents: reports,
|
|
632
|
+
total: sumBy(reports, (r) => r.report.total),
|
|
633
|
+
matched,
|
|
634
|
+
mismatched,
|
|
635
|
+
gaps,
|
|
636
|
+
unresolved,
|
|
637
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function sumBy(items, project) {
|
|
641
|
+
return items.reduce((total, item) => total + project(item), 0);
|
|
642
|
+
}
|
|
643
|
+
function formatOutcome(outcome) {
|
|
644
|
+
switch (outcome.outcome) {
|
|
645
|
+
case "match": return `match: ${outcome.targetSymbol}`;
|
|
646
|
+
case "mismatch": return `MISMATCH: ${outcome.targetSymbol} -- expected ${formatEvaluationResult(outcome.expected)}, got ${formatEvaluationResult(outcome.actual)}`;
|
|
647
|
+
case "gap": return `GAP (${outcome.gap}): ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
648
|
+
case "unresolved": return `unresolved: ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function formatEvaluationResult(value) {
|
|
652
|
+
if (value.kind === "interval") return `[${value.min}, ${value.max}]`;
|
|
653
|
+
return `${value.magnitude}`;
|
|
654
|
+
}
|
|
655
|
+
function formatCorpusReport(report) {
|
|
656
|
+
const lines = [];
|
|
657
|
+
for (const { label, report: documentReport } of report.documents) {
|
|
658
|
+
const coverageText = documentReport.coverage === void 0 ? "no stated answers" : `${(documentReport.coverage * 100).toFixed(1)}% (${documentReport.matched}/${documentReport.matched + documentReport.mismatched})`;
|
|
659
|
+
lines.push(`${label}: ${coverageText}`);
|
|
660
|
+
for (const outcome of documentReport.outcomes) if (outcome.outcome !== "match") lines.push(` ${formatOutcome(outcome)}`);
|
|
661
|
+
}
|
|
662
|
+
const combinedText = report.coverage === void 0 ? "no stated answers in corpus" : `${(report.coverage * 100).toFixed(1)}% (${report.matched}/${report.matched + report.mismatched}), ${report.gaps} gap(s), ${report.unresolved} unresolved`;
|
|
663
|
+
lines.push(`TOTAL: ${combinedText}`);
|
|
664
|
+
return lines.join("\n");
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
459
667
|
exports.DivisionByZeroError = DivisionByZeroError;
|
|
460
668
|
exports.IncompatibleDimensionsError = IncompatibleDimensionsError;
|
|
461
669
|
exports.NonConvergentSolveError = NonConvergentSolveError;
|
|
@@ -468,6 +676,7 @@ exports.absQuantity = absQuantity;
|
|
|
468
676
|
exports.addIntervals = addIntervals;
|
|
469
677
|
exports.addQuantities = addQuantities;
|
|
470
678
|
exports.addRational = addRational;
|
|
679
|
+
exports.collectFormulas = collectFormulas;
|
|
471
680
|
exports.cosQuantity = cosQuantity;
|
|
472
681
|
exports.dimensionExponent = dimensionExponent;
|
|
473
682
|
exports.dimensionToString = dimensionToString;
|
|
@@ -477,6 +686,7 @@ exports.divideIntervals = divideIntervals;
|
|
|
477
686
|
exports.divideQuantities = divideQuantities;
|
|
478
687
|
exports.divideRational = divideRational;
|
|
479
688
|
exports.evaluate = evaluate;
|
|
689
|
+
exports.formatCorpusReport = formatCorpusReport;
|
|
480
690
|
exports.interval = interval;
|
|
481
691
|
exports.isDimensionless = isDimensionless;
|
|
482
692
|
exports.isInterval = isInterval;
|
|
@@ -490,6 +700,8 @@ exports.pointInterval = pointInterval;
|
|
|
490
700
|
exports.powQuantity = powQuantity;
|
|
491
701
|
exports.quantity = quantity;
|
|
492
702
|
exports.rationalToNumber = rationalToNumber;
|
|
703
|
+
exports.runCorpus = runCorpus;
|
|
704
|
+
exports.runWorkedExampleSequence = runWorkedExampleSequence;
|
|
493
705
|
exports.scaleDimension = scaleDimension;
|
|
494
706
|
exports.sinQuantity = sinQuantity;
|
|
495
707
|
exports.solveFor = solveFor;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
1
|
+
import { ContentDocument, ContentFormula, DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
2
2
|
//#region src/compute/rational.d.ts
|
|
3
3
|
interface Rational {
|
|
4
4
|
readonly n: bigint;
|
|
@@ -103,4 +103,66 @@ interface SolveForOptions {
|
|
|
103
103
|
}
|
|
104
104
|
declare function solveFor(expression: MathExpression, targetValue: number, unknownSymbol: string, bindings: FormulaBindings, options?: SolveForOptions, context?: SymbolTable): number;
|
|
105
105
|
//#endregion
|
|
106
|
-
|
|
106
|
+
//#region src/harness/worked-example.d.ts
|
|
107
|
+
type WorkedExampleGap = "unbound-symbol" | "unknown-unit" | "incompatible-dimensions" | "division-by-zero" | "unsupported-construct" | "numeric-domain" | "non-convergent-solve" | "other-evaluation-error";
|
|
108
|
+
interface WorkedExampleMatch {
|
|
109
|
+
readonly outcome: "match";
|
|
110
|
+
readonly targetSymbol: string;
|
|
111
|
+
readonly expected: Quantity;
|
|
112
|
+
readonly actual: Quantity;
|
|
113
|
+
}
|
|
114
|
+
interface WorkedExampleMismatch {
|
|
115
|
+
readonly outcome: "mismatch";
|
|
116
|
+
readonly targetSymbol: string;
|
|
117
|
+
readonly expected: Quantity;
|
|
118
|
+
readonly actual: Quantity;
|
|
119
|
+
}
|
|
120
|
+
interface WorkedExampleGapResult {
|
|
121
|
+
readonly outcome: "gap";
|
|
122
|
+
readonly gap: WorkedExampleGap;
|
|
123
|
+
readonly targetSymbol: string;
|
|
124
|
+
readonly message: string;
|
|
125
|
+
}
|
|
126
|
+
interface WorkedExampleUnresolved {
|
|
127
|
+
readonly outcome: "unresolved";
|
|
128
|
+
readonly targetSymbol: string;
|
|
129
|
+
readonly message: string;
|
|
130
|
+
}
|
|
131
|
+
type WorkedExampleOutcome = WorkedExampleMatch | WorkedExampleMismatch | WorkedExampleGapResult | WorkedExampleUnresolved;
|
|
132
|
+
interface WorkedExampleReport {
|
|
133
|
+
readonly outcomes: readonly WorkedExampleOutcome[];
|
|
134
|
+
readonly total: number;
|
|
135
|
+
readonly matched: number;
|
|
136
|
+
readonly mismatched: number;
|
|
137
|
+
readonly gaps: number;
|
|
138
|
+
readonly unresolved: number;
|
|
139
|
+
readonly coverage: number | undefined;
|
|
140
|
+
}
|
|
141
|
+
interface WorkedExampleOptions {
|
|
142
|
+
readonly relativeTolerance?: number;
|
|
143
|
+
}
|
|
144
|
+
declare function runWorkedExampleSequence(formulas: readonly ContentFormula[], symbolTable?: SymbolTable, options?: WorkedExampleOptions): WorkedExampleReport;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/harness/corpus.d.ts
|
|
147
|
+
declare function collectFormulas(document: ContentDocument): readonly ContentFormula[];
|
|
148
|
+
interface CorpusDocument {
|
|
149
|
+
readonly label: string;
|
|
150
|
+
readonly document: ContentDocument;
|
|
151
|
+
}
|
|
152
|
+
interface CorpusDocumentReport {
|
|
153
|
+
readonly label: string;
|
|
154
|
+
readonly report: WorkedExampleReport;
|
|
155
|
+
}
|
|
156
|
+
interface CorpusReport {
|
|
157
|
+
readonly documents: readonly CorpusDocumentReport[];
|
|
158
|
+
readonly total: number;
|
|
159
|
+
readonly matched: number;
|
|
160
|
+
readonly mismatched: number;
|
|
161
|
+
readonly gaps: number;
|
|
162
|
+
readonly unresolved: number;
|
|
163
|
+
readonly coverage: number | undefined;
|
|
164
|
+
}
|
|
165
|
+
declare function runCorpus(documents: readonly CorpusDocument[], options?: WorkedExampleOptions): CorpusReport;
|
|
166
|
+
declare function formatCorpusReport(report: CorpusReport): string;
|
|
167
|
+
//#endregion
|
|
168
|
+
export { CorpusDocument, CorpusDocumentReport, CorpusReport, DivisionByZeroError, EvaluationResult, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, Rational, SolveForOptions, SolveMethod, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, WorkedExampleGap, WorkedExampleGapResult, WorkedExampleMatch, WorkedExampleMismatch, WorkedExampleOptions, WorkedExampleOutcome, WorkedExampleReport, WorkedExampleUnresolved, absInterval, absQuantity, addIntervals, addQuantities, addRational, collectFormulas, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, formatCorpusReport, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, runCorpus, runWorkedExampleSequence, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
1
|
+
import { ContentDocument, ContentFormula, DimensionVector, ExactRational, FormulaBindings, Interval, MathExpression, Quantity, SiBaseDimension, SymbolTable } from "document-schema.js";
|
|
2
2
|
//#region src/compute/rational.d.ts
|
|
3
3
|
interface Rational {
|
|
4
4
|
readonly n: bigint;
|
|
@@ -103,4 +103,66 @@ interface SolveForOptions {
|
|
|
103
103
|
}
|
|
104
104
|
declare function solveFor(expression: MathExpression, targetValue: number, unknownSymbol: string, bindings: FormulaBindings, options?: SolveForOptions, context?: SymbolTable): number;
|
|
105
105
|
//#endregion
|
|
106
|
-
|
|
106
|
+
//#region src/harness/worked-example.d.ts
|
|
107
|
+
type WorkedExampleGap = "unbound-symbol" | "unknown-unit" | "incompatible-dimensions" | "division-by-zero" | "unsupported-construct" | "numeric-domain" | "non-convergent-solve" | "other-evaluation-error";
|
|
108
|
+
interface WorkedExampleMatch {
|
|
109
|
+
readonly outcome: "match";
|
|
110
|
+
readonly targetSymbol: string;
|
|
111
|
+
readonly expected: Quantity;
|
|
112
|
+
readonly actual: Quantity;
|
|
113
|
+
}
|
|
114
|
+
interface WorkedExampleMismatch {
|
|
115
|
+
readonly outcome: "mismatch";
|
|
116
|
+
readonly targetSymbol: string;
|
|
117
|
+
readonly expected: Quantity;
|
|
118
|
+
readonly actual: Quantity;
|
|
119
|
+
}
|
|
120
|
+
interface WorkedExampleGapResult {
|
|
121
|
+
readonly outcome: "gap";
|
|
122
|
+
readonly gap: WorkedExampleGap;
|
|
123
|
+
readonly targetSymbol: string;
|
|
124
|
+
readonly message: string;
|
|
125
|
+
}
|
|
126
|
+
interface WorkedExampleUnresolved {
|
|
127
|
+
readonly outcome: "unresolved";
|
|
128
|
+
readonly targetSymbol: string;
|
|
129
|
+
readonly message: string;
|
|
130
|
+
}
|
|
131
|
+
type WorkedExampleOutcome = WorkedExampleMatch | WorkedExampleMismatch | WorkedExampleGapResult | WorkedExampleUnresolved;
|
|
132
|
+
interface WorkedExampleReport {
|
|
133
|
+
readonly outcomes: readonly WorkedExampleOutcome[];
|
|
134
|
+
readonly total: number;
|
|
135
|
+
readonly matched: number;
|
|
136
|
+
readonly mismatched: number;
|
|
137
|
+
readonly gaps: number;
|
|
138
|
+
readonly unresolved: number;
|
|
139
|
+
readonly coverage: number | undefined;
|
|
140
|
+
}
|
|
141
|
+
interface WorkedExampleOptions {
|
|
142
|
+
readonly relativeTolerance?: number;
|
|
143
|
+
}
|
|
144
|
+
declare function runWorkedExampleSequence(formulas: readonly ContentFormula[], symbolTable?: SymbolTable, options?: WorkedExampleOptions): WorkedExampleReport;
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/harness/corpus.d.ts
|
|
147
|
+
declare function collectFormulas(document: ContentDocument): readonly ContentFormula[];
|
|
148
|
+
interface CorpusDocument {
|
|
149
|
+
readonly label: string;
|
|
150
|
+
readonly document: ContentDocument;
|
|
151
|
+
}
|
|
152
|
+
interface CorpusDocumentReport {
|
|
153
|
+
readonly label: string;
|
|
154
|
+
readonly report: WorkedExampleReport;
|
|
155
|
+
}
|
|
156
|
+
interface CorpusReport {
|
|
157
|
+
readonly documents: readonly CorpusDocumentReport[];
|
|
158
|
+
readonly total: number;
|
|
159
|
+
readonly matched: number;
|
|
160
|
+
readonly mismatched: number;
|
|
161
|
+
readonly gaps: number;
|
|
162
|
+
readonly unresolved: number;
|
|
163
|
+
readonly coverage: number | undefined;
|
|
164
|
+
}
|
|
165
|
+
declare function runCorpus(documents: readonly CorpusDocument[], options?: WorkedExampleOptions): CorpusReport;
|
|
166
|
+
declare function formatCorpusReport(report: CorpusReport): string;
|
|
167
|
+
//#endregion
|
|
168
|
+
export { CorpusDocument, CorpusDocumentReport, CorpusReport, DivisionByZeroError, EvaluationResult, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, Rational, SolveForOptions, SolveMethod, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, WorkedExampleGap, WorkedExampleGapResult, WorkedExampleMatch, WorkedExampleMismatch, WorkedExampleOptions, WorkedExampleOutcome, WorkedExampleReport, WorkedExampleUnresolved, absInterval, absQuantity, addIntervals, addQuantities, addRational, collectFormulas, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, formatCorpusReport, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, runCorpus, runWorkedExampleSequence, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
|
package/dist/index.js
CHANGED
|
@@ -276,7 +276,7 @@ function absInterval(a) {
|
|
|
276
276
|
}
|
|
277
277
|
//#endregion
|
|
278
278
|
//#region src/compute/evaluate.ts
|
|
279
|
-
const EMPTY_SYMBOL_TABLE$
|
|
279
|
+
const EMPTY_SYMBOL_TABLE$2 = {
|
|
280
280
|
symbols: [],
|
|
281
281
|
units: []
|
|
282
282
|
};
|
|
@@ -286,7 +286,7 @@ function isInterval(value) {
|
|
|
286
286
|
function toInterval(value) {
|
|
287
287
|
return isInterval(value) ? value : pointInterval(value.magnitude, value.dimension);
|
|
288
288
|
}
|
|
289
|
-
function asQuantity(value, context) {
|
|
289
|
+
function asQuantity$1(value, context) {
|
|
290
290
|
if (isInterval(value)) throw new UnsupportedExpressionError(context, "this position requires a plain Quantity, not an Interval");
|
|
291
291
|
return value;
|
|
292
292
|
}
|
|
@@ -322,7 +322,7 @@ const UNARY_OPERATORS = {
|
|
|
322
322
|
"math:cos": { quantity: cosQuantity },
|
|
323
323
|
"math:tan": { quantity: tanQuantity }
|
|
324
324
|
};
|
|
325
|
-
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$
|
|
325
|
+
function evaluate(expression, bindings, context = EMPTY_SYMBOL_TABLE$2) {
|
|
326
326
|
switch (expression.kind) {
|
|
327
327
|
case "num": return quantity(rationalToNumber(toRational(expression)), {});
|
|
328
328
|
case "qty": return evaluateQty(expression, context);
|
|
@@ -374,13 +374,13 @@ function evaluateApp(node, bindings, context) {
|
|
|
374
374
|
}
|
|
375
375
|
if (node.operator === "math:pow") {
|
|
376
376
|
const [base, exponent] = expectTwoArgs(args, "'math:pow'");
|
|
377
|
-
return powQuantity(asQuantity(base, "evaluate"), asQuantity(exponent, "evaluate"));
|
|
377
|
+
return powQuantity(asQuantity$1(base, "evaluate"), asQuantity$1(exponent, "evaluate"));
|
|
378
378
|
}
|
|
379
379
|
throw new UnsupportedExpressionError("evaluate", `unknown operator '${node.operator}'`);
|
|
380
380
|
}
|
|
381
381
|
function evaluateBinder(node, bindings, context) {
|
|
382
|
-
const lower = asQuantity(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
383
|
-
const upper = asQuantity(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
382
|
+
const lower = asQuantity$1(evaluate(node.lower, bindings, context), `evaluate:${node.kind}`);
|
|
383
|
+
const upper = asQuantity$1(evaluate(node.upper, bindings, context), `evaluate:${node.kind}`);
|
|
384
384
|
if (!isDimensionless(lower.dimension) || !isDimensionless(upper.dimension)) throw new IncompatibleDimensionsError(`math:${node.kind}`, lower.dimension, upper.dimension, "binder bounds must be dimensionless");
|
|
385
385
|
if (!Number.isInteger(lower.magnitude) || !Number.isInteger(upper.magnitude)) throw new UnsupportedExpressionError(`evaluate:${node.kind}`, "binder bounds must evaluate to integers");
|
|
386
386
|
let accumulator = node.kind === "sum" ? quantity(0, {}) : quantity(1, {});
|
|
@@ -389,7 +389,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
389
389
|
...bindings,
|
|
390
390
|
[node.binder]: quantity(i, {})
|
|
391
391
|
};
|
|
392
|
-
const bodyValue = asQuantity(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
392
|
+
const bodyValue = asQuantity$1(evaluate(node.body, bodyBindings, context), `evaluate:${node.kind}`);
|
|
393
393
|
accumulator = node.kind === "sum" ? addQuantities(accumulator, bodyValue) : multiplyQuantities(accumulator, bodyValue);
|
|
394
394
|
}
|
|
395
395
|
return accumulator;
|
|
@@ -399,7 +399,7 @@ function evaluateBinder(node, bindings, context) {
|
|
|
399
399
|
const DEFAULT_TOLERANCE = 1e-9;
|
|
400
400
|
const DEFAULT_MAX_ITERATIONS = 100;
|
|
401
401
|
const DEFAULT_DERIVATIVE_STEP = 1e-6;
|
|
402
|
-
const EMPTY_SYMBOL_TABLE = {
|
|
402
|
+
const EMPTY_SYMBOL_TABLE$1 = {
|
|
403
403
|
symbols: [],
|
|
404
404
|
units: []
|
|
405
405
|
};
|
|
@@ -413,7 +413,7 @@ function residualFn(expression, targetValue, unknownSymbol, bindings, context, d
|
|
|
413
413
|
return result.magnitude - targetValue;
|
|
414
414
|
};
|
|
415
415
|
}
|
|
416
|
-
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE) {
|
|
416
|
+
function solveFor(expression, targetValue, unknownSymbol, bindings, options = {}, context = EMPTY_SYMBOL_TABLE$1) {
|
|
417
417
|
const method = options.method ?? "bisection";
|
|
418
418
|
const tolerance = options.tolerance ?? DEFAULT_TOLERANCE;
|
|
419
419
|
const maxIterations = options.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
@@ -455,4 +455,212 @@ function newton(f, initialGuess, tolerance, maxIterations, h) {
|
|
|
455
455
|
throw new NonConvergentSolveError("newton", maxIterations, `residual still exceeds tolerance ${tolerance} after ${maxIterations} iterations`);
|
|
456
456
|
}
|
|
457
457
|
//#endregion
|
|
458
|
-
|
|
458
|
+
//#region src/harness/worked-example.ts
|
|
459
|
+
const DEFAULT_RELATIVE_TOLERANCE = .001;
|
|
460
|
+
const EMPTY_BINDINGS = {};
|
|
461
|
+
const EMPTY_SYMBOL_TABLE = {
|
|
462
|
+
symbols: [],
|
|
463
|
+
units: []
|
|
464
|
+
};
|
|
465
|
+
function asEquality(expression) {
|
|
466
|
+
if (expression.kind !== "app" || expression.operator !== "math:eq") return;
|
|
467
|
+
const [lhs, rhs] = expression.args;
|
|
468
|
+
if (lhs === void 0 || rhs === void 0 || lhs.kind !== "sym") return;
|
|
469
|
+
return {
|
|
470
|
+
targetSymbol: lhs.id,
|
|
471
|
+
rhs
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
function containsSymbol(expression) {
|
|
475
|
+
switch (expression.kind) {
|
|
476
|
+
case "sym": return true;
|
|
477
|
+
case "num":
|
|
478
|
+
case "qty":
|
|
479
|
+
case "unparsed": return false;
|
|
480
|
+
case "app": return expression.args.some(containsSymbol);
|
|
481
|
+
case "sum":
|
|
482
|
+
case "prod": return containsSymbol(expression.lower) || containsSymbol(expression.upper) || containsSymbol(expression.body);
|
|
483
|
+
case "matrix": return expression.rows.some((row) => row.some(containsSymbol));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function asQuantity(value) {
|
|
487
|
+
if (isInterval(value)) throw new UnsupportedExpressionError("runWorkedExampleSequence", "this harness compares point-valued Quantity answers only; a symbol resolving to a range (Interval) has no stated-answer comparison defined yet");
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
function gapFromError(error) {
|
|
491
|
+
if (error instanceof UnboundSymbolError) return "unbound-symbol";
|
|
492
|
+
if (error instanceof UnknownUnitError) return "unknown-unit";
|
|
493
|
+
if (error instanceof IncompatibleDimensionsError) return "incompatible-dimensions";
|
|
494
|
+
if (error instanceof DivisionByZeroError) return "division-by-zero";
|
|
495
|
+
if (error instanceof UnsupportedExpressionError) return "unsupported-construct";
|
|
496
|
+
if (error instanceof NumericDomainError) return "numeric-domain";
|
|
497
|
+
if (error instanceof NonConvergentSolveError) return "non-convergent-solve";
|
|
498
|
+
return "other-evaluation-error";
|
|
499
|
+
}
|
|
500
|
+
function errorMessage(error) {
|
|
501
|
+
return error instanceof Error ? error.message : String(error);
|
|
502
|
+
}
|
|
503
|
+
function withinTolerance(actual, expected, relativeTolerance) {
|
|
504
|
+
if (expected === 0) return Math.abs(actual) <= relativeTolerance;
|
|
505
|
+
return Math.abs(actual - expected) / Math.abs(expected) <= relativeTolerance;
|
|
506
|
+
}
|
|
507
|
+
function resultsMatch(actual, expected, relativeTolerance) {
|
|
508
|
+
return dimensionsEqual(actual.dimension, expected.dimension) && withinTolerance(actual.magnitude, expected.magnitude, relativeTolerance);
|
|
509
|
+
}
|
|
510
|
+
function runWorkedExampleSequence(formulas, symbolTable = EMPTY_SYMBOL_TABLE, options) {
|
|
511
|
+
const relativeTolerance = options?.relativeTolerance ?? DEFAULT_RELATIVE_TOLERANCE;
|
|
512
|
+
const bindings = {};
|
|
513
|
+
const outcomes = [];
|
|
514
|
+
let pending;
|
|
515
|
+
const closeUnresolved = () => {
|
|
516
|
+
if (pending === void 0) return;
|
|
517
|
+
outcomes.push({
|
|
518
|
+
outcome: "unresolved",
|
|
519
|
+
targetSymbol: pending.targetSymbol,
|
|
520
|
+
message: `"${pending.targetSymbol}" was defined but the sequence never restated it as a closed numeric result before ending or being superseded by another definition`
|
|
521
|
+
});
|
|
522
|
+
pending = void 0;
|
|
523
|
+
};
|
|
524
|
+
for (const formula of formulas) {
|
|
525
|
+
if (formula.content === void 0) continue;
|
|
526
|
+
const equality = asEquality(formula.content);
|
|
527
|
+
if (equality === void 0) continue;
|
|
528
|
+
const { targetSymbol, rhs } = equality;
|
|
529
|
+
if (containsSymbol(rhs)) {
|
|
530
|
+
closeUnresolved();
|
|
531
|
+
pending = {
|
|
532
|
+
targetSymbol,
|
|
533
|
+
rhs
|
|
534
|
+
};
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
let closedValue;
|
|
538
|
+
try {
|
|
539
|
+
closedValue = asQuantity(evaluate(rhs, EMPTY_BINDINGS, symbolTable));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
outcomes.push({
|
|
542
|
+
outcome: "gap",
|
|
543
|
+
gap: gapFromError(error),
|
|
544
|
+
targetSymbol,
|
|
545
|
+
message: errorMessage(error)
|
|
546
|
+
});
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (pending?.targetSymbol === targetSymbol) {
|
|
550
|
+
const { rhs: definitionRhs } = pending;
|
|
551
|
+
pending = void 0;
|
|
552
|
+
let actual;
|
|
553
|
+
try {
|
|
554
|
+
actual = asQuantity(evaluate(definitionRhs, bindings, symbolTable));
|
|
555
|
+
} catch (error) {
|
|
556
|
+
outcomes.push({
|
|
557
|
+
outcome: "gap",
|
|
558
|
+
gap: gapFromError(error),
|
|
559
|
+
targetSymbol,
|
|
560
|
+
message: errorMessage(error)
|
|
561
|
+
});
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
outcomes.push(resultsMatch(actual, closedValue, relativeTolerance) ? {
|
|
565
|
+
outcome: "match",
|
|
566
|
+
targetSymbol,
|
|
567
|
+
expected: closedValue,
|
|
568
|
+
actual
|
|
569
|
+
} : {
|
|
570
|
+
outcome: "mismatch",
|
|
571
|
+
targetSymbol,
|
|
572
|
+
expected: closedValue,
|
|
573
|
+
actual
|
|
574
|
+
});
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
bindings[targetSymbol] = closedValue;
|
|
578
|
+
}
|
|
579
|
+
closeUnresolved();
|
|
580
|
+
const matched = outcomes.filter((o) => o.outcome === "match").length;
|
|
581
|
+
const mismatched = outcomes.filter((o) => o.outcome === "mismatch").length;
|
|
582
|
+
const gaps = outcomes.filter((o) => o.outcome === "gap").length;
|
|
583
|
+
const unresolved = outcomes.filter((o) => o.outcome === "unresolved").length;
|
|
584
|
+
return {
|
|
585
|
+
outcomes,
|
|
586
|
+
total: outcomes.length,
|
|
587
|
+
matched,
|
|
588
|
+
mismatched,
|
|
589
|
+
gaps,
|
|
590
|
+
unresolved,
|
|
591
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
//#endregion
|
|
595
|
+
//#region src/harness/corpus.ts
|
|
596
|
+
function collectFormulasFromBlocks(blocks, out) {
|
|
597
|
+
for (const block of blocks) {
|
|
598
|
+
if (block.kind === "table") {
|
|
599
|
+
for (const row of block.rows) for (const cell of row.cells) collectFormulasFromBlocks(cell.blocks, out);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (block.kind === "embeddedObject" && block.objectKind === "formula") {
|
|
603
|
+
const embedded = block.document;
|
|
604
|
+
if (embedded.kind === "formula") out.push(embedded.formula);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function collectFormulas(document) {
|
|
609
|
+
if (document.kind !== "wordprocessing") return [];
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const section of document.sections) collectFormulasFromBlocks(section.blocks, out);
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
function runCorpus(documents, options) {
|
|
615
|
+
const reports = documents.map(({ label, document }) => {
|
|
616
|
+
const symbolTable = document.symbolTable ?? {
|
|
617
|
+
symbols: [],
|
|
618
|
+
units: []
|
|
619
|
+
};
|
|
620
|
+
return {
|
|
621
|
+
label,
|
|
622
|
+
report: runWorkedExampleSequence(collectFormulas(document), symbolTable, options)
|
|
623
|
+
};
|
|
624
|
+
});
|
|
625
|
+
const matched = sumBy(reports, (r) => r.report.matched);
|
|
626
|
+
const mismatched = sumBy(reports, (r) => r.report.mismatched);
|
|
627
|
+
const gaps = sumBy(reports, (r) => r.report.gaps);
|
|
628
|
+
const unresolved = sumBy(reports, (r) => r.report.unresolved);
|
|
629
|
+
return {
|
|
630
|
+
documents: reports,
|
|
631
|
+
total: sumBy(reports, (r) => r.report.total),
|
|
632
|
+
matched,
|
|
633
|
+
mismatched,
|
|
634
|
+
gaps,
|
|
635
|
+
unresolved,
|
|
636
|
+
coverage: matched + mismatched === 0 ? void 0 : matched / (matched + mismatched)
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function sumBy(items, project) {
|
|
640
|
+
return items.reduce((total, item) => total + project(item), 0);
|
|
641
|
+
}
|
|
642
|
+
function formatOutcome(outcome) {
|
|
643
|
+
switch (outcome.outcome) {
|
|
644
|
+
case "match": return `match: ${outcome.targetSymbol}`;
|
|
645
|
+
case "mismatch": return `MISMATCH: ${outcome.targetSymbol} -- expected ${formatEvaluationResult(outcome.expected)}, got ${formatEvaluationResult(outcome.actual)}`;
|
|
646
|
+
case "gap": return `GAP (${outcome.gap}): ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
647
|
+
case "unresolved": return `unresolved: ${outcome.targetSymbol} -- ${outcome.message}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function formatEvaluationResult(value) {
|
|
651
|
+
if (value.kind === "interval") return `[${value.min}, ${value.max}]`;
|
|
652
|
+
return `${value.magnitude}`;
|
|
653
|
+
}
|
|
654
|
+
function formatCorpusReport(report) {
|
|
655
|
+
const lines = [];
|
|
656
|
+
for (const { label, report: documentReport } of report.documents) {
|
|
657
|
+
const coverageText = documentReport.coverage === void 0 ? "no stated answers" : `${(documentReport.coverage * 100).toFixed(1)}% (${documentReport.matched}/${documentReport.matched + documentReport.mismatched})`;
|
|
658
|
+
lines.push(`${label}: ${coverageText}`);
|
|
659
|
+
for (const outcome of documentReport.outcomes) if (outcome.outcome !== "match") lines.push(` ${formatOutcome(outcome)}`);
|
|
660
|
+
}
|
|
661
|
+
const combinedText = report.coverage === void 0 ? "no stated answers in corpus" : `${(report.coverage * 100).toFixed(1)}% (${report.matched}/${report.matched + report.mismatched}), ${report.gaps} gap(s), ${report.unresolved} unresolved`;
|
|
662
|
+
lines.push(`TOTAL: ${combinedText}`);
|
|
663
|
+
return lines.join("\n");
|
|
664
|
+
}
|
|
665
|
+
//#endregion
|
|
666
|
+
export { DivisionByZeroError, IncompatibleDimensionsError, NonConvergentSolveError, NumericDomainError, UnboundSymbolError, UnknownUnitError, UnsupportedExpressionError, absInterval, absQuantity, addIntervals, addQuantities, addRational, collectFormulas, cosQuantity, dimensionExponent, dimensionToString, dimensionsEqual, divideDimensions, divideIntervals, divideQuantities, divideRational, evaluate, formatCorpusReport, interval, isDimensionless, isInterval, multiplyDimensions, multiplyIntervals, multiplyQuantities, multiplyRational, negateInterval, negateQuantity, pointInterval, powQuantity, quantity, rationalToNumber, runCorpus, runWorkedExampleSequence, scaleDimension, sinQuantity, solveFor, sqrtQuantity, subtractIntervals, subtractQuantities, subtractRational, tanQuantity, toExactRational, toRational };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "document-compute.js",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Units-typed, tree-walking evaluator for document-schema.js's MathExpression -- exact-rational unit conversion, interval arithmetic, and bisection/Newton numeric solve-for, the compute package for the documents.js family.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -65,14 +65,16 @@
|
|
|
65
65
|
},
|
|
66
66
|
"packageManager": "pnpm@11.6.0",
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"document-schema.js": "^5.
|
|
68
|
+
"document-schema.js": "^5.2.0"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
72
72
|
"@cloudflare/vitest-pool-workers": "^0.20.1",
|
|
73
73
|
"@types/node": "^26.1.2",
|
|
74
|
+
"documents.js": "^6.1.3",
|
|
74
75
|
"eslint": "^10.8.0",
|
|
75
76
|
"husky": "^9.1.7",
|
|
77
|
+
"markdown-codec": "^6.1.1",
|
|
76
78
|
"publint": "^0.3.21",
|
|
77
79
|
"tsdown": "^0.22.13",
|
|
78
80
|
"turbo": "^2.10.8",
|