@teachinglab/omd 0.6.0 → 0.6.2
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 +257 -251
- package/README.old.md +137 -137
- package/canvas/core/canvasConfig.js +202 -202
- package/canvas/drawing/segment.js +167 -167
- package/canvas/drawing/stroke.js +385 -385
- package/canvas/events/eventManager.js +444 -444
- package/canvas/events/pointerEventHandler.js +262 -262
- package/canvas/index.js +48 -48
- package/canvas/tools/PointerTool.js +71 -71
- package/canvas/tools/tool.js +222 -222
- package/canvas/utils/boundingBox.js +377 -377
- package/canvas/utils/mathUtils.js +258 -258
- package/docs/api/configuration-options.md +198 -198
- package/docs/api/eventManager.md +82 -82
- package/docs/api/focusFrameManager.md +144 -144
- package/docs/api/index.md +105 -105
- package/docs/api/main.md +62 -62
- package/docs/api/omdBinaryExpressionNode.md +86 -86
- package/docs/api/omdCanvas.md +83 -83
- package/docs/api/omdConfigManager.md +112 -112
- package/docs/api/omdConstantNode.md +52 -52
- package/docs/api/omdDisplay.md +87 -87
- package/docs/api/omdEquationNode.md +174 -174
- package/docs/api/omdEquationSequenceNode.md +258 -258
- package/docs/api/omdEquationStack.md +192 -192
- package/docs/api/omdFunctionNode.md +82 -82
- package/docs/api/omdGroupNode.md +78 -78
- package/docs/api/omdHelpers.md +87 -87
- package/docs/api/omdLeafNode.md +85 -85
- package/docs/api/omdNode.md +201 -201
- package/docs/api/omdOperationDisplayNode.md +117 -117
- package/docs/api/omdOperatorNode.md +91 -91
- package/docs/api/omdParenthesisNode.md +133 -133
- package/docs/api/omdPopup.md +191 -191
- package/docs/api/omdPowerNode.md +131 -131
- package/docs/api/omdRationalNode.md +144 -144
- package/docs/api/omdSequenceNode.md +128 -128
- package/docs/api/omdSimplification.md +78 -78
- package/docs/api/omdSqrtNode.md +144 -144
- package/docs/api/omdStepVisualizer.md +146 -146
- package/docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/docs/api/omdStepVisualizerLayout.md +70 -70
- package/docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/docs/api/omdToolbar.md +130 -130
- package/docs/api/omdTranscriptionService.md +95 -95
- package/docs/api/omdTreeDiff.md +169 -169
- package/docs/api/omdUnaryExpressionNode.md +137 -137
- package/docs/api/omdUtilities.md +82 -82
- package/docs/api/omdVariableNode.md +123 -123
- package/docs/api/selectTool.md +74 -74
- package/docs/api/simplificationEngine.md +97 -97
- package/docs/api/simplificationRules.md +76 -76
- package/docs/api/simplificationUtils.md +64 -64
- package/docs/api/transcribe.md +43 -43
- package/docs/api-reference.md +85 -85
- package/docs/index.html +453 -453
- package/docs/index.md +38 -38
- package/docs/omd-objects.md +258 -258
- package/index.js +79 -79
- package/jsvg/index.js +3 -0
- package/jsvg/jsvg.js +898 -898
- package/jsvg/jsvgComponents.js +357 -358
- package/npm-docs/DOCUMENTATION_SUMMARY.md +220 -220
- package/npm-docs/README.md +251 -251
- package/npm-docs/api/api-reference.md +85 -85
- package/npm-docs/api/configuration-options.md +198 -198
- package/npm-docs/api/eventManager.md +82 -82
- package/npm-docs/api/expression-nodes.md +561 -561
- package/npm-docs/api/focusFrameManager.md +144 -144
- package/npm-docs/api/index.md +105 -105
- package/npm-docs/api/main.md +62 -62
- package/npm-docs/api/omdBinaryExpressionNode.md +86 -86
- package/npm-docs/api/omdCanvas.md +83 -83
- package/npm-docs/api/omdConfigManager.md +112 -112
- package/npm-docs/api/omdConstantNode.md +52 -52
- package/npm-docs/api/omdDisplay.md +87 -87
- package/npm-docs/api/omdEquationNode.md +174 -174
- package/npm-docs/api/omdEquationSequenceNode.md +258 -258
- package/npm-docs/api/omdEquationStack.md +192 -192
- package/npm-docs/api/omdFunctionNode.md +82 -82
- package/npm-docs/api/omdGroupNode.md +78 -78
- package/npm-docs/api/omdHelpers.md +87 -87
- package/npm-docs/api/omdLeafNode.md +85 -85
- package/npm-docs/api/omdNode.md +201 -201
- package/npm-docs/api/omdOperationDisplayNode.md +117 -117
- package/npm-docs/api/omdOperatorNode.md +91 -91
- package/npm-docs/api/omdParenthesisNode.md +133 -133
- package/npm-docs/api/omdPopup.md +191 -191
- package/npm-docs/api/omdPowerNode.md +131 -131
- package/npm-docs/api/omdRationalNode.md +144 -144
- package/npm-docs/api/omdSequenceNode.md +128 -128
- package/npm-docs/api/omdSimplification.md +78 -78
- package/npm-docs/api/omdSqrtNode.md +144 -144
- package/npm-docs/api/omdStepVisualizer.md +146 -146
- package/npm-docs/api/omdStepVisualizerHighlighting.md +65 -65
- package/npm-docs/api/omdStepVisualizerInteractiveSteps.md +108 -108
- package/npm-docs/api/omdStepVisualizerLayout.md +70 -70
- package/npm-docs/api/omdStepVisualizerNodeUtils.md +140 -140
- package/npm-docs/api/omdStepVisualizerTextBoxes.md +76 -76
- package/npm-docs/api/omdToolbar.md +130 -130
- package/npm-docs/api/omdTranscriptionService.md +95 -95
- package/npm-docs/api/omdTreeDiff.md +169 -169
- package/npm-docs/api/omdUnaryExpressionNode.md +137 -137
- package/npm-docs/api/omdUtilities.md +82 -82
- package/npm-docs/api/omdVariableNode.md +123 -123
- package/npm-docs/api/selectTool.md +74 -74
- package/npm-docs/api/simplificationEngine.md +97 -97
- package/npm-docs/api/simplificationRules.md +76 -76
- package/npm-docs/api/simplificationUtils.md +64 -64
- package/npm-docs/api/transcribe.md +43 -43
- package/npm-docs/guides/equations.md +854 -854
- package/npm-docs/guides/factory-functions.md +354 -354
- package/npm-docs/guides/getting-started.md +318 -318
- package/npm-docs/guides/quick-examples.md +525 -525
- package/npm-docs/guides/visualizations.md +682 -682
- package/npm-docs/index.html +12 -0
- package/npm-docs/json-schemas.md +826 -826
- package/omd/config/omdConfigManager.js +279 -267
- package/omd/core/index.js +158 -158
- package/omd/core/omdEquationStack.js +546 -546
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +969 -962
- package/omd/display/omdToolbar.js +501 -501
- package/omd/nodes/omdBinaryExpressionNode.js +459 -459
- package/omd/nodes/omdConstantNode.js +141 -141
- package/omd/nodes/omdEquationNode.js +1327 -1327
- package/omd/nodes/omdFunctionNode.js +351 -351
- package/omd/nodes/omdGroupNode.js +67 -67
- package/omd/nodes/omdLeafNode.js +76 -76
- package/omd/nodes/omdNode.js +556 -556
- package/omd/nodes/omdOperationDisplayNode.js +321 -321
- package/omd/nodes/omdOperatorNode.js +108 -108
- package/omd/nodes/omdParenthesisNode.js +292 -292
- package/omd/nodes/omdPowerNode.js +235 -235
- package/omd/nodes/omdRationalNode.js +295 -295
- package/omd/nodes/omdSqrtNode.js +307 -307
- package/omd/nodes/omdUnaryExpressionNode.js +227 -227
- package/omd/nodes/omdVariableNode.js +122 -122
- package/omd/simplification/omdSimplification.js +140 -140
- package/omd/simplification/omdSimplificationEngine.js +887 -887
- package/omd/simplification/package.json +5 -5
- package/omd/simplification/rules/binaryRules.js +1037 -1037
- package/omd/simplification/rules/functionRules.js +111 -111
- package/omd/simplification/rules/index.js +48 -48
- package/omd/simplification/rules/parenthesisRules.js +19 -19
- package/omd/simplification/rules/powerRules.js +143 -143
- package/omd/simplification/rules/rationalRules.js +725 -725
- package/omd/simplification/rules/sqrtRules.js +48 -48
- package/omd/simplification/rules/unaryRules.js +37 -37
- package/omd/simplification/simplificationRules.js +31 -31
- package/omd/simplification/simplificationUtils.js +1055 -1055
- package/omd/step-visualizer/omdStepVisualizer.js +947 -947
- package/omd/step-visualizer/omdStepVisualizerHighlighting.js +246 -246
- package/omd/step-visualizer/omdStepVisualizerLayout.js +892 -892
- package/omd/step-visualizer/omdStepVisualizerTextBoxes.js +200 -200
- package/omd/utils/aiNextEquationStep.js +106 -106
- package/omd/utils/omdNodeOverlay.js +638 -638
- package/omd/utils/omdPopup.js +1203 -1203
- package/omd/utils/omdStepVisualizerInteractiveSteps.js +684 -684
- package/omd/utils/omdStepVisualizerNodeUtils.js +267 -267
- package/omd/utils/omdTranscriptionService.js +123 -123
- package/omd/utils/omdTreeDiff.js +733 -733
- package/package.json +59 -56
- package/readme.html +184 -120
- package/src/index.js +74 -74
- package/src/json-schemas.md +576 -576
- package/src/omd-json-samples.js +147 -147
- package/src/omdApp.js +391 -391
- package/src/omdAppCanvas.js +335 -335
- package/src/omdBalanceHanger.js +199 -199
- package/src/omdColor.js +13 -13
- package/src/omdCoordinatePlane.js +541 -541
- package/src/omdExpression.js +115 -115
- package/src/omdFactory.js +150 -150
- package/src/omdFunction.js +114 -114
- package/src/omdMetaExpression.js +290 -290
- package/src/omdNaturalExpression.js +563 -563
- package/src/omdNode.js +383 -383
- package/src/omdNumber.js +52 -52
- package/src/omdNumberLine.js +114 -112
- package/src/omdNumberTile.js +118 -118
- package/src/omdOperator.js +72 -72
- package/src/omdPowerExpression.js +91 -91
- package/src/omdProblem.js +259 -259
- package/src/omdRatioChart.js +251 -251
- package/src/omdRationalExpression.js +114 -114
- package/src/omdSampleData.js +215 -215
- package/src/omdShapes.js +512 -512
- package/src/omdSpinner.js +151 -151
- package/src/omdString.js +49 -49
- package/src/omdTable.js +498 -498
- package/src/omdTapeDiagram.js +244 -244
- package/src/omdTerm.js +91 -91
- package/src/omdTileEquation.js +349 -349
- package/src/omdUtils.js +84 -84
- package/src/omdVariable.js +51 -51
|
@@ -1,888 +1,888 @@
|
|
|
1
|
-
import { omdRationalNode } from "../nodes/omdRationalNode.js";
|
|
2
|
-
import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
|
|
3
|
-
import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
|
|
4
|
-
import { omdVariableNode } from "../nodes/omdVariableNode.js";
|
|
5
|
-
import { omdPowerNode } from "../nodes/omdPowerNode.js";
|
|
6
|
-
import { omdConstantNode } from "../nodes/omdConstantNode.js";
|
|
7
|
-
import { createConstantNode, applyProvenance } from './simplificationUtils.js';
|
|
8
|
-
import * as utils from './simplificationUtils.js';
|
|
9
|
-
import { useImplicitMultiplication, getMultiplicationSymbol } from "../config/omdConfigManager.js";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @class SimplificationEngine
|
|
13
|
-
* @classdesc Provides a collection of static methods for creating, matching, and transforming
|
|
14
|
-
* mathematical expression nodes for simplification purposes.
|
|
15
|
-
* This class serves as the core logic for applying simplification rules within the OMD system.
|
|
16
|
-
*/
|
|
17
|
-
export class SimplificationEngine {
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* ===== SIMPLIFIED NODE CREATION HELPERS =====
|
|
21
|
-
* These functions provide an easy way to create various types of AST nodes
|
|
22
|
-
* without directly dealing with the complexities of the Math.js AST structure.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates a new constant node.
|
|
27
|
-
* @param {number} value - The numeric value of the constant.
|
|
28
|
-
* @param {number} [fontSize=16] - The font size for the node.
|
|
29
|
-
* @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
|
|
30
|
-
* @returns {Object} A new constant node.
|
|
31
|
-
*/
|
|
32
|
-
static createConstant(value, fontSize = 16, ...sourceNodes) {
|
|
33
|
-
const node = createConstantNode(value, fontSize);
|
|
34
|
-
|
|
35
|
-
// Apply manual provenance tracking if source nodes provided
|
|
36
|
-
if (sourceNodes.length > 0) {
|
|
37
|
-
applyProvenance(node, ...sourceNodes);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return node;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Creates a new binary expression node.
|
|
45
|
-
* @param {Object} left - The left-hand side operand node.
|
|
46
|
-
* @param {string} operator - The operator (e.g., '+', '-', '*', '/').
|
|
47
|
-
* @param {Object} right - The right-hand side operand node.
|
|
48
|
-
* @param {number} [fontSize=16] - The font size for the node.
|
|
49
|
-
* @param {Array|null} [provenance=null] - The provenance array for the node (deprecated - use ...sourceNodes instead).
|
|
50
|
-
* @param {omdNode|null} [operatorNode=null] - The original operator node for provenance (deprecated - use ...sourceNodes instead).
|
|
51
|
-
* @param {...omdNode} sourceNodes - Additional source nodes for automatic provenance tracking.
|
|
52
|
-
* @returns {Object} A new binary expression node.
|
|
53
|
-
* @throws {Error} If an unknown operator is provided.
|
|
54
|
-
*/
|
|
55
|
-
static createBinaryOp(left, operator, right, fontSize = 16, provenance = null, operatorNode = null, ...sourceNodes) {
|
|
56
|
-
const opMap = {
|
|
57
|
-
'add': { op: '+', fn: 'add' },
|
|
58
|
-
'subtract': { op: '−', fn: 'subtract' },
|
|
59
|
-
'multiply': { op: getMultiplicationSymbol(), fn: 'multiply' },
|
|
60
|
-
'divide': { op: '÷', fn: 'divide' }
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const opInfo = opMap[operator];
|
|
64
|
-
if (!opInfo) throw new Error(`Unknown operator: ${operator}`);
|
|
65
|
-
|
|
66
|
-
// For multiplication, reorder operands if needed (e.g., x*2 -> 2*x)
|
|
67
|
-
let leftOperand = left;
|
|
68
|
-
let rightOperand = right;
|
|
69
|
-
|
|
70
|
-
if (operator === 'multiply') {
|
|
71
|
-
const leftIsConstant = (left instanceof omdConstantNode && left.isConstant());
|
|
72
|
-
const rightIsConstant = (right instanceof omdConstantNode && right.isConstant());
|
|
73
|
-
|
|
74
|
-
// If left is non-constant and right is constant, swap them
|
|
75
|
-
if (!leftIsConstant && rightIsConstant) {
|
|
76
|
-
leftOperand = right;
|
|
77
|
-
rightOperand = left;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const ast = {
|
|
82
|
-
type: 'OperatorNode',
|
|
83
|
-
op: opInfo.op,
|
|
84
|
-
fn: opInfo.fn,
|
|
85
|
-
args: [leftOperand.toMathJSNode(), rightOperand.toMathJSNode()],
|
|
86
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Pass operator provenance into the constructor via the AST
|
|
90
|
-
if (operatorNode) {
|
|
91
|
-
ast.operatorProvenance = [operatorNode.id];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const node = new omdBinaryExpressionNode(ast);
|
|
95
|
-
node.setFontSize(fontSize);
|
|
96
|
-
|
|
97
|
-
// Use manual provenance tracking
|
|
98
|
-
if (provenance) {
|
|
99
|
-
// Legacy support: apply manual provenance
|
|
100
|
-
node.provenance = provenance;
|
|
101
|
-
} else {
|
|
102
|
-
// Manual provenance tracking for binary operations
|
|
103
|
-
const allSources = [leftOperand, rightOperand];
|
|
104
|
-
if (operatorNode) allSources.push(operatorNode);
|
|
105
|
-
allSources.push(...sourceNodes);
|
|
106
|
-
applyProvenance(node, ...allSources);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
node.initialize();
|
|
110
|
-
return node;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Creates a multiplication expression where the left operand is a constant value.
|
|
115
|
-
* @param {Object} leftConstantNode - The constant node for the left operand.
|
|
116
|
-
* @param {Object} rightNode - The node for the right operand.
|
|
117
|
-
* @param {number} [fontSize=16] - The font size for the node.
|
|
118
|
-
* @param {Array|null} [provenance=null] - The provenance array for the node.
|
|
119
|
-
* @returns {Object} A new binary expression node representing multiplication.
|
|
120
|
-
*/
|
|
121
|
-
static createMultiplication(leftConstantNode, rightNode, fontSize = 16, provenance = null) {
|
|
122
|
-
// Clone the constant to create a new instance for the new expression,
|
|
123
|
-
// but ensure its provenance points back to the original constant.
|
|
124
|
-
const newLeftConstant = leftConstantNode.clone();
|
|
125
|
-
newLeftConstant.provenance = [leftConstantNode.id];
|
|
126
|
-
|
|
127
|
-
const rightClone = rightNode.clone(); // Clone the other node to ensure we don't modify the original
|
|
128
|
-
|
|
129
|
-
// Safely get the operator node for provenance, handling cases where parent might be null
|
|
130
|
-
const operatorNode = leftConstantNode.parent?.op || null;
|
|
131
|
-
|
|
132
|
-
return SimplificationEngine.createBinaryOp(newLeftConstant, 'multiply', rightClone, fontSize, provenance, operatorNode);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Creates a rational node (fraction).
|
|
137
|
-
* @param {number} numerator - The numerator value.
|
|
138
|
-
* @param {number} denominator - The denominator value.
|
|
139
|
-
* @param {number} [fontSize=16] - The font size for the node.
|
|
140
|
-
* @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
|
|
141
|
-
* @returns {Object} A new rational node, or a constant node if the denominator is 1.
|
|
142
|
-
*/
|
|
143
|
-
static createRational(numerator, denominator, fontSize = 16, ...sourceNodes) {
|
|
144
|
-
if (denominator === 1) {
|
|
145
|
-
return SimplificationEngine.createConstant(numerator, fontSize, ...sourceNodes);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const ast = {
|
|
149
|
-
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
150
|
-
args: [
|
|
151
|
-
{ type: 'ConstantNode', value: Math.abs(numerator) },
|
|
152
|
-
{ type: 'ConstantNode', value: denominator }
|
|
153
|
-
],
|
|
154
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
let node = new omdRationalNode(ast);
|
|
158
|
-
|
|
159
|
-
// Handle negative fractions by wrapping in a unary minus node
|
|
160
|
-
if (numerator < 0) {
|
|
161
|
-
const unaryAST = {
|
|
162
|
-
type: 'OperatorNode', op: '-', fn: 'unaryMinus',
|
|
163
|
-
args: [node.toMathJSNode()],
|
|
164
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
165
|
-
};
|
|
166
|
-
node = new omdUnaryExpressionNode(unaryAST);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
node.setFontSize(fontSize);
|
|
170
|
-
|
|
171
|
-
// Apply manual provenance tracking if source nodes provided
|
|
172
|
-
if (sourceNodes.length > 0) {
|
|
173
|
-
applyProvenance(node, ...sourceNodes);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
node.initialize();
|
|
177
|
-
return node;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* ===== SIMPLIFIED PATTERN MATCHING HELPERS =====
|
|
182
|
-
* These functions provide convenient ways to check common patterns in AST nodes.
|
|
183
|
-
*/
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Checks if a node is of a specific type.
|
|
187
|
-
* @param {Object} node - The node to check.
|
|
188
|
-
* @param {string} typeName - The name of the constructor type (e.g., 'omdConstantNode').
|
|
189
|
-
* @returns {boolean} True if the node matches the type, false otherwise.
|
|
190
|
-
*/
|
|
191
|
-
static isType(node, typeName) {
|
|
192
|
-
if (!node) return false;
|
|
193
|
-
return node.type === typeName;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Checks if a node is a binary operation, optionally with a specific operator.
|
|
198
|
-
* @param {Object} node - The node to check.
|
|
199
|
-
* @param {string} [operator=null] - The specific operator to check for (e.g., 'add', 'multiply').
|
|
200
|
-
* @returns {boolean} True if the node is a binary operation (and matches operator if provided), false otherwise.
|
|
201
|
-
*/
|
|
202
|
-
static isBinaryOp(node, operator = null) {
|
|
203
|
-
if (!SimplificationEngine.isType(node, 'omdBinaryExpressionNode')) return false;
|
|
204
|
-
|
|
205
|
-
if (!operator) return true;
|
|
206
|
-
|
|
207
|
-
// Handle both string operations and object operations (from Math.js AST)
|
|
208
|
-
let nodeOp = node.operation;
|
|
209
|
-
if (typeof nodeOp === 'object' && nodeOp.name) {
|
|
210
|
-
nodeOp = nodeOp.name;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return nodeOp === operator;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Checks if a node is a constant, optionally with a specific value.
|
|
218
|
-
* @param {Object} node - The node to check.
|
|
219
|
-
* @param {number} [value=null] - The specific constant value to check for.
|
|
220
|
-
* @returns {boolean} True if the node is a constant (and matches value if provided), false otherwise.
|
|
221
|
-
*/
|
|
222
|
-
static isConstantValue(node, value = null) {
|
|
223
|
-
if (!node.isConstant()) return false;
|
|
224
|
-
return value !== null ? node.getValue() === value : true;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Checks if a binary operation node has a constant operand and returns it along with the other operand.
|
|
229
|
-
* @param {Object} node - The binary expression node to check.
|
|
230
|
-
* @returns {Object|null} An object containing the constant and other operand, or null if no constant operand is found.
|
|
231
|
-
*/
|
|
232
|
-
static hasConstantOperand(node) {
|
|
233
|
-
if (!SimplificationEngine.isBinaryOp(node)) return null;
|
|
234
|
-
|
|
235
|
-
if (node.left.isConstant()) return { constant: node.left, other: node.right };
|
|
236
|
-
if (node.right.isConstant()) return { constant: node.right, other: node.left };
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Unwraps a parenthesis node, returning its inner expression. If the node is not a parenthesis node, it returns the node itself.
|
|
242
|
-
* @param {Object} node - The node to unwrap.
|
|
243
|
-
* @returns {Object} The inner expression if the node is a parenthesis node, otherwise the original node.
|
|
244
|
-
*/
|
|
245
|
-
static unwrapParentheses(node) {
|
|
246
|
-
return SimplificationEngine.isType(node, 'omdParenthesisNode') ? node.expression : node;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* ===== MONOMIAL AND LIKE TERMS HELPERS =====
|
|
251
|
-
* These functions help identify and work with monomials and like terms.
|
|
252
|
-
*/
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Checks if a node represents a monomial (a term of the form coefficient * variable^power).
|
|
256
|
-
* Examples: x, 2x, 3y, -5z, x^2, 2x^3
|
|
257
|
-
* @param {Object} node - The node to check.
|
|
258
|
-
* @returns {Object|null} An object with coefficient, variable, and power if it's a monomial, null otherwise.
|
|
259
|
-
*/
|
|
260
|
-
static isMonomial(node) {
|
|
261
|
-
let coefficientMultiplier = 1;
|
|
262
|
-
|
|
263
|
-
// Unwrap parentheses first, as they can contain a unary minus
|
|
264
|
-
node = SimplificationEngine.unwrapParentheses(node);
|
|
265
|
-
|
|
266
|
-
// Handle any number of nested unary minuses by repeatedly unwrapping them
|
|
267
|
-
// and flipping the coefficient multiplier.
|
|
268
|
-
while (SimplificationEngine.isType(node, 'omdUnaryExpressionNode') && node.operation === 'unaryMinus') {
|
|
269
|
-
node = node.argument;
|
|
270
|
-
coefficientMultiplier *= -1;
|
|
271
|
-
// The argument of the unary minus could also be in parentheses
|
|
272
|
-
node = SimplificationEngine.unwrapParentheses(node);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Case 1: Just a variable (e.g., x)
|
|
276
|
-
if (SimplificationEngine.isType(node, 'omdVariableNode')) {
|
|
277
|
-
return {
|
|
278
|
-
coefficient: 1 * coefficientMultiplier,
|
|
279
|
-
variable: node.name,
|
|
280
|
-
power: 1,
|
|
281
|
-
variableNode: node
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Case 2: Constant * Variable (e.g., 2x, 3y)
|
|
286
|
-
if (SimplificationEngine.isBinaryOp(node, 'multiply')) {
|
|
287
|
-
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
288
|
-
if (constOp && SimplificationEngine.isType(constOp.other, 'omdVariableNode')) {
|
|
289
|
-
return {
|
|
290
|
-
coefficient: constOp.constant.getValue() * coefficientMultiplier,
|
|
291
|
-
variable: constOp.other.name,
|
|
292
|
-
power: 1,
|
|
293
|
-
variableNode: constOp.other,
|
|
294
|
-
coefficientNode: constOp.constant
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Case 3: Variable * Constant (e.g., x*2, y*3) - less common but possible
|
|
299
|
-
if (constOp && SimplificationEngine.isType(constOp.constant, 'omdVariableNode') && constOp.other.isConstant()) {
|
|
300
|
-
return {
|
|
301
|
-
coefficient: constOp.other.getValue() * coefficientMultiplier,
|
|
302
|
-
variable: constOp.constant.name,
|
|
303
|
-
power: 1,
|
|
304
|
-
variableNode: constOp.constant,
|
|
305
|
-
coefficientNode: constOp.other
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Case 4: Constant * Power (e.g., 2*x^3)
|
|
310
|
-
const constPowerOp = SimplificationEngine.hasConstantOperand(node);
|
|
311
|
-
if (constPowerOp && SimplificationEngine.isType(constPowerOp.other, 'omdPowerNode')) {
|
|
312
|
-
const powerNode = constPowerOp.other;
|
|
313
|
-
if (SimplificationEngine.isType(powerNode.base, 'omdVariableNode') && powerNode.exponent.isConstant()) {
|
|
314
|
-
return {
|
|
315
|
-
coefficient: constPowerOp.constant.getValue() * coefficientMultiplier,
|
|
316
|
-
variable: powerNode.base.name,
|
|
317
|
-
power: powerNode.exponent.getValue(),
|
|
318
|
-
variableNode: powerNode.base,
|
|
319
|
-
coefficientNode: constPowerOp.constant,
|
|
320
|
-
powerNode: powerNode
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Case 5: Just a power (e.g., x^2)
|
|
327
|
-
if (SimplificationEngine.isType(node, 'omdPowerNode')) {
|
|
328
|
-
if (SimplificationEngine.isType(node.base, 'omdVariableNode') && node.exponent.isConstant()) {
|
|
329
|
-
return {
|
|
330
|
-
coefficient: 1 * coefficientMultiplier,
|
|
331
|
-
variable: node.base.name,
|
|
332
|
-
power: node.exponent.getValue(),
|
|
333
|
-
variableNode: node.base,
|
|
334
|
-
powerNode: node
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Checks if two monomials are like terms (same variable and power).
|
|
344
|
-
* @param {Object} monomial1 - First monomial info from isMonomial().
|
|
345
|
-
* @param {Object} monomial2 - Second monomial info from isMonomial().
|
|
346
|
-
* @returns {boolean} True if they are like terms, false otherwise.
|
|
347
|
-
*/
|
|
348
|
-
static areLikeTerms(monomial1, monomial2) {
|
|
349
|
-
return monomial1.variable === monomial2.variable && monomial1.power === monomial2.power;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Creates a monomial node from coefficient, variable, and power.
|
|
354
|
-
* @param {number} coefficient - The coefficient of the monomial.
|
|
355
|
-
* @param {string} variable - The variable name.
|
|
356
|
-
* @param {number} power - The power of the variable.
|
|
357
|
-
* @param {number} fontSize - The font size for the node.
|
|
358
|
-
* @param {Array} [provenance=[]] - The provenance array to preserve lineage.
|
|
359
|
-
* @returns {Object} A new monomial node.
|
|
360
|
-
*/
|
|
361
|
-
static createMonomial(coefficient, variable, power, fontSize, provenance = []) {
|
|
362
|
-
// Create variable node
|
|
363
|
-
const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
|
|
364
|
-
const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
|
|
365
|
-
variableNode.setFontSize(fontSize);
|
|
366
|
-
variableNode.initialize();
|
|
367
|
-
|
|
368
|
-
// CRITICAL: Set provenance on the variable node
|
|
369
|
-
if (provenance && provenance.length > 0) {
|
|
370
|
-
variableNode.provenance = [...provenance];
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
let termNode = variableNode;
|
|
374
|
-
|
|
375
|
-
// Add power if not 1
|
|
376
|
-
if (power !== 1) {
|
|
377
|
-
const powerAST = {
|
|
378
|
-
type: 'OperatorNode',
|
|
379
|
-
op: '^',
|
|
380
|
-
fn: 'pow',
|
|
381
|
-
args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
|
|
382
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
383
|
-
};
|
|
384
|
-
termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
|
|
385
|
-
termNode.setFontSize(fontSize);
|
|
386
|
-
termNode.initialize();
|
|
387
|
-
|
|
388
|
-
// CRITICAL: Set provenance on the power node and its components
|
|
389
|
-
if (provenance && provenance.length > 0) {
|
|
390
|
-
termNode.provenance = [...provenance];
|
|
391
|
-
// Set provenance on the base (variable) and exponent
|
|
392
|
-
if (termNode.base) {
|
|
393
|
-
termNode.base.provenance = [...provenance];
|
|
394
|
-
}
|
|
395
|
-
if (termNode.exponent) {
|
|
396
|
-
termNode.exponent.provenance = [...provenance];
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
let result;
|
|
402
|
-
if (coefficient === 1) {
|
|
403
|
-
result = termNode;
|
|
404
|
-
} else if (coefficient === -1) {
|
|
405
|
-
// Create unary minus
|
|
406
|
-
const unaryAST = {
|
|
407
|
-
type: 'OperatorNode',
|
|
408
|
-
op: '-',
|
|
409
|
-
fn: 'unaryMinus',
|
|
410
|
-
args: [termNode.toMathJSNode()],
|
|
411
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
412
|
-
};
|
|
413
|
-
result = new omdUnaryExpressionNode(unaryAST);
|
|
414
|
-
result.setFontSize(fontSize);
|
|
415
|
-
result.initialize();
|
|
416
|
-
|
|
417
|
-
// CRITICAL: Set provenance on unary minus and its argument
|
|
418
|
-
if (provenance && provenance.length > 0) {
|
|
419
|
-
result.provenance = [...provenance];
|
|
420
|
-
if (result.argument) {
|
|
421
|
-
result.argument.provenance = [...provenance];
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} else {
|
|
425
|
-
// Create coefficient * term
|
|
426
|
-
const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
|
|
427
|
-
|
|
428
|
-
// CRITICAL: Set provenance on the coefficient node
|
|
429
|
-
if (provenance && provenance.length > 0) {
|
|
430
|
-
coeffNode.provenance = [...provenance];
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
|
|
434
|
-
|
|
435
|
-
// CRITICAL: Set provenance on the multiplication node
|
|
436
|
-
if (provenance && provenance.length > 0) {
|
|
437
|
-
multiplicationNode.provenance = [...provenance];
|
|
438
|
-
// Ensure left (coefficient) and right (variable/power) have provenance
|
|
439
|
-
if (multiplicationNode.left) {
|
|
440
|
-
multiplicationNode.left.provenance = [...provenance];
|
|
441
|
-
}
|
|
442
|
-
if (multiplicationNode.right) {
|
|
443
|
-
multiplicationNode.right.provenance = [...provenance];
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Wrap in unary minus if coefficient is negative
|
|
448
|
-
if (coefficient < 0) {
|
|
449
|
-
const unaryAST = {
|
|
450
|
-
type: 'OperatorNode',
|
|
451
|
-
op: '-',
|
|
452
|
-
fn: 'unaryMinus',
|
|
453
|
-
args: [multiplicationNode.toMathJSNode()],
|
|
454
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
455
|
-
};
|
|
456
|
-
result = new omdUnaryExpressionNode(unaryAST);
|
|
457
|
-
result.setFontSize(fontSize);
|
|
458
|
-
result.initialize();
|
|
459
|
-
|
|
460
|
-
// CRITICAL: Set provenance on negative wrapper and all its components
|
|
461
|
-
if (provenance && provenance.length > 0) {
|
|
462
|
-
result.provenance = [...provenance];
|
|
463
|
-
if (result.argument) {
|
|
464
|
-
result.argument.provenance = [...provenance];
|
|
465
|
-
// Also set on the multiplication components within the unary minus
|
|
466
|
-
if (result.argument.left) {
|
|
467
|
-
result.argument.left.provenance = [...provenance];
|
|
468
|
-
}
|
|
469
|
-
if (result.argument.right) {
|
|
470
|
-
result.argument.right.provenance = [...provenance];
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
} else {
|
|
475
|
-
result = multiplicationNode;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Preserve provenance from the original terms that were combined
|
|
480
|
-
if (provenance && provenance.length > 0) {
|
|
481
|
-
provenance.forEach(id => {
|
|
482
|
-
if (!result.provenance.includes(id)) {
|
|
483
|
-
result.provenance.push(id);
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return result;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Helper to get node class by name (needed for dynamic instantiation).
|
|
493
|
-
* @param {string} className - The class name.
|
|
494
|
-
* @returns {Function} The class constructor.
|
|
495
|
-
*/
|
|
496
|
-
static getNodeClass(className) {
|
|
497
|
-
const classMap = {
|
|
498
|
-
'omdVariableNode': omdVariableNode,
|
|
499
|
-
'omdPowerNode': omdPowerNode,
|
|
500
|
-
'omdUnaryExpressionNode': omdUnaryExpressionNode
|
|
501
|
-
};
|
|
502
|
-
return classMap[className];
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* ===== SIMPLIFIED RULE ENGINE CLASS =====
|
|
507
|
-
* Defines the structure and behavior of a simplification rule.
|
|
508
|
-
*/
|
|
509
|
-
static SimplificationRule = class SimplificationRule {
|
|
510
|
-
/**
|
|
511
|
-
* Creates an instance of SimplificationRule.
|
|
512
|
-
* @param {string} name - The name of the rule.
|
|
513
|
-
* @param {function(Object): boolean|Object|null} matchFn - A function that attempts to match the rule against a node. Returns `false`, `null`, `undefined` for no match, `true` for a match with no additional data, or an object with data for a match.
|
|
514
|
-
* @param {function(Object, Object): Object} transformFn - A function that transforms the matched node. Receives the node and data from `matchFn`.
|
|
515
|
-
* @param {function(Object, Object, Object): string} [messageFn=null] - A function that generates a human-readable message for the transformation. Receives the original node, rule data, and the new node.
|
|
516
|
-
* @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
|
|
517
|
-
*/
|
|
518
|
-
constructor(name, matchFn, transformFn, messageFn = null, type = null) {
|
|
519
|
-
this.name = name;
|
|
520
|
-
this.type = type;
|
|
521
|
-
this.match = matchFn;
|
|
522
|
-
this.transform = transformFn;
|
|
523
|
-
this.message = messageFn;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Determines if the rule can be applied to a given node.
|
|
528
|
-
* @param {Object} node - The node to check.
|
|
529
|
-
* @returns {Object|null} An object containing rule data if the rule can be applied, otherwise null.
|
|
530
|
-
*/
|
|
531
|
-
canApply(node) {
|
|
532
|
-
const result = this.match(node);
|
|
533
|
-
return result === false || result === null || result === undefined ? null :
|
|
534
|
-
result === true ? {} : result; // Normalize match results
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* Applies the transformation defined by the rule to a node.
|
|
539
|
-
* @param {Object} node - The node to transform.
|
|
540
|
-
* @param {Object} ruleData - Data returned by the `matchFn`.
|
|
541
|
-
* @param {Object} currentRoot - The current root of the expression tree.
|
|
542
|
-
* @returns {Object} An object indicating success, the new root, and history information.
|
|
543
|
-
*/
|
|
544
|
-
apply(node, ruleData, currentRoot) {
|
|
545
|
-
try {
|
|
546
|
-
// Ensure the transform function is valid
|
|
547
|
-
if (typeof this.transform !== 'function') {
|
|
548
|
-
throw new Error(`Invalid transform function for rule: ${this.name}`);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const newNode = this.transform(node, ruleData, currentRoot);
|
|
552
|
-
if (!newNode) {
|
|
553
|
-
throw new Error(`Transform function for rule '${this.name}' did not return a new node.`);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Collect the IDs of all nodes that are about to be replaced.
|
|
557
|
-
const affectedNodeIds = this._collectAffectedNodeIds(node);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const { success, newRoot } = utils._replaceNodeInTree(node, newNode, currentRoot);
|
|
561
|
-
|
|
562
|
-
if (success) {
|
|
563
|
-
// Use custom affected nodes if provided
|
|
564
|
-
const finalAffectedIds = newNode.__affectedNodeIds || affectedNodeIds;
|
|
565
|
-
const finalResultNodeId = newNode.__resultNodeId || newNode.id;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const historyEntry = {
|
|
569
|
-
name: this.name,
|
|
570
|
-
affectedNodes: finalAffectedIds,
|
|
571
|
-
resultNodeId: finalResultNodeId,
|
|
572
|
-
resultProvSources: newNode.provenance,
|
|
573
|
-
message: this.message(node, ruleData, newNode)
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return { success: true, newRoot, historyEntry };
|
|
579
|
-
} else {
|
|
580
|
-
return { success: false, newRoot: currentRoot };
|
|
581
|
-
}
|
|
582
|
-
} catch (error) {
|
|
583
|
-
console.error(`Error applying rule '${this.name}':`, error);
|
|
584
|
-
return { success: false, newRoot: currentRoot };
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Collects IDs of nodes directly affected by this rule application.
|
|
590
|
-
* For rules that create new nodes with provenance, use that to determine affected nodes
|
|
591
|
-
* Otherwise, use the direct node ID.
|
|
592
|
-
* @param {Object} node - The original node that was transformed.
|
|
593
|
-
* @returns {Array<string>} Array of node IDs that were affected.
|
|
594
|
-
*/
|
|
595
|
-
_collectAffectedNodeIds(node) {
|
|
596
|
-
// For simplification rules that set provenance, use that to determine affected nodes
|
|
597
|
-
if (node && node.provenance && node.provenance.length > 0) {
|
|
598
|
-
// Return the provenance IDs (excluding the original node ID which represents the transformation result)
|
|
599
|
-
return node.provenance.filter(id => id !== node.id);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Fallback: return the direct node ID
|
|
603
|
-
return node && node.id ? [node.id] : [];
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
/**
|
|
607
|
-
* Generates a human-readable message for this rule application.
|
|
608
|
-
* @param {Object} originalNode - The original node before transformation.
|
|
609
|
-
* @param {Object} ruleData - Data from the match function.
|
|
610
|
-
* @param {Object} newNode - The new node after transformation.
|
|
611
|
-
* @returns {string} A human-readable message describing what happened.
|
|
612
|
-
*/
|
|
613
|
-
_generateMessage(originalNode, ruleData, newNode) {
|
|
614
|
-
if (this.message) {
|
|
615
|
-
try {
|
|
616
|
-
return this.message(originalNode, ruleData, newNode);
|
|
617
|
-
} catch (error) {
|
|
618
|
-
console.warn(`Error generating message for rule ${this.name}:`, error);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Default message if no custom message function is provided
|
|
623
|
-
const originalValue = originalNode.toString ? originalNode.toString() : 'expression';
|
|
624
|
-
const newValue = newNode.toString ? newNode.toString() : 'expression';
|
|
625
|
-
return `Applied ${this.name}: "${originalValue}" → "${newValue}"`;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* ===== HELPER FUNCTIONS FOR COMMON RULE PATTERNS =====
|
|
631
|
-
* These functions create common types of simplification rules.
|
|
632
|
-
*/
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* Creates a rational number node, simplifying it by dividing by the greatest common divisor.
|
|
636
|
-
* Handles negative numerators and denominators to ensure a positive denominator.
|
|
637
|
-
* @param {number} numerator - The numerator of the rational number.
|
|
638
|
-
* @param {number} denominator - The denominator of the rational number.
|
|
639
|
-
* @param {number} fontSize - The font size for the resulting node.
|
|
640
|
-
* @returns {Object} A new rational node or a constant node if the denominator simplifies to 1.
|
|
641
|
-
*/
|
|
642
|
-
static rational(numerator, denominator, fontSize) {
|
|
643
|
-
const gcd = utils.gcd(Math.abs(numerator), Math.abs(denominator));
|
|
644
|
-
const simpleNum = numerator / gcd;
|
|
645
|
-
const simpleDen = denominator / gcd;
|
|
646
|
-
|
|
647
|
-
// Ensure denominator is positive
|
|
648
|
-
if (simpleDen < 0) {
|
|
649
|
-
return SimplificationEngine.#createRationalSafe(-simpleNum, -simpleDen, fontSize);
|
|
650
|
-
}
|
|
651
|
-
return SimplificationEngine.#createRationalSafe(simpleNum, simpleDen, fontSize);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* A safe internal function to create a rational node, handling simplification and negative signs.
|
|
656
|
-
* @param {number} numerator - The numerator.
|
|
657
|
-
* @param {number} denominator - The denominator.
|
|
658
|
-
* @param {number} fontSize - The font size.
|
|
659
|
-
* @returns {Object} The created omdRationalNode or omdConstantNode.
|
|
660
|
-
*/
|
|
661
|
-
static #createRationalSafe(numerator, denominator, fontSize) {
|
|
662
|
-
if (denominator === 1) {
|
|
663
|
-
const result = SimplificationEngine.createConstant(numerator, fontSize);
|
|
664
|
-
return result;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const ast = {
|
|
668
|
-
type: 'OperatorNode',
|
|
669
|
-
op: '/',
|
|
670
|
-
fn: 'divide',
|
|
671
|
-
args: [
|
|
672
|
-
{ type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
|
|
673
|
-
{ type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
|
|
674
|
-
],
|
|
675
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
676
|
-
};
|
|
677
|
-
|
|
678
|
-
let node = new omdRationalNode(ast);
|
|
679
|
-
|
|
680
|
-
// Wrap in unary minus if the numerator is negative
|
|
681
|
-
if (numerator < 0) {
|
|
682
|
-
const unaryAST = {
|
|
683
|
-
type: 'OperatorNode',
|
|
684
|
-
op: '-',
|
|
685
|
-
fn: 'unaryMinus',
|
|
686
|
-
args: [node.toMathJSNode()],
|
|
687
|
-
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
688
|
-
};
|
|
689
|
-
node = new omdUnaryExpressionNode(unaryAST);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
node.setFontSize(fontSize);
|
|
693
|
-
node.initialize();
|
|
694
|
-
|
|
695
|
-
return node;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/**
|
|
699
|
-
* Combines two constant nodes based on a given operator.
|
|
700
|
-
* @param {Object} left - The left constant node.
|
|
701
|
-
* @param {Object} right - The right constant node.
|
|
702
|
-
* @param {string} operator - The operation to perform (add, subtract, multiply, divide).
|
|
703
|
-
* @param {number} fontSize - The font size for the resulting node.
|
|
704
|
-
* @returns {Object|null} A new constant or rational node representing the combined value, or null if division by zero occurs.
|
|
705
|
-
*/
|
|
706
|
-
static combineConstants(left, right, operator, fontSize) {
|
|
707
|
-
const leftVal = left.getValue();
|
|
708
|
-
const rightVal = right.getValue();
|
|
709
|
-
|
|
710
|
-
let resultNode = null;
|
|
711
|
-
|
|
712
|
-
switch(operator) {
|
|
713
|
-
case 'add':
|
|
714
|
-
resultNode = SimplificationEngine.createConstant(leftVal + rightVal, fontSize, left, right);
|
|
715
|
-
break;
|
|
716
|
-
case 'subtract':
|
|
717
|
-
resultNode = SimplificationEngine.createConstant(leftVal - rightVal, fontSize, left, right);
|
|
718
|
-
break;
|
|
719
|
-
case 'multiply':
|
|
720
|
-
resultNode = SimplificationEngine.createConstant(leftVal * rightVal, fontSize, left, right);
|
|
721
|
-
break;
|
|
722
|
-
case 'divide':
|
|
723
|
-
if (rightVal === 0) return null; // Avoid division by zero
|
|
724
|
-
resultNode = SimplificationEngine.rational(leftVal, rightVal, fontSize, left, right);
|
|
725
|
-
break;
|
|
726
|
-
default:
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// The automatic provenance tracking is now handled by the create methods above
|
|
731
|
-
return resultNode;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* ===== RULE FACTORY FUNCTIONS =====
|
|
736
|
-
* Functions to create instances of SimplificationRule for common simplification patterns.
|
|
737
|
-
*/
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Creates a new SimplificationRule instance.
|
|
741
|
-
* @param {string} name - The name of the rule.
|
|
742
|
-
* @param {function(Object): boolean|Object|null} matchFn - The match function for the rule.
|
|
743
|
-
* @param {function(Object, Object): Object} transformFn - The transform function for the rule.
|
|
744
|
-
* @param {function(Object, Object, Object): string} [messageFn=null] - Optional function to generate human-readable messages.
|
|
745
|
-
* @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
|
|
746
|
-
* @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule instance.
|
|
747
|
-
*/
|
|
748
|
-
static createRule(name, matchFn, transformFn, messageFn = null, type = null) {
|
|
749
|
-
return new SimplificationEngine.SimplificationRule(name, matchFn, transformFn, messageFn, type);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Creates a rule for folding (simplifying) binary operations with two constant operands.
|
|
754
|
-
* @param {string} name - The name of the constant fold rule.
|
|
755
|
-
* @param {string} operator - The operator of the binary expression (e.g., 'add', 'subtract').
|
|
756
|
-
* @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for constant folding.
|
|
757
|
-
*/
|
|
758
|
-
static createConstantFoldRule(name, operator) {
|
|
759
|
-
return SimplificationEngine.createRule(name,
|
|
760
|
-
// Match: binary op with two constant or rational constant operands
|
|
761
|
-
(node) => {
|
|
762
|
-
if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
|
|
763
|
-
return node.left.isConstant() && node.right.isConstant();
|
|
764
|
-
},
|
|
765
|
-
// Transform: combine the constants
|
|
766
|
-
(node) => {
|
|
767
|
-
const newNode = SimplificationEngine.combineConstants(node.left, node.right, operator, node.getFontSize());
|
|
768
|
-
// The combineConstants function now correctly handles its own provenance.
|
|
769
|
-
// No need to manually add the parent node's id.
|
|
770
|
-
return newNode;
|
|
771
|
-
},
|
|
772
|
-
// Message: describe the constant folding operation
|
|
773
|
-
(originalNode, ruleData, newNode) => {
|
|
774
|
-
const leftVal = originalNode.left.getValue();
|
|
775
|
-
const rightVal = originalNode.right.getValue();
|
|
776
|
-
const result = newNode.getValue ? newNode.getValue() : newNode.toString();
|
|
777
|
-
|
|
778
|
-
const operatorSymbols = {
|
|
779
|
-
'add': '+',
|
|
780
|
-
'subtract': '-',
|
|
781
|
-
'multiply': getMultiplicationSymbol(),
|
|
782
|
-
'divide': '÷'
|
|
783
|
-
};
|
|
784
|
-
|
|
785
|
-
const symbol = operatorSymbols[operator] || operator;
|
|
786
|
-
return `Combined constants: ${leftVal} ${symbol} ${rightVal} = ${result}`;
|
|
787
|
-
}
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* Creates an identity rule (e.g., x + 0 → x, x * 1 → x).
|
|
793
|
-
* @param {string} name - The name of the identity rule.
|
|
794
|
-
* @param {string} operator - The operator of the binary expression.
|
|
795
|
-
* @param {number} identityValue - The identity value for the operation (e.g., 0 for addition, 1 for multiplication).
|
|
796
|
-
* @param {'left'|'right'|'both'} [side='both'] - Specifies which side the identity value should be on ('left', 'right', or 'both').
|
|
797
|
-
* @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for the identity operation.
|
|
798
|
-
*/
|
|
799
|
-
static createIdentityRule(name, operator, identityValue, side = 'both') {
|
|
800
|
-
return SimplificationEngine.createRule(name,
|
|
801
|
-
// Match: binary op with one constant operand being the identity value
|
|
802
|
-
(node) => {
|
|
803
|
-
if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
|
|
804
|
-
|
|
805
|
-
const constOperandInfo = SimplificationEngine.hasConstantOperand(node);
|
|
806
|
-
if (!constOperandInfo || !constOperandInfo.constant.isConstant() || constOperandInfo.constant.getValue() !== identityValue) {
|
|
807
|
-
return false;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (side === 'left' && node.right.isConstant()) return false;
|
|
811
|
-
if (side === 'right' && node.left.isConstant()) return false;
|
|
812
|
-
|
|
813
|
-
return { other: constOperandInfo.other };
|
|
814
|
-
},
|
|
815
|
-
// Transform: return the other operand
|
|
816
|
-
(node, data) => {
|
|
817
|
-
const newNode = data.other.clone();
|
|
818
|
-
|
|
819
|
-
// Use manual provenance tracking for substitution
|
|
820
|
-
applyProvenance(newNode, node);
|
|
821
|
-
|
|
822
|
-
// Add a flag to tell the highlighting engine that this was a simple identity transform.
|
|
823
|
-
newNode.__isSimpleIdentity = true;
|
|
824
|
-
|
|
825
|
-
return newNode;
|
|
826
|
-
},
|
|
827
|
-
// Message: describe the identity operation
|
|
828
|
-
(originalNode, ruleData, newNode) => {
|
|
829
|
-
const { other } = ruleData;
|
|
830
|
-
const otherStr = other.toString ? other.toString() : 'expression';
|
|
831
|
-
|
|
832
|
-
const operatorNames = {
|
|
833
|
-
'add': 'addition',
|
|
834
|
-
'subtract': 'subtraction',
|
|
835
|
-
'multiply': 'multiplication',
|
|
836
|
-
'divide': 'division'
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
const operatorSymbols = {
|
|
840
|
-
'add': '+',
|
|
841
|
-
'subtract': '-',
|
|
842
|
-
'multiply': getMultiplicationSymbol(),
|
|
843
|
-
'divide': '÷'
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
const opName = operatorNames[operator] || operator;
|
|
847
|
-
const symbol = operatorSymbols[operator] || operator;
|
|
848
|
-
|
|
849
|
-
const isLeftIdentity = originalNode.left.isConstant() && originalNode.left.getValue() === identityValue;
|
|
850
|
-
const position = isLeftIdentity ? 'left' : 'right';
|
|
851
|
-
|
|
852
|
-
if (operator === 'add' && identityValue === 0) {
|
|
853
|
-
return `Applied additive identity: "${otherStr} + 0" simplified to "${otherStr}" (adding 0 doesn't change the value)`;
|
|
854
|
-
} else if (operator === 'subtract' && identityValue === 0) {
|
|
855
|
-
return `Applied subtraction identity: "${otherStr} - 0" simplified to "${otherStr}" (subtracting 0 doesn't change the value)`;
|
|
856
|
-
} else if (operator === 'multiply' && identityValue === 1) {
|
|
857
|
-
return `Applied multiplicative identity: "${otherStr} ${getMultiplicationSymbol()} 1" simplified to "${otherStr}" (multiplying by 1 doesn't change the value)`;
|
|
858
|
-
} else if (operator === 'divide' && identityValue === 1) {
|
|
859
|
-
return `Applied division identity: "${otherStr} ÷ 1" simplified to "${otherStr}" (dividing by 1 doesn't change the value)`;
|
|
860
|
-
} else {
|
|
861
|
-
return `Applied ${opName} identity: removed ${identityValue} from ${position} side, leaving "${otherStr}"`;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* Creates a rule for simplifying zero multiplication (e.g., x * 0 = 0).
|
|
869
|
-
*/
|
|
870
|
-
static createZeroMultiplicationRule() {
|
|
871
|
-
return SimplificationEngine.createRule("Zero Multiplication",
|
|
872
|
-
(node) => {
|
|
873
|
-
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
874
|
-
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
875
|
-
return constOp && constOp.constant.getValue() === 0;
|
|
876
|
-
},
|
|
877
|
-
(node) => {
|
|
878
|
-
const newNode = SimplificationEngine.createConstant(0, node.getFontSize(), node);
|
|
879
|
-
return newNode;
|
|
880
|
-
},
|
|
881
|
-
(originalNode, ruleData, newNode) => {
|
|
882
|
-
const constOp = SimplificationEngine.hasConstantOperand(originalNode);
|
|
883
|
-
const otherOperand = constOp.other;
|
|
884
|
-
return `Applied zero multiplication: ${utils.nodeToString(otherOperand)} ${getMultiplicationSymbol()} 0 = 0`;
|
|
885
|
-
}
|
|
886
|
-
);
|
|
887
|
-
}
|
|
1
|
+
import { omdRationalNode } from "../nodes/omdRationalNode.js";
|
|
2
|
+
import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
|
|
3
|
+
import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
|
|
4
|
+
import { omdVariableNode } from "../nodes/omdVariableNode.js";
|
|
5
|
+
import { omdPowerNode } from "../nodes/omdPowerNode.js";
|
|
6
|
+
import { omdConstantNode } from "../nodes/omdConstantNode.js";
|
|
7
|
+
import { createConstantNode, applyProvenance } from './simplificationUtils.js';
|
|
8
|
+
import * as utils from './simplificationUtils.js';
|
|
9
|
+
import { useImplicitMultiplication, getMultiplicationSymbol } from "../config/omdConfigManager.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @class SimplificationEngine
|
|
13
|
+
* @classdesc Provides a collection of static methods for creating, matching, and transforming
|
|
14
|
+
* mathematical expression nodes for simplification purposes.
|
|
15
|
+
* This class serves as the core logic for applying simplification rules within the OMD system.
|
|
16
|
+
*/
|
|
17
|
+
export class SimplificationEngine {
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ===== SIMPLIFIED NODE CREATION HELPERS =====
|
|
21
|
+
* These functions provide an easy way to create various types of AST nodes
|
|
22
|
+
* without directly dealing with the complexities of the Math.js AST structure.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new constant node.
|
|
27
|
+
* @param {number} value - The numeric value of the constant.
|
|
28
|
+
* @param {number} [fontSize=16] - The font size for the node.
|
|
29
|
+
* @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
|
|
30
|
+
* @returns {Object} A new constant node.
|
|
31
|
+
*/
|
|
32
|
+
static createConstant(value, fontSize = 16, ...sourceNodes) {
|
|
33
|
+
const node = createConstantNode(value, fontSize);
|
|
34
|
+
|
|
35
|
+
// Apply manual provenance tracking if source nodes provided
|
|
36
|
+
if (sourceNodes.length > 0) {
|
|
37
|
+
applyProvenance(node, ...sourceNodes);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new binary expression node.
|
|
45
|
+
* @param {Object} left - The left-hand side operand node.
|
|
46
|
+
* @param {string} operator - The operator (e.g., '+', '-', '*', '/').
|
|
47
|
+
* @param {Object} right - The right-hand side operand node.
|
|
48
|
+
* @param {number} [fontSize=16] - The font size for the node.
|
|
49
|
+
* @param {Array|null} [provenance=null] - The provenance array for the node (deprecated - use ...sourceNodes instead).
|
|
50
|
+
* @param {omdNode|null} [operatorNode=null] - The original operator node for provenance (deprecated - use ...sourceNodes instead).
|
|
51
|
+
* @param {...omdNode} sourceNodes - Additional source nodes for automatic provenance tracking.
|
|
52
|
+
* @returns {Object} A new binary expression node.
|
|
53
|
+
* @throws {Error} If an unknown operator is provided.
|
|
54
|
+
*/
|
|
55
|
+
static createBinaryOp(left, operator, right, fontSize = 16, provenance = null, operatorNode = null, ...sourceNodes) {
|
|
56
|
+
const opMap = {
|
|
57
|
+
'add': { op: '+', fn: 'add' },
|
|
58
|
+
'subtract': { op: '−', fn: 'subtract' },
|
|
59
|
+
'multiply': { op: getMultiplicationSymbol(), fn: 'multiply' },
|
|
60
|
+
'divide': { op: '÷', fn: 'divide' }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const opInfo = opMap[operator];
|
|
64
|
+
if (!opInfo) throw new Error(`Unknown operator: ${operator}`);
|
|
65
|
+
|
|
66
|
+
// For multiplication, reorder operands if needed (e.g., x*2 -> 2*x)
|
|
67
|
+
let leftOperand = left;
|
|
68
|
+
let rightOperand = right;
|
|
69
|
+
|
|
70
|
+
if (operator === 'multiply') {
|
|
71
|
+
const leftIsConstant = (left instanceof omdConstantNode && left.isConstant());
|
|
72
|
+
const rightIsConstant = (right instanceof omdConstantNode && right.isConstant());
|
|
73
|
+
|
|
74
|
+
// If left is non-constant and right is constant, swap them
|
|
75
|
+
if (!leftIsConstant && rightIsConstant) {
|
|
76
|
+
leftOperand = right;
|
|
77
|
+
rightOperand = left;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ast = {
|
|
82
|
+
type: 'OperatorNode',
|
|
83
|
+
op: opInfo.op,
|
|
84
|
+
fn: opInfo.fn,
|
|
85
|
+
args: [leftOperand.toMathJSNode(), rightOperand.toMathJSNode()],
|
|
86
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Pass operator provenance into the constructor via the AST
|
|
90
|
+
if (operatorNode) {
|
|
91
|
+
ast.operatorProvenance = [operatorNode.id];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const node = new omdBinaryExpressionNode(ast);
|
|
95
|
+
node.setFontSize(fontSize);
|
|
96
|
+
|
|
97
|
+
// Use manual provenance tracking
|
|
98
|
+
if (provenance) {
|
|
99
|
+
// Legacy support: apply manual provenance
|
|
100
|
+
node.provenance = provenance;
|
|
101
|
+
} else {
|
|
102
|
+
// Manual provenance tracking for binary operations
|
|
103
|
+
const allSources = [leftOperand, rightOperand];
|
|
104
|
+
if (operatorNode) allSources.push(operatorNode);
|
|
105
|
+
allSources.push(...sourceNodes);
|
|
106
|
+
applyProvenance(node, ...allSources);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
node.initialize();
|
|
110
|
+
return node;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates a multiplication expression where the left operand is a constant value.
|
|
115
|
+
* @param {Object} leftConstantNode - The constant node for the left operand.
|
|
116
|
+
* @param {Object} rightNode - The node for the right operand.
|
|
117
|
+
* @param {number} [fontSize=16] - The font size for the node.
|
|
118
|
+
* @param {Array|null} [provenance=null] - The provenance array for the node.
|
|
119
|
+
* @returns {Object} A new binary expression node representing multiplication.
|
|
120
|
+
*/
|
|
121
|
+
static createMultiplication(leftConstantNode, rightNode, fontSize = 16, provenance = null) {
|
|
122
|
+
// Clone the constant to create a new instance for the new expression,
|
|
123
|
+
// but ensure its provenance points back to the original constant.
|
|
124
|
+
const newLeftConstant = leftConstantNode.clone();
|
|
125
|
+
newLeftConstant.provenance = [leftConstantNode.id];
|
|
126
|
+
|
|
127
|
+
const rightClone = rightNode.clone(); // Clone the other node to ensure we don't modify the original
|
|
128
|
+
|
|
129
|
+
// Safely get the operator node for provenance, handling cases where parent might be null
|
|
130
|
+
const operatorNode = leftConstantNode.parent?.op || null;
|
|
131
|
+
|
|
132
|
+
return SimplificationEngine.createBinaryOp(newLeftConstant, 'multiply', rightClone, fontSize, provenance, operatorNode);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a rational node (fraction).
|
|
137
|
+
* @param {number} numerator - The numerator value.
|
|
138
|
+
* @param {number} denominator - The denominator value.
|
|
139
|
+
* @param {number} [fontSize=16] - The font size for the node.
|
|
140
|
+
* @param {...omdNode} sourceNodes - Source nodes for automatic provenance tracking.
|
|
141
|
+
* @returns {Object} A new rational node, or a constant node if the denominator is 1.
|
|
142
|
+
*/
|
|
143
|
+
static createRational(numerator, denominator, fontSize = 16, ...sourceNodes) {
|
|
144
|
+
if (denominator === 1) {
|
|
145
|
+
return SimplificationEngine.createConstant(numerator, fontSize, ...sourceNodes);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ast = {
|
|
149
|
+
type: 'OperatorNode', op: '/', fn: 'divide',
|
|
150
|
+
args: [
|
|
151
|
+
{ type: 'ConstantNode', value: Math.abs(numerator) },
|
|
152
|
+
{ type: 'ConstantNode', value: denominator }
|
|
153
|
+
],
|
|
154
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let node = new omdRationalNode(ast);
|
|
158
|
+
|
|
159
|
+
// Handle negative fractions by wrapping in a unary minus node
|
|
160
|
+
if (numerator < 0) {
|
|
161
|
+
const unaryAST = {
|
|
162
|
+
type: 'OperatorNode', op: '-', fn: 'unaryMinus',
|
|
163
|
+
args: [node.toMathJSNode()],
|
|
164
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
165
|
+
};
|
|
166
|
+
node = new omdUnaryExpressionNode(unaryAST);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
node.setFontSize(fontSize);
|
|
170
|
+
|
|
171
|
+
// Apply manual provenance tracking if source nodes provided
|
|
172
|
+
if (sourceNodes.length > 0) {
|
|
173
|
+
applyProvenance(node, ...sourceNodes);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
node.initialize();
|
|
177
|
+
return node;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* ===== SIMPLIFIED PATTERN MATCHING HELPERS =====
|
|
182
|
+
* These functions provide convenient ways to check common patterns in AST nodes.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Checks if a node is of a specific type.
|
|
187
|
+
* @param {Object} node - The node to check.
|
|
188
|
+
* @param {string} typeName - The name of the constructor type (e.g., 'omdConstantNode').
|
|
189
|
+
* @returns {boolean} True if the node matches the type, false otherwise.
|
|
190
|
+
*/
|
|
191
|
+
static isType(node, typeName) {
|
|
192
|
+
if (!node) return false;
|
|
193
|
+
return node.type === typeName;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Checks if a node is a binary operation, optionally with a specific operator.
|
|
198
|
+
* @param {Object} node - The node to check.
|
|
199
|
+
* @param {string} [operator=null] - The specific operator to check for (e.g., 'add', 'multiply').
|
|
200
|
+
* @returns {boolean} True if the node is a binary operation (and matches operator if provided), false otherwise.
|
|
201
|
+
*/
|
|
202
|
+
static isBinaryOp(node, operator = null) {
|
|
203
|
+
if (!SimplificationEngine.isType(node, 'omdBinaryExpressionNode')) return false;
|
|
204
|
+
|
|
205
|
+
if (!operator) return true;
|
|
206
|
+
|
|
207
|
+
// Handle both string operations and object operations (from Math.js AST)
|
|
208
|
+
let nodeOp = node.operation;
|
|
209
|
+
if (typeof nodeOp === 'object' && nodeOp.name) {
|
|
210
|
+
nodeOp = nodeOp.name;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return nodeOp === operator;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Checks if a node is a constant, optionally with a specific value.
|
|
218
|
+
* @param {Object} node - The node to check.
|
|
219
|
+
* @param {number} [value=null] - The specific constant value to check for.
|
|
220
|
+
* @returns {boolean} True if the node is a constant (and matches value if provided), false otherwise.
|
|
221
|
+
*/
|
|
222
|
+
static isConstantValue(node, value = null) {
|
|
223
|
+
if (!node.isConstant()) return false;
|
|
224
|
+
return value !== null ? node.getValue() === value : true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Checks if a binary operation node has a constant operand and returns it along with the other operand.
|
|
229
|
+
* @param {Object} node - The binary expression node to check.
|
|
230
|
+
* @returns {Object|null} An object containing the constant and other operand, or null if no constant operand is found.
|
|
231
|
+
*/
|
|
232
|
+
static hasConstantOperand(node) {
|
|
233
|
+
if (!SimplificationEngine.isBinaryOp(node)) return null;
|
|
234
|
+
|
|
235
|
+
if (node.left.isConstant()) return { constant: node.left, other: node.right };
|
|
236
|
+
if (node.right.isConstant()) return { constant: node.right, other: node.left };
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Unwraps a parenthesis node, returning its inner expression. If the node is not a parenthesis node, it returns the node itself.
|
|
242
|
+
* @param {Object} node - The node to unwrap.
|
|
243
|
+
* @returns {Object} The inner expression if the node is a parenthesis node, otherwise the original node.
|
|
244
|
+
*/
|
|
245
|
+
static unwrapParentheses(node) {
|
|
246
|
+
return SimplificationEngine.isType(node, 'omdParenthesisNode') ? node.expression : node;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* ===== MONOMIAL AND LIKE TERMS HELPERS =====
|
|
251
|
+
* These functions help identify and work with monomials and like terms.
|
|
252
|
+
*/
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Checks if a node represents a monomial (a term of the form coefficient * variable^power).
|
|
256
|
+
* Examples: x, 2x, 3y, -5z, x^2, 2x^3
|
|
257
|
+
* @param {Object} node - The node to check.
|
|
258
|
+
* @returns {Object|null} An object with coefficient, variable, and power if it's a monomial, null otherwise.
|
|
259
|
+
*/
|
|
260
|
+
static isMonomial(node) {
|
|
261
|
+
let coefficientMultiplier = 1;
|
|
262
|
+
|
|
263
|
+
// Unwrap parentheses first, as they can contain a unary minus
|
|
264
|
+
node = SimplificationEngine.unwrapParentheses(node);
|
|
265
|
+
|
|
266
|
+
// Handle any number of nested unary minuses by repeatedly unwrapping them
|
|
267
|
+
// and flipping the coefficient multiplier.
|
|
268
|
+
while (SimplificationEngine.isType(node, 'omdUnaryExpressionNode') && node.operation === 'unaryMinus') {
|
|
269
|
+
node = node.argument;
|
|
270
|
+
coefficientMultiplier *= -1;
|
|
271
|
+
// The argument of the unary minus could also be in parentheses
|
|
272
|
+
node = SimplificationEngine.unwrapParentheses(node);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Case 1: Just a variable (e.g., x)
|
|
276
|
+
if (SimplificationEngine.isType(node, 'omdVariableNode')) {
|
|
277
|
+
return {
|
|
278
|
+
coefficient: 1 * coefficientMultiplier,
|
|
279
|
+
variable: node.name,
|
|
280
|
+
power: 1,
|
|
281
|
+
variableNode: node
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Case 2: Constant * Variable (e.g., 2x, 3y)
|
|
286
|
+
if (SimplificationEngine.isBinaryOp(node, 'multiply')) {
|
|
287
|
+
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
288
|
+
if (constOp && SimplificationEngine.isType(constOp.other, 'omdVariableNode')) {
|
|
289
|
+
return {
|
|
290
|
+
coefficient: constOp.constant.getValue() * coefficientMultiplier,
|
|
291
|
+
variable: constOp.other.name,
|
|
292
|
+
power: 1,
|
|
293
|
+
variableNode: constOp.other,
|
|
294
|
+
coefficientNode: constOp.constant
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Case 3: Variable * Constant (e.g., x*2, y*3) - less common but possible
|
|
299
|
+
if (constOp && SimplificationEngine.isType(constOp.constant, 'omdVariableNode') && constOp.other.isConstant()) {
|
|
300
|
+
return {
|
|
301
|
+
coefficient: constOp.other.getValue() * coefficientMultiplier,
|
|
302
|
+
variable: constOp.constant.name,
|
|
303
|
+
power: 1,
|
|
304
|
+
variableNode: constOp.constant,
|
|
305
|
+
coefficientNode: constOp.other
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Case 4: Constant * Power (e.g., 2*x^3)
|
|
310
|
+
const constPowerOp = SimplificationEngine.hasConstantOperand(node);
|
|
311
|
+
if (constPowerOp && SimplificationEngine.isType(constPowerOp.other, 'omdPowerNode')) {
|
|
312
|
+
const powerNode = constPowerOp.other;
|
|
313
|
+
if (SimplificationEngine.isType(powerNode.base, 'omdVariableNode') && powerNode.exponent.isConstant()) {
|
|
314
|
+
return {
|
|
315
|
+
coefficient: constPowerOp.constant.getValue() * coefficientMultiplier,
|
|
316
|
+
variable: powerNode.base.name,
|
|
317
|
+
power: powerNode.exponent.getValue(),
|
|
318
|
+
variableNode: powerNode.base,
|
|
319
|
+
coefficientNode: constPowerOp.constant,
|
|
320
|
+
powerNode: powerNode
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Case 5: Just a power (e.g., x^2)
|
|
327
|
+
if (SimplificationEngine.isType(node, 'omdPowerNode')) {
|
|
328
|
+
if (SimplificationEngine.isType(node.base, 'omdVariableNode') && node.exponent.isConstant()) {
|
|
329
|
+
return {
|
|
330
|
+
coefficient: 1 * coefficientMultiplier,
|
|
331
|
+
variable: node.base.name,
|
|
332
|
+
power: node.exponent.getValue(),
|
|
333
|
+
variableNode: node.base,
|
|
334
|
+
powerNode: node
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Checks if two monomials are like terms (same variable and power).
|
|
344
|
+
* @param {Object} monomial1 - First monomial info from isMonomial().
|
|
345
|
+
* @param {Object} monomial2 - Second monomial info from isMonomial().
|
|
346
|
+
* @returns {boolean} True if they are like terms, false otherwise.
|
|
347
|
+
*/
|
|
348
|
+
static areLikeTerms(monomial1, monomial2) {
|
|
349
|
+
return monomial1.variable === monomial2.variable && monomial1.power === monomial2.power;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Creates a monomial node from coefficient, variable, and power.
|
|
354
|
+
* @param {number} coefficient - The coefficient of the monomial.
|
|
355
|
+
* @param {string} variable - The variable name.
|
|
356
|
+
* @param {number} power - The power of the variable.
|
|
357
|
+
* @param {number} fontSize - The font size for the node.
|
|
358
|
+
* @param {Array} [provenance=[]] - The provenance array to preserve lineage.
|
|
359
|
+
* @returns {Object} A new monomial node.
|
|
360
|
+
*/
|
|
361
|
+
static createMonomial(coefficient, variable, power, fontSize, provenance = []) {
|
|
362
|
+
// Create variable node
|
|
363
|
+
const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
|
|
364
|
+
const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
|
|
365
|
+
variableNode.setFontSize(fontSize);
|
|
366
|
+
variableNode.initialize();
|
|
367
|
+
|
|
368
|
+
// CRITICAL: Set provenance on the variable node
|
|
369
|
+
if (provenance && provenance.length > 0) {
|
|
370
|
+
variableNode.provenance = [...provenance];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let termNode = variableNode;
|
|
374
|
+
|
|
375
|
+
// Add power if not 1
|
|
376
|
+
if (power !== 1) {
|
|
377
|
+
const powerAST = {
|
|
378
|
+
type: 'OperatorNode',
|
|
379
|
+
op: '^',
|
|
380
|
+
fn: 'pow',
|
|
381
|
+
args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
|
|
382
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
383
|
+
};
|
|
384
|
+
termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
|
|
385
|
+
termNode.setFontSize(fontSize);
|
|
386
|
+
termNode.initialize();
|
|
387
|
+
|
|
388
|
+
// CRITICAL: Set provenance on the power node and its components
|
|
389
|
+
if (provenance && provenance.length > 0) {
|
|
390
|
+
termNode.provenance = [...provenance];
|
|
391
|
+
// Set provenance on the base (variable) and exponent
|
|
392
|
+
if (termNode.base) {
|
|
393
|
+
termNode.base.provenance = [...provenance];
|
|
394
|
+
}
|
|
395
|
+
if (termNode.exponent) {
|
|
396
|
+
termNode.exponent.provenance = [...provenance];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let result;
|
|
402
|
+
if (coefficient === 1) {
|
|
403
|
+
result = termNode;
|
|
404
|
+
} else if (coefficient === -1) {
|
|
405
|
+
// Create unary minus
|
|
406
|
+
const unaryAST = {
|
|
407
|
+
type: 'OperatorNode',
|
|
408
|
+
op: '-',
|
|
409
|
+
fn: 'unaryMinus',
|
|
410
|
+
args: [termNode.toMathJSNode()],
|
|
411
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
412
|
+
};
|
|
413
|
+
result = new omdUnaryExpressionNode(unaryAST);
|
|
414
|
+
result.setFontSize(fontSize);
|
|
415
|
+
result.initialize();
|
|
416
|
+
|
|
417
|
+
// CRITICAL: Set provenance on unary minus and its argument
|
|
418
|
+
if (provenance && provenance.length > 0) {
|
|
419
|
+
result.provenance = [...provenance];
|
|
420
|
+
if (result.argument) {
|
|
421
|
+
result.argument.provenance = [...provenance];
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
// Create coefficient * term
|
|
426
|
+
const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
|
|
427
|
+
|
|
428
|
+
// CRITICAL: Set provenance on the coefficient node
|
|
429
|
+
if (provenance && provenance.length > 0) {
|
|
430
|
+
coeffNode.provenance = [...provenance];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
|
|
434
|
+
|
|
435
|
+
// CRITICAL: Set provenance on the multiplication node
|
|
436
|
+
if (provenance && provenance.length > 0) {
|
|
437
|
+
multiplicationNode.provenance = [...provenance];
|
|
438
|
+
// Ensure left (coefficient) and right (variable/power) have provenance
|
|
439
|
+
if (multiplicationNode.left) {
|
|
440
|
+
multiplicationNode.left.provenance = [...provenance];
|
|
441
|
+
}
|
|
442
|
+
if (multiplicationNode.right) {
|
|
443
|
+
multiplicationNode.right.provenance = [...provenance];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Wrap in unary minus if coefficient is negative
|
|
448
|
+
if (coefficient < 0) {
|
|
449
|
+
const unaryAST = {
|
|
450
|
+
type: 'OperatorNode',
|
|
451
|
+
op: '-',
|
|
452
|
+
fn: 'unaryMinus',
|
|
453
|
+
args: [multiplicationNode.toMathJSNode()],
|
|
454
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
455
|
+
};
|
|
456
|
+
result = new omdUnaryExpressionNode(unaryAST);
|
|
457
|
+
result.setFontSize(fontSize);
|
|
458
|
+
result.initialize();
|
|
459
|
+
|
|
460
|
+
// CRITICAL: Set provenance on negative wrapper and all its components
|
|
461
|
+
if (provenance && provenance.length > 0) {
|
|
462
|
+
result.provenance = [...provenance];
|
|
463
|
+
if (result.argument) {
|
|
464
|
+
result.argument.provenance = [...provenance];
|
|
465
|
+
// Also set on the multiplication components within the unary minus
|
|
466
|
+
if (result.argument.left) {
|
|
467
|
+
result.argument.left.provenance = [...provenance];
|
|
468
|
+
}
|
|
469
|
+
if (result.argument.right) {
|
|
470
|
+
result.argument.right.provenance = [...provenance];
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} else {
|
|
475
|
+
result = multiplicationNode;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Preserve provenance from the original terms that were combined
|
|
480
|
+
if (provenance && provenance.length > 0) {
|
|
481
|
+
provenance.forEach(id => {
|
|
482
|
+
if (!result.provenance.includes(id)) {
|
|
483
|
+
result.provenance.push(id);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Helper to get node class by name (needed for dynamic instantiation).
|
|
493
|
+
* @param {string} className - The class name.
|
|
494
|
+
* @returns {Function} The class constructor.
|
|
495
|
+
*/
|
|
496
|
+
static getNodeClass(className) {
|
|
497
|
+
const classMap = {
|
|
498
|
+
'omdVariableNode': omdVariableNode,
|
|
499
|
+
'omdPowerNode': omdPowerNode,
|
|
500
|
+
'omdUnaryExpressionNode': omdUnaryExpressionNode
|
|
501
|
+
};
|
|
502
|
+
return classMap[className];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* ===== SIMPLIFIED RULE ENGINE CLASS =====
|
|
507
|
+
* Defines the structure and behavior of a simplification rule.
|
|
508
|
+
*/
|
|
509
|
+
static SimplificationRule = class SimplificationRule {
|
|
510
|
+
/**
|
|
511
|
+
* Creates an instance of SimplificationRule.
|
|
512
|
+
* @param {string} name - The name of the rule.
|
|
513
|
+
* @param {function(Object): boolean|Object|null} matchFn - A function that attempts to match the rule against a node. Returns `false`, `null`, `undefined` for no match, `true` for a match with no additional data, or an object with data for a match.
|
|
514
|
+
* @param {function(Object, Object): Object} transformFn - A function that transforms the matched node. Receives the node and data from `matchFn`.
|
|
515
|
+
* @param {function(Object, Object, Object): string} [messageFn=null] - A function that generates a human-readable message for the transformation. Receives the original node, rule data, and the new node.
|
|
516
|
+
* @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
|
|
517
|
+
*/
|
|
518
|
+
constructor(name, matchFn, transformFn, messageFn = null, type = null) {
|
|
519
|
+
this.name = name;
|
|
520
|
+
this.type = type;
|
|
521
|
+
this.match = matchFn;
|
|
522
|
+
this.transform = transformFn;
|
|
523
|
+
this.message = messageFn;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Determines if the rule can be applied to a given node.
|
|
528
|
+
* @param {Object} node - The node to check.
|
|
529
|
+
* @returns {Object|null} An object containing rule data if the rule can be applied, otherwise null.
|
|
530
|
+
*/
|
|
531
|
+
canApply(node) {
|
|
532
|
+
const result = this.match(node);
|
|
533
|
+
return result === false || result === null || result === undefined ? null :
|
|
534
|
+
result === true ? {} : result; // Normalize match results
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Applies the transformation defined by the rule to a node.
|
|
539
|
+
* @param {Object} node - The node to transform.
|
|
540
|
+
* @param {Object} ruleData - Data returned by the `matchFn`.
|
|
541
|
+
* @param {Object} currentRoot - The current root of the expression tree.
|
|
542
|
+
* @returns {Object} An object indicating success, the new root, and history information.
|
|
543
|
+
*/
|
|
544
|
+
apply(node, ruleData, currentRoot) {
|
|
545
|
+
try {
|
|
546
|
+
// Ensure the transform function is valid
|
|
547
|
+
if (typeof this.transform !== 'function') {
|
|
548
|
+
throw new Error(`Invalid transform function for rule: ${this.name}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const newNode = this.transform(node, ruleData, currentRoot);
|
|
552
|
+
if (!newNode) {
|
|
553
|
+
throw new Error(`Transform function for rule '${this.name}' did not return a new node.`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Collect the IDs of all nodes that are about to be replaced.
|
|
557
|
+
const affectedNodeIds = this._collectAffectedNodeIds(node);
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
const { success, newRoot } = utils._replaceNodeInTree(node, newNode, currentRoot);
|
|
561
|
+
|
|
562
|
+
if (success) {
|
|
563
|
+
// Use custom affected nodes if provided
|
|
564
|
+
const finalAffectedIds = newNode.__affectedNodeIds || affectedNodeIds;
|
|
565
|
+
const finalResultNodeId = newNode.__resultNodeId || newNode.id;
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
const historyEntry = {
|
|
569
|
+
name: this.name,
|
|
570
|
+
affectedNodes: finalAffectedIds,
|
|
571
|
+
resultNodeId: finalResultNodeId,
|
|
572
|
+
resultProvSources: newNode.provenance,
|
|
573
|
+
message: this.message(node, ruleData, newNode)
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
return { success: true, newRoot, historyEntry };
|
|
579
|
+
} else {
|
|
580
|
+
return { success: false, newRoot: currentRoot };
|
|
581
|
+
}
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.error(`Error applying rule '${this.name}':`, error);
|
|
584
|
+
return { success: false, newRoot: currentRoot };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Collects IDs of nodes directly affected by this rule application.
|
|
590
|
+
* For rules that create new nodes with provenance, use that to determine affected nodes
|
|
591
|
+
* Otherwise, use the direct node ID.
|
|
592
|
+
* @param {Object} node - The original node that was transformed.
|
|
593
|
+
* @returns {Array<string>} Array of node IDs that were affected.
|
|
594
|
+
*/
|
|
595
|
+
_collectAffectedNodeIds(node) {
|
|
596
|
+
// For simplification rules that set provenance, use that to determine affected nodes
|
|
597
|
+
if (node && node.provenance && node.provenance.length > 0) {
|
|
598
|
+
// Return the provenance IDs (excluding the original node ID which represents the transformation result)
|
|
599
|
+
return node.provenance.filter(id => id !== node.id);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Fallback: return the direct node ID
|
|
603
|
+
return node && node.id ? [node.id] : [];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Generates a human-readable message for this rule application.
|
|
608
|
+
* @param {Object} originalNode - The original node before transformation.
|
|
609
|
+
* @param {Object} ruleData - Data from the match function.
|
|
610
|
+
* @param {Object} newNode - The new node after transformation.
|
|
611
|
+
* @returns {string} A human-readable message describing what happened.
|
|
612
|
+
*/
|
|
613
|
+
_generateMessage(originalNode, ruleData, newNode) {
|
|
614
|
+
if (this.message) {
|
|
615
|
+
try {
|
|
616
|
+
return this.message(originalNode, ruleData, newNode);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.warn(`Error generating message for rule ${this.name}:`, error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Default message if no custom message function is provided
|
|
623
|
+
const originalValue = originalNode.toString ? originalNode.toString() : 'expression';
|
|
624
|
+
const newValue = newNode.toString ? newNode.toString() : 'expression';
|
|
625
|
+
return `Applied ${this.name}: "${originalValue}" → "${newValue}"`;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* ===== HELPER FUNCTIONS FOR COMMON RULE PATTERNS =====
|
|
631
|
+
* These functions create common types of simplification rules.
|
|
632
|
+
*/
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Creates a rational number node, simplifying it by dividing by the greatest common divisor.
|
|
636
|
+
* Handles negative numerators and denominators to ensure a positive denominator.
|
|
637
|
+
* @param {number} numerator - The numerator of the rational number.
|
|
638
|
+
* @param {number} denominator - The denominator of the rational number.
|
|
639
|
+
* @param {number} fontSize - The font size for the resulting node.
|
|
640
|
+
* @returns {Object} A new rational node or a constant node if the denominator simplifies to 1.
|
|
641
|
+
*/
|
|
642
|
+
static rational(numerator, denominator, fontSize) {
|
|
643
|
+
const gcd = utils.gcd(Math.abs(numerator), Math.abs(denominator));
|
|
644
|
+
const simpleNum = numerator / gcd;
|
|
645
|
+
const simpleDen = denominator / gcd;
|
|
646
|
+
|
|
647
|
+
// Ensure denominator is positive
|
|
648
|
+
if (simpleDen < 0) {
|
|
649
|
+
return SimplificationEngine.#createRationalSafe(-simpleNum, -simpleDen, fontSize);
|
|
650
|
+
}
|
|
651
|
+
return SimplificationEngine.#createRationalSafe(simpleNum, simpleDen, fontSize);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* A safe internal function to create a rational node, handling simplification and negative signs.
|
|
656
|
+
* @param {number} numerator - The numerator.
|
|
657
|
+
* @param {number} denominator - The denominator.
|
|
658
|
+
* @param {number} fontSize - The font size.
|
|
659
|
+
* @returns {Object} The created omdRationalNode or omdConstantNode.
|
|
660
|
+
*/
|
|
661
|
+
static #createRationalSafe(numerator, denominator, fontSize) {
|
|
662
|
+
if (denominator === 1) {
|
|
663
|
+
const result = SimplificationEngine.createConstant(numerator, fontSize);
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const ast = {
|
|
668
|
+
type: 'OperatorNode',
|
|
669
|
+
op: '/',
|
|
670
|
+
fn: 'divide',
|
|
671
|
+
args: [
|
|
672
|
+
{ type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
|
|
673
|
+
{ type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
|
|
674
|
+
],
|
|
675
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
let node = new omdRationalNode(ast);
|
|
679
|
+
|
|
680
|
+
// Wrap in unary minus if the numerator is negative
|
|
681
|
+
if (numerator < 0) {
|
|
682
|
+
const unaryAST = {
|
|
683
|
+
type: 'OperatorNode',
|
|
684
|
+
op: '-',
|
|
685
|
+
fn: 'unaryMinus',
|
|
686
|
+
args: [node.toMathJSNode()],
|
|
687
|
+
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
|
|
688
|
+
};
|
|
689
|
+
node = new omdUnaryExpressionNode(unaryAST);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
node.setFontSize(fontSize);
|
|
693
|
+
node.initialize();
|
|
694
|
+
|
|
695
|
+
return node;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Combines two constant nodes based on a given operator.
|
|
700
|
+
* @param {Object} left - The left constant node.
|
|
701
|
+
* @param {Object} right - The right constant node.
|
|
702
|
+
* @param {string} operator - The operation to perform (add, subtract, multiply, divide).
|
|
703
|
+
* @param {number} fontSize - The font size for the resulting node.
|
|
704
|
+
* @returns {Object|null} A new constant or rational node representing the combined value, or null if division by zero occurs.
|
|
705
|
+
*/
|
|
706
|
+
static combineConstants(left, right, operator, fontSize) {
|
|
707
|
+
const leftVal = left.getValue();
|
|
708
|
+
const rightVal = right.getValue();
|
|
709
|
+
|
|
710
|
+
let resultNode = null;
|
|
711
|
+
|
|
712
|
+
switch(operator) {
|
|
713
|
+
case 'add':
|
|
714
|
+
resultNode = SimplificationEngine.createConstant(leftVal + rightVal, fontSize, left, right);
|
|
715
|
+
break;
|
|
716
|
+
case 'subtract':
|
|
717
|
+
resultNode = SimplificationEngine.createConstant(leftVal - rightVal, fontSize, left, right);
|
|
718
|
+
break;
|
|
719
|
+
case 'multiply':
|
|
720
|
+
resultNode = SimplificationEngine.createConstant(leftVal * rightVal, fontSize, left, right);
|
|
721
|
+
break;
|
|
722
|
+
case 'divide':
|
|
723
|
+
if (rightVal === 0) return null; // Avoid division by zero
|
|
724
|
+
resultNode = SimplificationEngine.rational(leftVal, rightVal, fontSize, left, right);
|
|
725
|
+
break;
|
|
726
|
+
default:
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// The automatic provenance tracking is now handled by the create methods above
|
|
731
|
+
return resultNode;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* ===== RULE FACTORY FUNCTIONS =====
|
|
736
|
+
* Functions to create instances of SimplificationRule for common simplification patterns.
|
|
737
|
+
*/
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Creates a new SimplificationRule instance.
|
|
741
|
+
* @param {string} name - The name of the rule.
|
|
742
|
+
* @param {function(Object): boolean|Object|null} matchFn - The match function for the rule.
|
|
743
|
+
* @param {function(Object, Object): Object} transformFn - The transform function for the rule.
|
|
744
|
+
* @param {function(Object, Object, Object): string} [messageFn=null] - Optional function to generate human-readable messages.
|
|
745
|
+
* @param {string} [type=null] - The type/category of the rule (e.g., 'rational', 'arithmetic', 'algebraic').
|
|
746
|
+
* @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule instance.
|
|
747
|
+
*/
|
|
748
|
+
static createRule(name, matchFn, transformFn, messageFn = null, type = null) {
|
|
749
|
+
return new SimplificationEngine.SimplificationRule(name, matchFn, transformFn, messageFn, type);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Creates a rule for folding (simplifying) binary operations with two constant operands.
|
|
754
|
+
* @param {string} name - The name of the constant fold rule.
|
|
755
|
+
* @param {string} operator - The operator of the binary expression (e.g., 'add', 'subtract').
|
|
756
|
+
* @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for constant folding.
|
|
757
|
+
*/
|
|
758
|
+
static createConstantFoldRule(name, operator) {
|
|
759
|
+
return SimplificationEngine.createRule(name,
|
|
760
|
+
// Match: binary op with two constant or rational constant operands
|
|
761
|
+
(node) => {
|
|
762
|
+
if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
|
|
763
|
+
return node.left.isConstant() && node.right.isConstant();
|
|
764
|
+
},
|
|
765
|
+
// Transform: combine the constants
|
|
766
|
+
(node) => {
|
|
767
|
+
const newNode = SimplificationEngine.combineConstants(node.left, node.right, operator, node.getFontSize());
|
|
768
|
+
// The combineConstants function now correctly handles its own provenance.
|
|
769
|
+
// No need to manually add the parent node's id.
|
|
770
|
+
return newNode;
|
|
771
|
+
},
|
|
772
|
+
// Message: describe the constant folding operation
|
|
773
|
+
(originalNode, ruleData, newNode) => {
|
|
774
|
+
const leftVal = originalNode.left.getValue();
|
|
775
|
+
const rightVal = originalNode.right.getValue();
|
|
776
|
+
const result = newNode.getValue ? newNode.getValue() : newNode.toString();
|
|
777
|
+
|
|
778
|
+
const operatorSymbols = {
|
|
779
|
+
'add': '+',
|
|
780
|
+
'subtract': '-',
|
|
781
|
+
'multiply': getMultiplicationSymbol(),
|
|
782
|
+
'divide': '÷'
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const symbol = operatorSymbols[operator] || operator;
|
|
786
|
+
return `Combined constants: ${leftVal} ${symbol} ${rightVal} = ${result}`;
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Creates an identity rule (e.g., x + 0 → x, x * 1 → x).
|
|
793
|
+
* @param {string} name - The name of the identity rule.
|
|
794
|
+
* @param {string} operator - The operator of the binary expression.
|
|
795
|
+
* @param {number} identityValue - The identity value for the operation (e.g., 0 for addition, 1 for multiplication).
|
|
796
|
+
* @param {'left'|'right'|'both'} [side='both'] - Specifies which side the identity value should be on ('left', 'right', or 'both').
|
|
797
|
+
* @returns {SimplificationEngine.SimplificationRule} A new SimplificationRule for the identity operation.
|
|
798
|
+
*/
|
|
799
|
+
static createIdentityRule(name, operator, identityValue, side = 'both') {
|
|
800
|
+
return SimplificationEngine.createRule(name,
|
|
801
|
+
// Match: binary op with one constant operand being the identity value
|
|
802
|
+
(node) => {
|
|
803
|
+
if (!SimplificationEngine.isBinaryOp(node, operator)) return false;
|
|
804
|
+
|
|
805
|
+
const constOperandInfo = SimplificationEngine.hasConstantOperand(node);
|
|
806
|
+
if (!constOperandInfo || !constOperandInfo.constant.isConstant() || constOperandInfo.constant.getValue() !== identityValue) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (side === 'left' && node.right.isConstant()) return false;
|
|
811
|
+
if (side === 'right' && node.left.isConstant()) return false;
|
|
812
|
+
|
|
813
|
+
return { other: constOperandInfo.other };
|
|
814
|
+
},
|
|
815
|
+
// Transform: return the other operand
|
|
816
|
+
(node, data) => {
|
|
817
|
+
const newNode = data.other.clone();
|
|
818
|
+
|
|
819
|
+
// Use manual provenance tracking for substitution
|
|
820
|
+
applyProvenance(newNode, node);
|
|
821
|
+
|
|
822
|
+
// Add a flag to tell the highlighting engine that this was a simple identity transform.
|
|
823
|
+
newNode.__isSimpleIdentity = true;
|
|
824
|
+
|
|
825
|
+
return newNode;
|
|
826
|
+
},
|
|
827
|
+
// Message: describe the identity operation
|
|
828
|
+
(originalNode, ruleData, newNode) => {
|
|
829
|
+
const { other } = ruleData;
|
|
830
|
+
const otherStr = other.toString ? other.toString() : 'expression';
|
|
831
|
+
|
|
832
|
+
const operatorNames = {
|
|
833
|
+
'add': 'addition',
|
|
834
|
+
'subtract': 'subtraction',
|
|
835
|
+
'multiply': 'multiplication',
|
|
836
|
+
'divide': 'division'
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
const operatorSymbols = {
|
|
840
|
+
'add': '+',
|
|
841
|
+
'subtract': '-',
|
|
842
|
+
'multiply': getMultiplicationSymbol(),
|
|
843
|
+
'divide': '÷'
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const opName = operatorNames[operator] || operator;
|
|
847
|
+
const symbol = operatorSymbols[operator] || operator;
|
|
848
|
+
|
|
849
|
+
const isLeftIdentity = originalNode.left.isConstant() && originalNode.left.getValue() === identityValue;
|
|
850
|
+
const position = isLeftIdentity ? 'left' : 'right';
|
|
851
|
+
|
|
852
|
+
if (operator === 'add' && identityValue === 0) {
|
|
853
|
+
return `Applied additive identity: "${otherStr} + 0" simplified to "${otherStr}" (adding 0 doesn't change the value)`;
|
|
854
|
+
} else if (operator === 'subtract' && identityValue === 0) {
|
|
855
|
+
return `Applied subtraction identity: "${otherStr} - 0" simplified to "${otherStr}" (subtracting 0 doesn't change the value)`;
|
|
856
|
+
} else if (operator === 'multiply' && identityValue === 1) {
|
|
857
|
+
return `Applied multiplicative identity: "${otherStr} ${getMultiplicationSymbol()} 1" simplified to "${otherStr}" (multiplying by 1 doesn't change the value)`;
|
|
858
|
+
} else if (operator === 'divide' && identityValue === 1) {
|
|
859
|
+
return `Applied division identity: "${otherStr} ÷ 1" simplified to "${otherStr}" (dividing by 1 doesn't change the value)`;
|
|
860
|
+
} else {
|
|
861
|
+
return `Applied ${opName} identity: removed ${identityValue} from ${position} side, leaving "${otherStr}"`;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Creates a rule for simplifying zero multiplication (e.g., x * 0 = 0).
|
|
869
|
+
*/
|
|
870
|
+
static createZeroMultiplicationRule() {
|
|
871
|
+
return SimplificationEngine.createRule("Zero Multiplication",
|
|
872
|
+
(node) => {
|
|
873
|
+
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
874
|
+
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
875
|
+
return constOp && constOp.constant.getValue() === 0;
|
|
876
|
+
},
|
|
877
|
+
(node) => {
|
|
878
|
+
const newNode = SimplificationEngine.createConstant(0, node.getFontSize(), node);
|
|
879
|
+
return newNode;
|
|
880
|
+
},
|
|
881
|
+
(originalNode, ruleData, newNode) => {
|
|
882
|
+
const constOp = SimplificationEngine.hasConstantOperand(originalNode);
|
|
883
|
+
const otherOperand = constOp.other;
|
|
884
|
+
return `Applied zero multiplication: ${utils.nodeToString(otherOperand)} ${getMultiplicationSymbol()} 0 = 0`;
|
|
885
|
+
}
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
888
|
}
|