@teachinglab/omd 0.2.0 → 0.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.
|
@@ -28,7 +28,10 @@ export class omdUnaryExpressionNode extends omdNode {
|
|
|
28
28
|
this.operation = ast.fn || ast.op;
|
|
29
29
|
|
|
30
30
|
// Populate the argumentNodeList for mathematical child nodes
|
|
31
|
+
this.operand = this.operand;
|
|
32
|
+
this.argument = this.operand;
|
|
31
33
|
this.argumentNodeList.operand = this.operand;
|
|
34
|
+
this.argumentNodeList.argument = this.argument;
|
|
32
35
|
|
|
33
36
|
this.addChild(this.op);
|
|
34
37
|
this.addChild(this.operand);
|
|
@@ -83,14 +86,17 @@ export class omdUnaryExpressionNode extends omdNode {
|
|
|
83
86
|
clone.addChild(backRect);
|
|
84
87
|
|
|
85
88
|
// Manually clone the real children to ensure the entire tree has correct provenance.
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
clone.op = this.op.clone();
|
|
90
|
+
clone.addChild(clone.op);
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
clone.operand = this.operand.clone();
|
|
93
|
+
clone.addChild(clone.operand);
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
// Ensure `argument` alias exists on the clone for compatibility
|
|
96
|
+
clone.argument = clone.operand;
|
|
97
|
+
|
|
98
|
+
// Rebuild the argument list and copy AST data.
|
|
99
|
+
clone.argumentNodeList = { operand: clone.operand, argument: clone.argument };
|
|
94
100
|
clone.astNodeData = JSON.parse(JSON.stringify(this.astNodeData));
|
|
95
101
|
|
|
96
102
|
// The crucial step: link the clone to its origin.
|
|
@@ -99,6 +105,50 @@ export class omdUnaryExpressionNode extends omdNode {
|
|
|
99
105
|
return clone;
|
|
100
106
|
}
|
|
101
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A unary expression is constant if its operand is constant.
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
isConstant() {
|
|
113
|
+
return !!(this.operand && typeof this.operand.isConstant === 'function' && this.operand.isConstant());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get numeric value for unary expression (handles unary minus)
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
getValue() {
|
|
121
|
+
if (!this.isConstant()) throw new Error('Node is not a constant expression');
|
|
122
|
+
const val = this.operand.getValue();
|
|
123
|
+
if (this.operation === 'unaryMinus' || (this.op && this.op.opName === '-')) {
|
|
124
|
+
return -val;
|
|
125
|
+
}
|
|
126
|
+
return val;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* If the operand represents a rational constant, return its rational pair adjusted for sign.
|
|
131
|
+
*/
|
|
132
|
+
getRationalValue() {
|
|
133
|
+
if (!this.isConstant()) throw new Error('Node is not a constant expression');
|
|
134
|
+
|
|
135
|
+
// If operand supports getRationalValue, use it and adjust sign
|
|
136
|
+
if (typeof this.operand.getRationalValue === 'function') {
|
|
137
|
+
const { num, den } = this.operand.getRationalValue();
|
|
138
|
+
if (this.operation === 'unaryMinus' || (this.op && this.op.opName === '-')) {
|
|
139
|
+
return { num: -num, den };
|
|
140
|
+
}
|
|
141
|
+
return { num, den };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback: treat operand as integer rational
|
|
145
|
+
const raw = this.operand.getValue();
|
|
146
|
+
if (this.operation === 'unaryMinus' || (this.op && this.op.opName === '-')) {
|
|
147
|
+
return { num: -raw, den: 1 };
|
|
148
|
+
}
|
|
149
|
+
return { num: raw, den: 1 };
|
|
150
|
+
}
|
|
151
|
+
|
|
102
152
|
/**
|
|
103
153
|
* Converts the omdUnaryExpressionNode to a math.js AST node.
|
|
104
154
|
* @returns {Object} A math.js-compatible AST node.
|
|
@@ -189,7 +189,8 @@ export class SimplificationEngine {
|
|
|
189
189
|
* @returns {boolean} True if the node matches the type, false otherwise.
|
|
190
190
|
*/
|
|
191
191
|
static isType(node, typeName) {
|
|
192
|
-
|
|
192
|
+
if (!node) return false;
|
|
193
|
+
return node.type === typeName;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
/**
|
|
@@ -495,7 +496,8 @@ export class SimplificationEngine {
|
|
|
495
496
|
static getNodeClass(className) {
|
|
496
497
|
const classMap = {
|
|
497
498
|
'omdVariableNode': omdVariableNode,
|
|
498
|
-
'omdPowerNode': omdPowerNode
|
|
499
|
+
'omdPowerNode': omdPowerNode,
|
|
500
|
+
'omdUnaryExpressionNode': omdUnaryExpressionNode
|
|
499
501
|
};
|
|
500
502
|
return classMap[className];
|
|
501
503
|
}
|
|
@@ -528,7 +530,7 @@ export class SimplificationEngine {
|
|
|
528
530
|
*/
|
|
529
531
|
canApply(node) {
|
|
530
532
|
const result = this.match(node);
|
|
531
|
-
return result === false || result === null || result === undefined ? null :
|
|
533
|
+
return result === false || result === null || result === undefined ? null :
|
|
532
534
|
result === true ? {} : result; // Normalize match results
|
|
533
535
|
}
|
|
534
536
|
|
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
import { SimplificationEngine } from '../omdSimplificationEngine.js';
|
|
2
2
|
import * as utils from '../simplificationUtils.js';
|
|
3
|
+
import { omdRationalNode } from '../../nodes/omdRationalNode.js';
|
|
3
4
|
|
|
4
5
|
// ===== RATIONAL NODE RULES =====
|
|
5
6
|
export const rationalRules = [
|
|
7
|
+
// Simplify when both numerator and denominator are unary negatives: (-a)/(-b) -> a/b
|
|
8
|
+
SimplificationEngine.createRule("Unary Minus Cancellation",
|
|
9
|
+
(node) => {
|
|
10
|
+
if (node.type !== 'omdRationalNode') return false;
|
|
11
|
+
|
|
12
|
+
const num = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
13
|
+
const den = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
14
|
+
|
|
15
|
+
if (SimplificationEngine.isType(num, 'omdUnaryExpressionNode') &&
|
|
16
|
+
SimplificationEngine.isType(den, 'omdUnaryExpressionNode')) {
|
|
17
|
+
// Both sides are unary; cancel the unary minus operators
|
|
18
|
+
return {
|
|
19
|
+
numeratorArg: num.argument || num.operand,
|
|
20
|
+
denominatorArg: den.argument || den.operand,
|
|
21
|
+
numeratorNode: num,
|
|
22
|
+
denominatorNode: den
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false;
|
|
27
|
+
},
|
|
28
|
+
(node, data) => {
|
|
29
|
+
// Create a new rational node with arguments unwrapped
|
|
30
|
+
const fontSize = node.getFontSize();
|
|
31
|
+
const numArg = data.numeratorArg;
|
|
32
|
+
const denArg = data.denominatorArg;
|
|
33
|
+
|
|
34
|
+
// If the denominator argument is a constant 1, return the numerator directly (e.g., x/1 -> x)
|
|
35
|
+
if (denArg && typeof denArg.isConstant === 'function' && denArg.isConstant() && denArg.getValue() === 1) {
|
|
36
|
+
const result = numArg.clone();
|
|
37
|
+
// Preserve provenance from original nodes and the rational node
|
|
38
|
+
try { utils.applyProvenance(result, node.numerator, node.denominator, node); } catch (e) {
|
|
39
|
+
// Fallback to manual pushes if applyProvenance fails
|
|
40
|
+
result.provenance = result.provenance || [];
|
|
41
|
+
if (data.numeratorNode) result.provenance.push(data.numeratorNode.id);
|
|
42
|
+
if (data.denominatorNode) result.provenance.push(data.denominatorNode.id);
|
|
43
|
+
result.provenance.push(node.id);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const newNum = numArg.clone();
|
|
49
|
+
const newDen = denArg.clone();
|
|
50
|
+
|
|
51
|
+
const ast = {
|
|
52
|
+
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
53
|
+
args: [newNum.toMathJSNode(), newDen.toMathJSNode()],
|
|
54
|
+
clone: function() { return { ...this, args: this.args.map(a => a.clone()) }; }
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const rational = new omdRationalNode(ast);
|
|
58
|
+
rational.setFontSize(fontSize);
|
|
59
|
+
rational.initialize();
|
|
60
|
+
|
|
61
|
+
// Give granular provenance to numerator and denominator children
|
|
62
|
+
try {
|
|
63
|
+
// Attach provenance to the child numerator and denominator specifically
|
|
64
|
+
if (rational.numerator) utils.applyProvenance(rational.numerator, node.numerator);
|
|
65
|
+
if (rational.denominator) utils.applyProvenance(rational.denominator, node.denominator);
|
|
66
|
+
// Also attach provenance to the rational node itself
|
|
67
|
+
utils.applyProvenance(rational, node.numerator, node.denominator, node);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
_preserveComponentProvenance(rational, data.numeratorNode.argument || data.numeratorNode.operand, data.denominatorNode.argument || data.denominatorNode.operand);
|
|
70
|
+
if (!rational.provenance.includes(node.id)) rational.provenance.push(node.id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return rational;
|
|
74
|
+
},
|
|
75
|
+
(originalNode, ruleData, newNode) => {
|
|
76
|
+
return `Canceled unary negatives in fraction`;
|
|
77
|
+
},
|
|
78
|
+
'rational'
|
|
79
|
+
),
|
|
80
|
+
|
|
81
|
+
// Simplify when numerator is unary minus and denominator is constant -1: (-x)/-1 -> x
|
|
82
|
+
SimplificationEngine.createRule("Unary Numerator Divide By -1",
|
|
83
|
+
(node) => {
|
|
84
|
+
if (node.type !== 'omdRationalNode') return false;
|
|
85
|
+
const num = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
86
|
+
const den = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
87
|
+
|
|
88
|
+
if (SimplificationEngine.isType(num, 'omdUnaryExpressionNode') && den.isConstant && den.isConstant() && den.getValue() === -1) {
|
|
89
|
+
return { numeratorArg: num.argument || num.operand, numeratorNode: num, denominatorNode: den };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
},
|
|
94
|
+
(node, data) => {
|
|
95
|
+
const newNode = data.numeratorArg.clone();
|
|
96
|
+
// Preserve granular provenance: numerator argument's provenance and the denominator and whole node
|
|
97
|
+
try {
|
|
98
|
+
// If the result is a leaf (variable/constant), ensure it gets provenance from numerator/denominator
|
|
99
|
+
utils.applyProvenance(newNode, node.numerator, node.denominator, node);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
newNode.provenance = newNode.provenance || [];
|
|
102
|
+
if (data.numeratorNode) newNode.provenance.push(data.numeratorNode.id);
|
|
103
|
+
if (data.denominatorNode) newNode.provenance.push(data.denominatorNode.id);
|
|
104
|
+
newNode.provenance.push(node.id);
|
|
105
|
+
}
|
|
106
|
+
return newNode;
|
|
107
|
+
},
|
|
108
|
+
(originalNode, ruleData, newNode) => {
|
|
109
|
+
return `Divided by -1 cancels unary minus in numerator`;
|
|
110
|
+
},
|
|
111
|
+
'rational'
|
|
112
|
+
),
|
|
6
113
|
// Simplify x/x = 1 (variable divided by itself)
|
|
7
114
|
SimplificationEngine.createRule("Variable Self Division",
|
|
8
115
|
(node) => {
|
|
@@ -282,6 +389,149 @@ export const rationalRules = [
|
|
|
282
389
|
'rational'
|
|
283
390
|
),
|
|
284
391
|
|
|
392
|
+
// Cancel numeric coefficient when numerator is unary-negative monomial and denominator is negative constant
|
|
393
|
+
SimplificationEngine.createRule("Cancel Negative Coefficient",
|
|
394
|
+
(node) => {
|
|
395
|
+
if (node.type !== 'omdRationalNode') return false;
|
|
396
|
+
|
|
397
|
+
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
398
|
+
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
399
|
+
|
|
400
|
+
// We expect numerator to be unary minus wrapping a multiplication (e.g., -(2*x) )
|
|
401
|
+
if (!SimplificationEngine.isType(numerator, 'omdUnaryExpressionNode')) return false;
|
|
402
|
+
const inner = numerator.argument || numerator.operand;
|
|
403
|
+
if (!SimplificationEngine.isBinaryOp(inner, 'multiply')) return false;
|
|
404
|
+
|
|
405
|
+
// Find the constant operand inside the multiplication
|
|
406
|
+
const constOp = SimplificationEngine.hasConstantOperand(inner);
|
|
407
|
+
if (!constOp || !constOp.constant.isConstant()) return false;
|
|
408
|
+
|
|
409
|
+
// Denominator should be a constant and negative
|
|
410
|
+
if (!denominator.isConstant()) return false;
|
|
411
|
+
const denVal = denominator.getValue();
|
|
412
|
+
if (denVal >= 0) return false;
|
|
413
|
+
|
|
414
|
+
const coeffVal = constOp.constant.getValue();
|
|
415
|
+
|
|
416
|
+
// If absolute values match, we can cancel the coefficient
|
|
417
|
+
if (Math.abs(coeffVal) === Math.abs(denVal)) {
|
|
418
|
+
return {
|
|
419
|
+
innerMultiplication: inner,
|
|
420
|
+
otherFactor: constOp.other,
|
|
421
|
+
numeratorUnary: numerator,
|
|
422
|
+
denominatorNode: denominator
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return false;
|
|
427
|
+
},
|
|
428
|
+
(node, data) => {
|
|
429
|
+
// Return the other factor (e.g., x) possibly adjusting sign if needed
|
|
430
|
+
const newNode = data.otherFactor.clone();
|
|
431
|
+
// Preserve granular provenance from the multiplication and the rational node
|
|
432
|
+
try { utils.applyProvenance(newNode, data.innerMultiplication || node.numerator, node.denominator, node); }
|
|
433
|
+
catch (e) {
|
|
434
|
+
newNode.provenance = newNode.provenance || [];
|
|
435
|
+
newNode.provenance.push(data.numeratorUnary.id);
|
|
436
|
+
newNode.provenance.push(data.denominatorNode.id);
|
|
437
|
+
newNode.provenance.push(node.id);
|
|
438
|
+
}
|
|
439
|
+
return newNode;
|
|
440
|
+
},
|
|
441
|
+
(originalNode, ruleData, newNode) => {
|
|
442
|
+
return `Canceled matching numeric factors between numerator and denominator`;
|
|
443
|
+
},
|
|
444
|
+
'rational'
|
|
445
|
+
),
|
|
446
|
+
|
|
447
|
+
// Handle negative constant numerator over multiplication with negative constant in denominator: (-a)/(-b * rest) -> a/(b * rest)
|
|
448
|
+
SimplificationEngine.createRule("Negative Constant Over Negative Product",
|
|
449
|
+
(node) => {
|
|
450
|
+
if (node.type !== 'omdRationalNode') return false;
|
|
451
|
+
|
|
452
|
+
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
453
|
+
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
454
|
+
|
|
455
|
+
if (!numerator.isConstant || !numerator.isConstant()) return false;
|
|
456
|
+
const numVal = numerator.getValue();
|
|
457
|
+
if (numVal >= 0) return false;
|
|
458
|
+
|
|
459
|
+
// Denominator must be a multiplication with a constant operand that's negative
|
|
460
|
+
if (!SimplificationEngine.isBinaryOp(denominator, 'multiply')) return false;
|
|
461
|
+
const constOp = SimplificationEngine.hasConstantOperand(denominator);
|
|
462
|
+
if (!constOp || !constOp.constant.isConstant()) return false;
|
|
463
|
+
const denConstVal = constOp.constant.getValue();
|
|
464
|
+
if (denConstVal >= 0) return false;
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
numeratorNode: numerator,
|
|
468
|
+
denominatorNode: denominator,
|
|
469
|
+
denominatorConst: constOp.constant,
|
|
470
|
+
denominatorOther: constOp.other
|
|
471
|
+
};
|
|
472
|
+
},
|
|
473
|
+
(node, data) => {
|
|
474
|
+
const fontSize = node.getFontSize();
|
|
475
|
+
|
|
476
|
+
// Build new numerator absolute value
|
|
477
|
+
const newNumConst = SimplificationEngine.createConstant(Math.abs(data.numeratorNode.getValue()), fontSize);
|
|
478
|
+
|
|
479
|
+
// Build new denominator as (abs(denConst) * other)
|
|
480
|
+
const newDenConst = SimplificationEngine.createConstant(Math.abs(data.denominatorConst.getValue()), fontSize);
|
|
481
|
+
const newDenProduct = SimplificationEngine.createBinaryOp(newDenConst, 'multiply', data.denominatorOther.clone(), fontSize);
|
|
482
|
+
|
|
483
|
+
// Construct rational AST
|
|
484
|
+
const ast = {
|
|
485
|
+
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
486
|
+
args: [newNumConst.toMathJSNode(), newDenProduct.toMathJSNode()],
|
|
487
|
+
clone: function() { return { ...this, args: this.args.map(a => a.clone()) }; }
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const rational = new omdRationalNode(ast);
|
|
491
|
+
rational.setFontSize(fontSize);
|
|
492
|
+
rational.initialize();
|
|
493
|
+
|
|
494
|
+
// Debug: show matching components and their provenance
|
|
495
|
+
|
|
496
|
+
// Preserve provenance from original components and whole node.
|
|
497
|
+
// Attach granular provenance to numerator and denominator children as well as the rational node.
|
|
498
|
+
try {
|
|
499
|
+
// Numerator: if original numerator was a unary expression, use its inner operand (the constant '1')
|
|
500
|
+
const numeratorSource = (data && data.numeratorNode && (data.numeratorNode.argument || data.numeratorNode.operand)) || node.numerator;
|
|
501
|
+
if (rational.numerator && numeratorSource) {
|
|
502
|
+
utils.applyProvenance(rational.numerator, numeratorSource);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Denominator: if original denominator was a multiplication, attach provenance to the left/right children
|
|
506
|
+
const denomSourceBinary = (data && data.denominatorNode) || node.denominator;
|
|
507
|
+
if (rational.denominator && SimplificationEngine.isBinaryOp(denomSourceBinary, 'multiply') && SimplificationEngine.isBinaryOp(rational.denominator, 'multiply')) {
|
|
508
|
+
// Attempt to map original const -> left, other -> right
|
|
509
|
+
const leftSource = (data && data.denominatorConst) || (denomSourceBinary.left || denomSourceBinary.right);
|
|
510
|
+
const rightSource = (data && data.denominatorOther) || ((denomSourceBinary.left === leftSource) ? denomSourceBinary.right : denomSourceBinary.left);
|
|
511
|
+
|
|
512
|
+
if (rational.denominator.left && leftSource) utils.applyProvenance(rational.denominator.left, leftSource);
|
|
513
|
+
if (rational.denominator.right && rightSource) utils.applyProvenance(rational.denominator.right, rightSource);
|
|
514
|
+
} else if (rational.denominator) {
|
|
515
|
+
// Fallback: attach provenance to the whole denominator
|
|
516
|
+
utils.applyProvenance(rational.denominator, node.denominator);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Finally, attach provenance to the rational node itself
|
|
520
|
+
utils.applyProvenance(rational, node.numerator, node.denominator, node);
|
|
521
|
+
} catch (e) {
|
|
522
|
+
// Fallback to copying provenance arrays/ids
|
|
523
|
+
_preserveComponentProvenance(rational, node.numerator, node.denominator);
|
|
524
|
+
if (!rational.provenance.includes(node.id)) rational.provenance.push(node.id);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// (debug logs removed)
|
|
528
|
+
|
|
529
|
+
return rational;
|
|
530
|
+
},
|
|
531
|
+
(originalNode, ruleData, newNode) => `Canceled matching negative factors: simplified sign and magnitude`,
|
|
532
|
+
'rational'
|
|
533
|
+
),
|
|
534
|
+
|
|
285
535
|
// Distribute division over addition/subtraction ((4*x+6)/3 → 4/3*x + 2)
|
|
286
536
|
SimplificationEngine.createRule("Simplify Rational Division",
|
|
287
537
|
(node) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teachinglab/omd",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "omd",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"start": "node --env-file=.env server.js",
|
|
43
|
-
"dev": "
|
|
43
|
+
"dev": "vite"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
@@ -339,8 +339,8 @@ export class omdCoordinatePlane extends jsvgGroup {
|
|
|
339
339
|
|
|
340
340
|
const compiledExpression = math.compile(expression);
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
const leftLimit = (domain && typeof domain.min === 'number') ? domain.min : this.xMin;
|
|
343
|
+
const rightLimit = (domain && typeof domain.max === 'number') ? domain.max : this.xMax;
|
|
344
344
|
|
|
345
345
|
const step = Math.abs(rightLimit - leftLimit) / 1000;
|
|
346
346
|
for (let x = leftLimit; x <= rightLimit; x += step) {
|