@teachinglab/omd 0.6.1 → 0.6.3
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 +606 -547
- package/omd/core/omdUtilities.js +113 -113
- package/omd/display/omdDisplay.js +1045 -963
- 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 -57
- 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,1037 +1,1037 @@
|
|
|
1
|
-
import { SimplificationEngine } from '../omdSimplificationEngine.js';
|
|
2
|
-
import * as utils from '../simplificationUtils.js';
|
|
3
|
-
import { getMultiplicationSymbol } from '../../config/omdConfigManager.js';
|
|
4
|
-
import { omdRationalNode } from '../../nodes/omdRationalNode.js';
|
|
5
|
-
|
|
6
|
-
// ===== BINARY EXPRESSION RULES =====
|
|
7
|
-
export const binaryRules = [
|
|
8
|
-
// Handle addition cases like a + (-a) = 0
|
|
9
|
-
SimplificationEngine.createRule("Opposite Term Cancellation",
|
|
10
|
-
(node) => {
|
|
11
|
-
// Only handle addition for now
|
|
12
|
-
if (!SimplificationEngine.isBinaryOp(node, 'add')) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Check for constant + (-constant) patterns (2 + (-2) = 0)
|
|
17
|
-
if (node.left.isConstant()) {
|
|
18
|
-
const leftVal = node.left.getValue();
|
|
19
|
-
|
|
20
|
-
// Check if right side is a unary minus of a constant
|
|
21
|
-
if (SimplificationEngine.isType(node.right, 'omdUnaryExpressionNode') &&
|
|
22
|
-
node.right.operation === 'unaryMinus' &&
|
|
23
|
-
node.right.argument.isConstant()) {
|
|
24
|
-
const rightVal = node.right.argument.getValue();
|
|
25
|
-
if (leftVal === rightVal) {
|
|
26
|
-
return {
|
|
27
|
-
leftTerm: node.left,
|
|
28
|
-
rightTerm: node.right,
|
|
29
|
-
termType: 'constant',
|
|
30
|
-
leftValue: leftVal,
|
|
31
|
-
rightValue: -rightVal,
|
|
32
|
-
isNegatedRight: true
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check for direct opposite constants (rare case)
|
|
38
|
-
if (node.right.isConstant()) {
|
|
39
|
-
const rightVal = node.right.getValue();
|
|
40
|
-
if (leftVal === -rightVal) {
|
|
41
|
-
return {
|
|
42
|
-
leftTerm: node.left,
|
|
43
|
-
rightTerm: node.right,
|
|
44
|
-
termType: 'constant',
|
|
45
|
-
leftValue: leftVal,
|
|
46
|
-
rightValue: rightVal,
|
|
47
|
-
isNegatedRight: false
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check if we have a + (-a) pattern for monomials
|
|
54
|
-
const leftMonomial = SimplificationEngine.isMonomial(node.left);
|
|
55
|
-
let rightMonomial = null;
|
|
56
|
-
let isNegatedRight = false;
|
|
57
|
-
|
|
58
|
-
// Check if right side is a unary minus
|
|
59
|
-
if (SimplificationEngine.isType(node.right, 'omdUnaryExpressionNode') &&
|
|
60
|
-
node.right.operation === 'unaryMinus') {
|
|
61
|
-
rightMonomial = SimplificationEngine.isMonomial(node.right.argument);
|
|
62
|
-
isNegatedRight = true;
|
|
63
|
-
} else {
|
|
64
|
-
rightMonomial = SimplificationEngine.isMonomial(node.right);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (leftMonomial && rightMonomial) {
|
|
68
|
-
// For a + (-a), check if left coefficient equals right coefficient
|
|
69
|
-
// For a + (-2a), check if left coefficient equals negative of right coefficient
|
|
70
|
-
let leftCoeff = leftMonomial.coefficient;
|
|
71
|
-
let rightCoeff = rightMonomial.coefficient;
|
|
72
|
-
|
|
73
|
-
if (isNegatedRight) {
|
|
74
|
-
rightCoeff = -rightCoeff; // Since it's wrapped in unary minus
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Check if they are the same variable with same power and opposite coefficients
|
|
78
|
-
if (leftMonomial.variable === rightMonomial.variable &&
|
|
79
|
-
leftMonomial.power === rightMonomial.power &&
|
|
80
|
-
leftCoeff === -rightCoeff) {
|
|
81
|
-
return {
|
|
82
|
-
leftTerm: node.left,
|
|
83
|
-
rightTerm: node.right,
|
|
84
|
-
termType: 'monomial',
|
|
85
|
-
variable: leftMonomial.variable,
|
|
86
|
-
power: leftMonomial.power,
|
|
87
|
-
leftCoeff: leftCoeff,
|
|
88
|
-
rightCoeff: rightCoeff,
|
|
89
|
-
isNegatedRight: isNegatedRight
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return false;
|
|
95
|
-
},
|
|
96
|
-
(node, data) => {
|
|
97
|
-
const { leftTerm, rightTerm } = data;
|
|
98
|
-
const zeroNode = SimplificationEngine.createConstant(0, node.getFontSize());
|
|
99
|
-
|
|
100
|
-
// Preserve provenance from both terms
|
|
101
|
-
zeroNode.provenance.push(leftTerm.id);
|
|
102
|
-
zeroNode.provenance.push(rightTerm.id);
|
|
103
|
-
zeroNode.provenance.push(node.id);
|
|
104
|
-
|
|
105
|
-
return zeroNode;
|
|
106
|
-
},
|
|
107
|
-
(originalNode, ruleData, newNode) => {
|
|
108
|
-
const { termType } = ruleData;
|
|
109
|
-
|
|
110
|
-
if (termType === 'constant') {
|
|
111
|
-
const { leftValue, rightValue, isNegatedRight } = ruleData;
|
|
112
|
-
if (isNegatedRight) {
|
|
113
|
-
return `Cancelled opposite terms: ${leftValue} + (-${leftValue}) = 0`;
|
|
114
|
-
} else {
|
|
115
|
-
return `Cancelled opposite terms: ${leftValue} + ${rightValue} = 0`;
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
const { variable, power, leftCoeff, rightCoeff, isNegatedRight } = ruleData;
|
|
119
|
-
const powerStr = power !== 1 ? `^${power}` : '';
|
|
120
|
-
|
|
121
|
-
// Format the left term
|
|
122
|
-
const leftCoeffStr = leftCoeff === 1 ? '' : leftCoeff === -1 ? '-' : `${leftCoeff}`;
|
|
123
|
-
const leftTermStr = `${leftCoeffStr}${variable}${powerStr}`;
|
|
124
|
-
|
|
125
|
-
// Format the right term
|
|
126
|
-
let rightTermStr;
|
|
127
|
-
if (isNegatedRight) {
|
|
128
|
-
const innerCoeff = Math.abs(rightCoeff);
|
|
129
|
-
const innerCoeffStr = innerCoeff === 1 ? '' : `${innerCoeff}`;
|
|
130
|
-
rightTermStr = `(-${innerCoeffStr}${variable}${powerStr})`;
|
|
131
|
-
} else {
|
|
132
|
-
const rightCoeffStr = rightCoeff === 1 ? '' : rightCoeff === -1 ? '-' : `${rightCoeff}`;
|
|
133
|
-
rightTermStr = `${rightCoeffStr}${variable}${powerStr}`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return `Cancelled opposite terms: ${leftTermStr} + ${rightTermStr} = 0`;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
),
|
|
140
|
-
|
|
141
|
-
// Handle constant cancellation in multi-term sums (3x + 2 - 2 → 3x + 0)
|
|
142
|
-
SimplificationEngine.createRule("Cancel Constants in Sums",
|
|
143
|
-
(node) => {
|
|
144
|
-
if (!SimplificationEngine.isBinaryOp(node) ||
|
|
145
|
-
(node.operation !== 'add' && node.operation !== 'subtract')) return false;
|
|
146
|
-
|
|
147
|
-
// Flatten the sum to get all terms
|
|
148
|
-
const terms = [];
|
|
149
|
-
utils.flattenSum(node, terms);
|
|
150
|
-
|
|
151
|
-
// Only proceed if we have at least 3 terms (need non-constants + constants that cancel)
|
|
152
|
-
if (terms.length < 3) return false;
|
|
153
|
-
|
|
154
|
-
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
155
|
-
const nonConstantTerms = terms.filter(t => !t.node.isConstant());
|
|
156
|
-
|
|
157
|
-
// Need at least one non-constant term and at least 2 constants
|
|
158
|
-
if (nonConstantTerms.length === 0 || constantTerms.length < 2) return false;
|
|
159
|
-
|
|
160
|
-
// Check if any constants cancel out exactly
|
|
161
|
-
const cancellingPairs = [];
|
|
162
|
-
const usedIndices = new Set();
|
|
163
|
-
|
|
164
|
-
for (let i = 0; i < constantTerms.length; i++) {
|
|
165
|
-
if (usedIndices.has(i)) continue;
|
|
166
|
-
|
|
167
|
-
const term1 = constantTerms[i];
|
|
168
|
-
const val1 = term1.node.getValue() * term1.sign;
|
|
169
|
-
|
|
170
|
-
for (let j = i + 1; j < constantTerms.length; j++) {
|
|
171
|
-
if (usedIndices.has(j)) continue;
|
|
172
|
-
|
|
173
|
-
const term2 = constantTerms[j];
|
|
174
|
-
const val2 = term2.node.getValue() * term2.sign;
|
|
175
|
-
|
|
176
|
-
// Check if they cancel exactly
|
|
177
|
-
if (val1 + val2 === 0) {
|
|
178
|
-
cancellingPairs.push([term1, term2]);
|
|
179
|
-
usedIndices.add(i);
|
|
180
|
-
usedIndices.add(j);
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (cancellingPairs.length > 0) {
|
|
187
|
-
return {
|
|
188
|
-
terms: terms,
|
|
189
|
-
cancellingPairs: cancellingPairs,
|
|
190
|
-
constantTerms: constantTerms,
|
|
191
|
-
nonConstantTerms: nonConstantTerms
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return false;
|
|
196
|
-
},
|
|
197
|
-
(node, data) => {
|
|
198
|
-
const { terms, cancellingPairs, constantTerms, nonConstantTerms } = data;
|
|
199
|
-
|
|
200
|
-
// Start with non-constant terms
|
|
201
|
-
const finalTerms = [...nonConstantTerms];
|
|
202
|
-
|
|
203
|
-
// Keep track of which constant terms were cancelled
|
|
204
|
-
const cancelledTerms = new Set();
|
|
205
|
-
cancellingPairs.forEach(pair => {
|
|
206
|
-
pair.forEach(term => cancelledTerms.add(term));
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Add back any constant terms that weren't cancelled
|
|
210
|
-
constantTerms.forEach(term => {
|
|
211
|
-
if (!cancelledTerms.has(term)) {
|
|
212
|
-
finalTerms.push(term);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Collect detailed provenance from all cancelled terms
|
|
217
|
-
let cancelledNodeIds = [];
|
|
218
|
-
if (cancellingPairs.length > 0) {
|
|
219
|
-
const zeroNode = SimplificationEngine.createConstant(0, node.getFontSize());
|
|
220
|
-
|
|
221
|
-
// Add provenance from all cancelled terms and their operators
|
|
222
|
-
cancellingPairs.forEach(pair => {
|
|
223
|
-
// Find the operator node between the pair
|
|
224
|
-
const [term1, term2] = pair;
|
|
225
|
-
const parent = terms.find(t =>
|
|
226
|
-
t.node.type === 'omdBinaryExpressionNode' &&
|
|
227
|
-
((t.node.left === term1.node && t.node.right === term2.node) ||
|
|
228
|
-
(t.node.left === term2.node && t.node.right === term1.node))
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
// Add provenance from the constants and the operator
|
|
232
|
-
pair.forEach(term => {
|
|
233
|
-
zeroNode.provenance.push(term.node.id);
|
|
234
|
-
cancelledNodeIds.push(term.node.id);
|
|
235
|
-
|
|
236
|
-
// If this term has an operator (e.g. the minus sign), include it
|
|
237
|
-
if (term.node.operation) {
|
|
238
|
-
zeroNode.provenance.push(term.node.operation.id);
|
|
239
|
-
cancelledNodeIds.push(term.node.operation.id);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
// Add provenance from the binary operation's operator
|
|
244
|
-
if (parent && parent.node.operation) {
|
|
245
|
-
zeroNode.provenance.push(parent.node.operation.id);
|
|
246
|
-
cancelledNodeIds.push(parent.node.operation.id);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
finalTerms.push({ node: zeroNode, sign: 1 });
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Build the result tree
|
|
254
|
-
const result = utils.buildSumTree(finalTerms, node.getFontSize());
|
|
255
|
-
|
|
256
|
-
if (result) {
|
|
257
|
-
// Only preserve provenance from the cancelled terms
|
|
258
|
-
cancelledNodeIds.forEach(id => {
|
|
259
|
-
if (!result.provenance.includes(id)) {
|
|
260
|
-
result.provenance.push(id);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return result;
|
|
266
|
-
},
|
|
267
|
-
(originalNode, ruleData, newNode) => {
|
|
268
|
-
const { cancellingPairs } = ruleData;
|
|
269
|
-
|
|
270
|
-
const cancellationDescriptions = cancellingPairs.map(pair => {
|
|
271
|
-
const [term1, term2] = pair;
|
|
272
|
-
const val1 = term1.node.getValue();
|
|
273
|
-
const val2 = term2.node.getValue();
|
|
274
|
-
const sign1 = term1.sign === 1 ? '+' : '-';
|
|
275
|
-
const sign2 = term2.sign === 1 ? '+' : '-';
|
|
276
|
-
|
|
277
|
-
return `${sign1} ${val1} ${sign2} ${val2} = 0`;
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
return `Cancelled constants in sum: ${cancellationDescriptions.join(', ')}`;
|
|
281
|
-
}
|
|
282
|
-
),
|
|
283
|
-
|
|
284
|
-
// Basic constant folding (works for both regular and rational constants)
|
|
285
|
-
SimplificationEngine.createConstantFoldRule("Add Constants", "add"),
|
|
286
|
-
SimplificationEngine.createConstantFoldRule("Subtract Constants", "subtract"),
|
|
287
|
-
SimplificationEngine.createConstantFoldRule("Multiply Constants", "multiply"),
|
|
288
|
-
SimplificationEngine.createConstantFoldRule("Divide Constants", "divide"),
|
|
289
|
-
|
|
290
|
-
// Identity operations
|
|
291
|
-
SimplificationEngine.createIdentityRule("Add Zero", "add", 0), // x + 0 → x, 0 + x → x
|
|
292
|
-
SimplificationEngine.createIdentityRule("Subtract Zero", "subtract", 0, 'right'), // x - 0 → x
|
|
293
|
-
SimplificationEngine.createIdentityRule("Multiply One", "multiply", 1), // x * 1 → x, 1 * x → x
|
|
294
|
-
SimplificationEngine.createIdentityRule("Divide One", "divide", 1, 'right'), // x / 1 → x
|
|
295
|
-
|
|
296
|
-
// Zero multiplication (anything times zero equals zero)
|
|
297
|
-
SimplificationEngine.createZeroMultiplicationRule(),
|
|
298
|
-
|
|
299
|
-
// Coefficient multiplication (2 * 3x → 6x)
|
|
300
|
-
SimplificationEngine.createRule("Combine Coefficients",
|
|
301
|
-
(node) => {
|
|
302
|
-
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
303
|
-
|
|
304
|
-
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
305
|
-
if (!constOp) return false;
|
|
306
|
-
|
|
307
|
-
const otherNode = constOp.other;
|
|
308
|
-
if (!SimplificationEngine.isBinaryOp(otherNode, 'multiply')) return false;
|
|
309
|
-
|
|
310
|
-
const innerConstOp = SimplificationEngine.hasConstantOperand(otherNode);
|
|
311
|
-
if (!innerConstOp) return false;
|
|
312
|
-
|
|
313
|
-
const outerConstant = constOp.constant.getValue();
|
|
314
|
-
const innerConstant = innerConstOp.constant.getValue();
|
|
315
|
-
const expression = innerConstOp.other;
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
coefficient: outerConstant * innerConstant,
|
|
319
|
-
expression
|
|
320
|
-
};
|
|
321
|
-
},
|
|
322
|
-
(node, data) => {
|
|
323
|
-
const { coefficient, expression } = data;
|
|
324
|
-
const newNode = SimplificationEngine.createMultiplication(
|
|
325
|
-
SimplificationEngine.createConstant(coefficient, node.getFontSize()),
|
|
326
|
-
expression.clone(),
|
|
327
|
-
node.getFontSize()
|
|
328
|
-
);
|
|
329
|
-
|
|
330
|
-
// Preserve provenance from both operands
|
|
331
|
-
newNode.provenance.push(node.id);
|
|
332
|
-
if (newNode.left) {
|
|
333
|
-
newNode.left.provenance.push(node.left.id, node.right.id);
|
|
334
|
-
}
|
|
335
|
-
if (newNode.right) {
|
|
336
|
-
newNode.right.provenance.push(expression.id);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return newNode;
|
|
340
|
-
},
|
|
341
|
-
(originalNode, ruleData, newNode) => {
|
|
342
|
-
const { coefficient, expression } = ruleData;
|
|
343
|
-
const constOp = SimplificationEngine.hasConstantOperand(originalNode);
|
|
344
|
-
const innerConstOp = SimplificationEngine.hasConstantOperand(constOp.other);
|
|
345
|
-
const outerVal = constOp.constant.getValue();
|
|
346
|
-
const innerVal = innerConstOp.constant.getValue();
|
|
347
|
-
|
|
348
|
-
return `Combined coefficients: ${outerVal} ${getMultiplicationSymbol()} ${innerVal} = ${coefficient}`;
|
|
349
|
-
}
|
|
350
|
-
),
|
|
351
|
-
|
|
352
|
-
// Distributive property (2*(x+3) → 2x + 6)
|
|
353
|
-
SimplificationEngine.createRule("Distributive Property",
|
|
354
|
-
(node) => {
|
|
355
|
-
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
356
|
-
|
|
357
|
-
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
358
|
-
if (!constOp) return false;
|
|
359
|
-
|
|
360
|
-
const otherNode = constOp.other;
|
|
361
|
-
|
|
362
|
-
// Check if the other operand is a parenthesized sum/difference
|
|
363
|
-
let innerExpr = otherNode;
|
|
364
|
-
if (SimplificationEngine.isType(otherNode, 'omdParenthesisNode')) {
|
|
365
|
-
innerExpr = otherNode.expression;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!SimplificationEngine.isBinaryOp(innerExpr, 'add') &&
|
|
369
|
-
!SimplificationEngine.isBinaryOp(innerExpr, 'subtract')) {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
constantNode: constOp.constant,
|
|
375
|
-
innerExpr: innerExpr,
|
|
376
|
-
originalInnerNode: otherNode
|
|
377
|
-
};
|
|
378
|
-
},
|
|
379
|
-
(node, data) => {
|
|
380
|
-
const { constantNode, innerExpr } = data;
|
|
381
|
-
const multiplier = constantNode.getValue();
|
|
382
|
-
const fontSize = node.getFontSize();
|
|
383
|
-
|
|
384
|
-
// Distribute the constant across the terms
|
|
385
|
-
const terms = [];
|
|
386
|
-
utils.flattenSum(innerExpr, terms);
|
|
387
|
-
|
|
388
|
-
const distributedTerms = terms.map(term => {
|
|
389
|
-
const newCoeff = multiplier * term.sign;
|
|
390
|
-
const distributedNode = SimplificationEngine.createBinaryOp(
|
|
391
|
-
SimplificationEngine.createConstant(Math.abs(newCoeff), fontSize),
|
|
392
|
-
'multiply',
|
|
393
|
-
term.node.clone(),
|
|
394
|
-
fontSize
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
// Preserve provenance
|
|
398
|
-
distributedNode.provenance.push(node.id, constantNode.id, term.node.id);
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
node: distributedNode,
|
|
402
|
-
sign: newCoeff >= 0 ? 1 : -1
|
|
403
|
-
};
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
return utils.buildSumTree(distributedTerms, fontSize);
|
|
407
|
-
},
|
|
408
|
-
(originalNode, ruleData, newNode) => {
|
|
409
|
-
const { constantNode, innerExpr } = ruleData;
|
|
410
|
-
const multiplierStr = constantNode.toString();
|
|
411
|
-
const expressionStr = innerExpr.toString();
|
|
412
|
-
|
|
413
|
-
return `Applied distributive property: ${multiplierStr} ${getMultiplicationSymbol()} (${expressionStr})`;
|
|
414
|
-
}
|
|
415
|
-
),
|
|
416
|
-
|
|
417
|
-
// Expand polynomial multiplication like (3x+3)(2x+2) using FOIL/distributive property
|
|
418
|
-
SimplificationEngine.createRule("Expand Polynomial Multiplication",
|
|
419
|
-
(node) => {
|
|
420
|
-
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
421
|
-
|
|
422
|
-
// Both operands should be sums or differences (optionally parenthesized)
|
|
423
|
-
let leftExpr = SimplificationEngine.unwrapParentheses(node.left);
|
|
424
|
-
let rightExpr = SimplificationEngine.unwrapParentheses(node.right);
|
|
425
|
-
|
|
426
|
-
// Check if both sides are sums or differences
|
|
427
|
-
const leftIsSum = SimplificationEngine.isBinaryOp(leftExpr, 'add') ||
|
|
428
|
-
SimplificationEngine.isBinaryOp(leftExpr, 'subtract');
|
|
429
|
-
const rightIsSum = SimplificationEngine.isBinaryOp(rightExpr, 'add') ||
|
|
430
|
-
SimplificationEngine.isBinaryOp(rightExpr, 'subtract');
|
|
431
|
-
|
|
432
|
-
if (!leftIsSum || !rightIsSum) return false;
|
|
433
|
-
|
|
434
|
-
// Extract terms from both expressions
|
|
435
|
-
const leftTerms = [];
|
|
436
|
-
const rightTerms = [];
|
|
437
|
-
utils.flattenSum(leftExpr, leftTerms);
|
|
438
|
-
utils.flattenSum(rightExpr, rightTerms);
|
|
439
|
-
|
|
440
|
-
// Limit to reasonable number of terms to avoid explosion
|
|
441
|
-
if (leftTerms.length > 4 || rightTerms.length > 4) return false;
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
leftExpression: leftExpr,
|
|
445
|
-
rightExpression: rightExpr,
|
|
446
|
-
leftTerms: leftTerms,
|
|
447
|
-
rightTerms: rightTerms
|
|
448
|
-
};
|
|
449
|
-
},
|
|
450
|
-
(node, data) => {
|
|
451
|
-
const { leftTerms, rightTerms } = data;
|
|
452
|
-
const fontSize = node.getFontSize();
|
|
453
|
-
|
|
454
|
-
// Apply distributive property: each term in left multiplied by each term in right
|
|
455
|
-
const expandedTerms = [];
|
|
456
|
-
|
|
457
|
-
for (const leftTerm of leftTerms) {
|
|
458
|
-
for (const rightTerm of rightTerms) {
|
|
459
|
-
const productSign = leftTerm.sign * rightTerm.sign;
|
|
460
|
-
const productNode = SimplificationEngine.createBinaryOp(
|
|
461
|
-
leftTerm.node.clone(),
|
|
462
|
-
'multiply',
|
|
463
|
-
rightTerm.node.clone(),
|
|
464
|
-
fontSize
|
|
465
|
-
);
|
|
466
|
-
|
|
467
|
-
// Use granular provenance
|
|
468
|
-
const allLeafNodes = [...(leftTerm.leafNodes || []), ...(rightTerm.leafNodes || [])];
|
|
469
|
-
allLeafNodes.forEach(leafNode => {
|
|
470
|
-
[productNode.left, productNode.right, productNode].forEach(part => {
|
|
471
|
-
if (part && !part.provenance.includes(leafNode.id)) {
|
|
472
|
-
part.provenance.push(leafNode.id);
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
expandedTerms.push({
|
|
478
|
-
node: productNode,
|
|
479
|
-
sign: productSign,
|
|
480
|
-
leafNodes: allLeafNodes
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return utils.buildSumTree(expandedTerms, fontSize);
|
|
486
|
-
},
|
|
487
|
-
(originalNode, ruleData, newNode) => {
|
|
488
|
-
const { leftExpression, rightExpression } = ruleData;
|
|
489
|
-
const leftStr = utils.nodeToString(leftExpression);
|
|
490
|
-
const rightStr = utils.nodeToString(rightExpression);
|
|
491
|
-
return `Expanded polynomial multiplication: (${leftStr})(${rightStr})`;
|
|
492
|
-
}
|
|
493
|
-
),
|
|
494
|
-
|
|
495
|
-
// Multiply monomials (2x * 3x -> 6x^2, 3y^2 * 4y -> 12y^3)
|
|
496
|
-
SimplificationEngine.createRule("Multiply Monomials",
|
|
497
|
-
(node) => {
|
|
498
|
-
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
499
|
-
|
|
500
|
-
// Check if both operands are monomials
|
|
501
|
-
const leftMonomial = SimplificationEngine.isMonomial(node.left);
|
|
502
|
-
const rightMonomial = SimplificationEngine.isMonomial(node.right);
|
|
503
|
-
|
|
504
|
-
if (!leftMonomial || !rightMonomial) return false;
|
|
505
|
-
|
|
506
|
-
// Check if they have the same variable
|
|
507
|
-
if (leftMonomial.variable !== rightMonomial.variable) return false;
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
variable: leftMonomial.variable,
|
|
511
|
-
leftCoeff: leftMonomial.coefficient,
|
|
512
|
-
rightCoeff: rightMonomial.coefficient,
|
|
513
|
-
leftPower: leftMonomial.power,
|
|
514
|
-
rightPower: rightMonomial.power,
|
|
515
|
-
leftNode: node.left,
|
|
516
|
-
rightNode: node.right
|
|
517
|
-
};
|
|
518
|
-
},
|
|
519
|
-
(node, data) => {
|
|
520
|
-
const { variable, leftCoeff, rightCoeff, leftPower, rightPower, leftNode, rightNode } = data;
|
|
521
|
-
const fontSize = node.getFontSize();
|
|
522
|
-
|
|
523
|
-
// Calculate new coefficient and power
|
|
524
|
-
const newCoeff = leftCoeff * rightCoeff;
|
|
525
|
-
const newPower = leftPower + rightPower;
|
|
526
|
-
|
|
527
|
-
// Extract granular provenance
|
|
528
|
-
const leftProvenance = utils.extractMonomialProvenance(leftNode);
|
|
529
|
-
const rightProvenance = utils.extractMonomialProvenance(rightNode);
|
|
530
|
-
|
|
531
|
-
const coefficientProvenance = [
|
|
532
|
-
...leftProvenance.coefficientNodes.map(n => n.id),
|
|
533
|
-
...rightProvenance.coefficientNodes.map(n => n.id)
|
|
534
|
-
];
|
|
535
|
-
|
|
536
|
-
const variableProvenance = [
|
|
537
|
-
...leftProvenance.variableNodes.map(n => n.id),
|
|
538
|
-
...rightProvenance.variableNodes.map(n => n.id)
|
|
539
|
-
];
|
|
540
|
-
|
|
541
|
-
// Create the result with granular provenance
|
|
542
|
-
const result = utils.createMonomialWithGranularProvenance(
|
|
543
|
-
newCoeff,
|
|
544
|
-
variable,
|
|
545
|
-
newPower,
|
|
546
|
-
fontSize,
|
|
547
|
-
coefficientProvenance,
|
|
548
|
-
variableProvenance
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
result.provenance.push(node.id);
|
|
552
|
-
|
|
553
|
-
return result;
|
|
554
|
-
},
|
|
555
|
-
(originalNode, ruleData, newNode) => {
|
|
556
|
-
const { leftCoeff, rightCoeff, variable, leftPower, rightPower } = ruleData;
|
|
557
|
-
const newCoeff = leftCoeff * rightCoeff;
|
|
558
|
-
const newPower = leftPower + rightPower;
|
|
559
|
-
const leftStr = utils.nodeToString(ruleData.leftNode);
|
|
560
|
-
const rightStr = utils.nodeToString(ruleData.rightNode);
|
|
561
|
-
|
|
562
|
-
return `Multiplied monomials: ${leftStr} ${getMultiplicationSymbol()} ${rightStr} = ${newCoeff}${variable}${newPower > 1 ? `^${newPower}` : ''}`;
|
|
563
|
-
}
|
|
564
|
-
),
|
|
565
|
-
|
|
566
|
-
// Combine identical terms in multiplication to create powers (x*x -> x^2, y*y*y -> y^3)
|
|
567
|
-
SimplificationEngine.createRule("Combine Like Factors",
|
|
568
|
-
(node) => {
|
|
569
|
-
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
570
|
-
|
|
571
|
-
// Check if both operands are identical variables or simple expressions
|
|
572
|
-
if (SimplificationEngine.isType(node.left, 'omdVariableNode') &&
|
|
573
|
-
SimplificationEngine.isType(node.right, 'omdVariableNode') &&
|
|
574
|
-
node.left.name === node.right.name) {
|
|
575
|
-
return {
|
|
576
|
-
variable: node.left.name,
|
|
577
|
-
leftNode: node.left,
|
|
578
|
-
rightNode: node.right,
|
|
579
|
-
power: 2
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Check for more complex patterns like x^2 * x -> x^3
|
|
584
|
-
let baseVar = null;
|
|
585
|
-
let leftPower = 1;
|
|
586
|
-
let rightPower = 1;
|
|
587
|
-
let leftNode = node.left;
|
|
588
|
-
let rightNode = node.right;
|
|
589
|
-
|
|
590
|
-
// Analyze left operand
|
|
591
|
-
if (SimplificationEngine.isType(node.left, 'omdVariableNode')) {
|
|
592
|
-
baseVar = node.left.name;
|
|
593
|
-
leftPower = 1;
|
|
594
|
-
} else if (SimplificationEngine.isType(node.left, 'omdPowerNode') &&
|
|
595
|
-
SimplificationEngine.isType(node.left.base, 'omdVariableNode') &&
|
|
596
|
-
node.left.exponent.isConstant()) {
|
|
597
|
-
baseVar = node.left.base.name;
|
|
598
|
-
leftPower = node.left.exponent.getValue();
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Analyze right operand
|
|
602
|
-
if (SimplificationEngine.isType(node.right, 'omdVariableNode')) {
|
|
603
|
-
if (baseVar === node.right.name) {
|
|
604
|
-
rightPower = 1;
|
|
605
|
-
} else {
|
|
606
|
-
return false;
|
|
607
|
-
}
|
|
608
|
-
} else if (SimplificationEngine.isType(node.right, 'omdPowerNode') &&
|
|
609
|
-
SimplificationEngine.isType(node.right.base, 'omdVariableNode') &&
|
|
610
|
-
node.right.exponent.isConstant()) {
|
|
611
|
-
if (baseVar === node.right.base.name) {
|
|
612
|
-
rightPower = node.right.exponent.getValue();
|
|
613
|
-
} else {
|
|
614
|
-
return false;
|
|
615
|
-
}
|
|
616
|
-
} else {
|
|
617
|
-
return false;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
if (baseVar && Number.isInteger(leftPower) && Number.isInteger(rightPower)) {
|
|
621
|
-
return {
|
|
622
|
-
variable: baseVar,
|
|
623
|
-
leftNode: leftNode,
|
|
624
|
-
rightNode: rightNode,
|
|
625
|
-
power: leftPower + rightPower
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
return false;
|
|
630
|
-
},
|
|
631
|
-
(node, data) => {
|
|
632
|
-
const { variable, power, leftNode, rightNode } = data;
|
|
633
|
-
const fontSize = node.getFontSize();
|
|
634
|
-
|
|
635
|
-
let result;
|
|
636
|
-
if (power === 1) {
|
|
637
|
-
result = SimplificationEngine.createMonomial(1, variable, 1, fontSize);
|
|
638
|
-
} else {
|
|
639
|
-
const variableNode = SimplificationEngine.createMonomial(1, variable, 1, fontSize);
|
|
640
|
-
result = utils.createPowerTerm(variableNode, power, fontSize);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Preserve provenance from both original factors
|
|
644
|
-
result.provenance.push(leftNode.id, rightNode.id, node.id);
|
|
645
|
-
|
|
646
|
-
return result;
|
|
647
|
-
},
|
|
648
|
-
(originalNode, ruleData, newNode) => {
|
|
649
|
-
const { variable, power } = ruleData;
|
|
650
|
-
const leftStr = utils.nodeToString(ruleData.leftNode);
|
|
651
|
-
const rightStr = utils.nodeToString(ruleData.rightNode);
|
|
652
|
-
|
|
653
|
-
return `Combined like factors: ${leftStr} ${getMultiplicationSymbol()} ${rightStr} = ${variable}${power > 1 ? `^${power}` : ''}`;
|
|
654
|
-
}
|
|
655
|
-
),
|
|
656
|
-
|
|
657
|
-
// Complex sum folding (x + 2 + 3 → x + 5)
|
|
658
|
-
SimplificationEngine.createRule("Combine Multiple Constants in Sums",
|
|
659
|
-
(node) => {
|
|
660
|
-
if (!SimplificationEngine.isBinaryOp(node) ||
|
|
661
|
-
(node.operation !== 'add' && node.operation !== 'subtract')) return false;
|
|
662
|
-
|
|
663
|
-
// Flatten the sum
|
|
664
|
-
const terms = [];
|
|
665
|
-
utils.flattenSum(node, terms);
|
|
666
|
-
|
|
667
|
-
const constantCount = terms.filter(t => t.node.isConstant()).length;
|
|
668
|
-
if (constantCount <= 1) return false;
|
|
669
|
-
|
|
670
|
-
// Calculate what the combined constant would be
|
|
671
|
-
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
672
|
-
let totalNum = 0, totalDen = 1;
|
|
673
|
-
for (const term of constantTerms) {
|
|
674
|
-
const { num, den } = term.node.getRationalValue();
|
|
675
|
-
const newTotalNum = (totalNum * den) + (num * term.sign * totalDen);
|
|
676
|
-
const newTotalDen = totalDen * den;
|
|
677
|
-
totalNum = newTotalNum;
|
|
678
|
-
totalDen = newTotalDen;
|
|
679
|
-
const commonDivisor = utils.gcd(Math.abs(totalNum), Math.abs(totalDen));
|
|
680
|
-
totalNum /= commonDivisor;
|
|
681
|
-
totalDen /= commonDivisor;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Don't handle cases where the result would be zero - let cancellation rules handle those
|
|
685
|
-
if (totalNum === 0) return false;
|
|
686
|
-
|
|
687
|
-
return { terms };
|
|
688
|
-
},
|
|
689
|
-
(node, data) => {
|
|
690
|
-
const { terms } = data;
|
|
691
|
-
|
|
692
|
-
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
693
|
-
const otherTerms = terms.filter(t => !t.node.isConstant());
|
|
694
|
-
|
|
695
|
-
// Get the actual constant nodes for provenance
|
|
696
|
-
const constantNodes = constantTerms.map(t => t.node);
|
|
697
|
-
|
|
698
|
-
// Combine all constants using rational arithmetic
|
|
699
|
-
let totalNum = 0, totalDen = 1;
|
|
700
|
-
for (const term of constantTerms) {
|
|
701
|
-
const { num, den } = term.node.getRationalValue();
|
|
702
|
-
const newTotalNum = (totalNum * den) + (num * term.sign * totalDen);
|
|
703
|
-
const newTotalDen = totalDen * den;
|
|
704
|
-
totalNum = newTotalNum;
|
|
705
|
-
totalDen = newTotalDen;
|
|
706
|
-
const commonDivisor = utils.gcd(Math.abs(totalNum), Math.abs(totalDen));
|
|
707
|
-
totalNum /= commonDivisor;
|
|
708
|
-
totalDen /= commonDivisor;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Add combined constant back to terms if non-zero
|
|
712
|
-
let finalTerms = [...otherTerms];
|
|
713
|
-
|
|
714
|
-
if (totalNum !== 0 || finalTerms.length === 0) {
|
|
715
|
-
if (totalDen < 0) { totalNum = -totalNum; totalDen = -totalDen; }
|
|
716
|
-
const sign = totalNum >= 0 ? 1 : -1;
|
|
717
|
-
const absNum = Math.abs(totalNum);
|
|
718
|
-
|
|
719
|
-
let newNode;
|
|
720
|
-
if (totalDen === 1) {
|
|
721
|
-
// Create a simple constant with automatic provenance tracking
|
|
722
|
-
newNode = SimplificationEngine.createConstant(absNum, node.getFontSize(), ...constantNodes);
|
|
723
|
-
} else {
|
|
724
|
-
// Create a rational node
|
|
725
|
-
newNode = new omdRationalNode({
|
|
726
|
-
type: 'OperatorNode',
|
|
727
|
-
fn: 'divide',
|
|
728
|
-
args: [
|
|
729
|
-
{ type: 'ConstantNode', value: absNum },
|
|
730
|
-
{ type: 'ConstantNode', value: totalDen }
|
|
731
|
-
]
|
|
732
|
-
});
|
|
733
|
-
newNode.setFontSize(node.getFontSize());
|
|
734
|
-
// Apply provenance for rational nodes manually using the same approach
|
|
735
|
-
constantNodes.forEach(sourceNode => {
|
|
736
|
-
if (sourceNode.id && !newNode.provenance.includes(sourceNode.id)) {
|
|
737
|
-
newNode.provenance.push(sourceNode.id);
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
finalTerms.push({ node: newNode, sign: sign });
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (finalTerms.length === 0) {
|
|
746
|
-
const zeroNode = SimplificationEngine.createConstant(0, node.getFontSize(), ...constantNodes);
|
|
747
|
-
return zeroNode;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const result = utils.buildSumTree(finalTerms, node.getFontSize());
|
|
751
|
-
|
|
752
|
-
if(result) {
|
|
753
|
-
constantNodes.forEach(sourceNode => {
|
|
754
|
-
if (sourceNode.id && !result.provenance.includes(sourceNode.id)) {
|
|
755
|
-
result.provenance.push(sourceNode.id);
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
result.provenance.push(node.id);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return result;
|
|
762
|
-
},
|
|
763
|
-
(originalNode, ruleData, newNode) => {
|
|
764
|
-
const { terms } = ruleData;
|
|
765
|
-
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
766
|
-
|
|
767
|
-
// Calculate the combined value
|
|
768
|
-
let totalNum = 0, totalDen = 1;
|
|
769
|
-
for (const term of constantTerms) {
|
|
770
|
-
const { num, den } = term.node.getRationalValue();
|
|
771
|
-
const newTotalNum = (totalNum * den) + (num * term.sign * totalDen);
|
|
772
|
-
const newTotalDen = totalDen * den;
|
|
773
|
-
totalNum = newTotalNum;
|
|
774
|
-
totalDen = newTotalDen;
|
|
775
|
-
const commonDivisor = utils.gcd(Math.abs(totalNum), Math.abs(totalDen));
|
|
776
|
-
totalNum /= commonDivisor;
|
|
777
|
-
totalDen /= commonDivisor;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const constantStrings = constantTerms.map(t => {
|
|
781
|
-
const valueStr = utils.nodeToString(t.node);
|
|
782
|
-
return t.sign === 1 ? `+ ${valueStr}` : `- ${valueStr}`;
|
|
783
|
-
});
|
|
784
|
-
// For the first term, if it's positive, remove the leading "+ ".
|
|
785
|
-
if (constantStrings.length > 0 && constantStrings[0].startsWith('+ ')) {
|
|
786
|
-
constantStrings[0] = constantStrings[0].substring(2);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const calculation = constantStrings.join(' ');
|
|
790
|
-
const resultStr = totalDen === 1 ? `${totalNum}` : `${totalNum}/${totalDen}`;
|
|
791
|
-
|
|
792
|
-
let message = `Combining the constant terms: ${calculation} = ${resultStr}. `;
|
|
793
|
-
|
|
794
|
-
if (totalNum === 0) {
|
|
795
|
-
message += `Since the constants sum to zero, they are replaced by 0.`;
|
|
796
|
-
} else {
|
|
797
|
-
message += `The constants are replaced by their sum.`;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
return message;
|
|
801
|
-
}
|
|
802
|
-
),
|
|
803
|
-
|
|
804
|
-
// Combine like terms (monomials with same variable and power)
|
|
805
|
-
SimplificationEngine.createRule("Combine Like Terms",
|
|
806
|
-
(node) => {
|
|
807
|
-
if (!SimplificationEngine.isBinaryOp(node, 'add') && !SimplificationEngine.isBinaryOp(node, 'subtract')) {
|
|
808
|
-
return false;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const terms = [];
|
|
812
|
-
utils.flattenSum(node, terms);
|
|
813
|
-
|
|
814
|
-
const likeTermGroups = new Map();
|
|
815
|
-
const otherTerms = [];
|
|
816
|
-
|
|
817
|
-
for (const term of terms) {
|
|
818
|
-
const monomialInfo = SimplificationEngine.isMonomial(term.node);
|
|
819
|
-
|
|
820
|
-
if (monomialInfo) {
|
|
821
|
-
const key = `${monomialInfo.variable}^${monomialInfo.power}`;
|
|
822
|
-
if (!likeTermGroups.has(key)) {
|
|
823
|
-
likeTermGroups.set(key, []);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
likeTermGroups.get(key).push({
|
|
827
|
-
term,
|
|
828
|
-
monomialInfo,
|
|
829
|
-
originalNodeId: term.node.id
|
|
830
|
-
});
|
|
831
|
-
} else {
|
|
832
|
-
otherTerms.push(term);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Check if we have like terms to combine
|
|
837
|
-
const foundLikeTerms = Array.from(likeTermGroups.values()).some(group => group.length > 1);
|
|
838
|
-
|
|
839
|
-
return foundLikeTerms ? { likeTermGroups, otherTerms } : false;
|
|
840
|
-
},
|
|
841
|
-
(node, data) => {
|
|
842
|
-
const { likeTermGroups, otherTerms } = data;
|
|
843
|
-
// Start with a copy of the original terms array (to preserve order)
|
|
844
|
-
let allNewTerms = [...otherTerms];
|
|
845
|
-
// We'll build a map from node id to its index in the original terms array
|
|
846
|
-
const originalTerms = [];
|
|
847
|
-
utils.flattenSum(node, originalTerms);
|
|
848
|
-
|
|
849
|
-
// For each group of like terms
|
|
850
|
-
for (const [key, termGroup] of likeTermGroups) {
|
|
851
|
-
if (termGroup.length === 1) {
|
|
852
|
-
// Find the index of this term in the original terms array
|
|
853
|
-
const idx = originalTerms.findIndex(t => t.node.id === termGroup[0].term.node.id);
|
|
854
|
-
if (idx !== -1) {
|
|
855
|
-
allNewTerms.splice(idx, 0, termGroup[0].term);
|
|
856
|
-
} else {
|
|
857
|
-
allNewTerms.push(termGroup[0].term);
|
|
858
|
-
}
|
|
859
|
-
} else {
|
|
860
|
-
// Combine multiple like terms
|
|
861
|
-
let totalCoeff = 0;
|
|
862
|
-
const coefficientProvenance = [];
|
|
863
|
-
const variableProvenance = [];
|
|
864
|
-
const likeTermIds = termGroup.map(t => t.term.node.id);
|
|
865
|
-
for (const termData of termGroup) {
|
|
866
|
-
const coeff = termData.monomialInfo.coefficient * termData.term.sign;
|
|
867
|
-
totalCoeff += coeff;
|
|
868
|
-
// Extract granular provenance
|
|
869
|
-
const monomialProvenance = utils.extractMonomialProvenance(termData.term.node);
|
|
870
|
-
monomialProvenance.coefficientNodes.forEach(coeffNode => {
|
|
871
|
-
if (!coefficientProvenance.includes(coeffNode.id)) {
|
|
872
|
-
coefficientProvenance.push(coeffNode.id);
|
|
873
|
-
}
|
|
874
|
-
});
|
|
875
|
-
monomialProvenance.variableNodes.forEach(varNode => {
|
|
876
|
-
if (!variableProvenance.includes(varNode.id)) {
|
|
877
|
-
variableProvenance.push(varNode.id);
|
|
878
|
-
}
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
// Create combined term if coefficient is non-zero
|
|
882
|
-
if (totalCoeff !== 0) {
|
|
883
|
-
const firstTerm = termGroup[0];
|
|
884
|
-
const newMonomial = utils.createMonomialWithGranularProvenance(
|
|
885
|
-
totalCoeff,
|
|
886
|
-
firstTerm.monomialInfo.variable,
|
|
887
|
-
firstTerm.monomialInfo.power,
|
|
888
|
-
node.getFontSize(),
|
|
889
|
-
coefficientProvenance,
|
|
890
|
-
variableProvenance
|
|
891
|
-
);
|
|
892
|
-
// Find the leftmost index of any like term in the original terms array
|
|
893
|
-
const leftmostIdx = originalTerms.findIndex(t => likeTermIds.includes(t.node.id));
|
|
894
|
-
// Remove all like terms from allNewTerms (by node id)
|
|
895
|
-
allNewTerms = allNewTerms.filter(t => !likeTermIds.includes(t.node.id));
|
|
896
|
-
// Insert the new combined term at the leftmost index
|
|
897
|
-
if (leftmostIdx !== -1) {
|
|
898
|
-
allNewTerms.splice(leftmostIdx, 0, { node: newMonomial, sign: 1 });
|
|
899
|
-
} else {
|
|
900
|
-
allNewTerms.push({ node: newMonomial, sign: 1 });
|
|
901
|
-
}
|
|
902
|
-
} else {
|
|
903
|
-
// If the sum is zero, just remove all like terms
|
|
904
|
-
allNewTerms = allNewTerms.filter(t => !likeTermIds.includes(t.node.id));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Handle zero case
|
|
910
|
-
if (allNewTerms.length === 0) {
|
|
911
|
-
return SimplificationEngine.createConstant(0, node.getFontSize());
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return utils.buildSumTree(allNewTerms, node.getFontSize());
|
|
915
|
-
},
|
|
916
|
-
(originalNode, ruleData, newNode) => {
|
|
917
|
-
const { likeTermGroups } = ruleData;
|
|
918
|
-
const combinations = [];
|
|
919
|
-
|
|
920
|
-
for (const [key, termGroup] of likeTermGroups) {
|
|
921
|
-
if (termGroup.length > 1) {
|
|
922
|
-
let totalCoeff = 0;
|
|
923
|
-
const termDetails = [];
|
|
924
|
-
|
|
925
|
-
for (const termData of termGroup) {
|
|
926
|
-
const coeff = termData.monomialInfo.coefficient * termData.term.sign;
|
|
927
|
-
totalCoeff += coeff;
|
|
928
|
-
|
|
929
|
-
// Format individual terms for the explanation
|
|
930
|
-
const variable = termData.monomialInfo.variable;
|
|
931
|
-
const power = termData.monomialInfo.power;
|
|
932
|
-
const powerStr = power !== 1 ? `^${power}` : '';
|
|
933
|
-
|
|
934
|
-
if (coeff === 1) {
|
|
935
|
-
termDetails.push(`${variable}${powerStr}`);
|
|
936
|
-
} else if (coeff === -1) {
|
|
937
|
-
termDetails.push(`-${variable}${powerStr}`);
|
|
938
|
-
} else {
|
|
939
|
-
termDetails.push(`${coeff}${variable}${powerStr}`);
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
const variable = termGroup[0].monomialInfo.variable;
|
|
944
|
-
const power = termGroup[0].monomialInfo.power;
|
|
945
|
-
const powerStr = power !== 1 ? `^${power}` : '';
|
|
946
|
-
|
|
947
|
-
if (totalCoeff === 0) {
|
|
948
|
-
combinations.push(`${termDetails.join(' + ').replace('+ -', '- ')} = 0 (like terms cancelled)`);
|
|
949
|
-
} else {
|
|
950
|
-
const resultStr = totalCoeff === 1 ? `${variable}${powerStr}` :
|
|
951
|
-
totalCoeff === -1 ? `-${variable}${powerStr}` :
|
|
952
|
-
`${totalCoeff}${variable}${powerStr}`;
|
|
953
|
-
combinations.push(`${termDetails.join(' + ').replace('+ -', '- ')} = ${resultStr}`);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (combinations.length === 0) {
|
|
959
|
-
return "No like terms were found to combine";
|
|
960
|
-
} else if (combinations.length === 1) {
|
|
961
|
-
return `Combined like terms: ${combinations[0]}`;
|
|
962
|
-
} else {
|
|
963
|
-
return `Combined like terms: ${combinations.join('; ')}`;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
),
|
|
967
|
-
|
|
968
|
-
// Multiply then divide by same factor: (a*x)/a → x or a*(x/a) → x
|
|
969
|
-
SimplificationEngine.createRule("Multiply Divide Same Factor",
|
|
970
|
-
(node) => {
|
|
971
|
-
// Check for (a*x)/a pattern (rational node with multiplication in numerator)
|
|
972
|
-
if (SimplificationEngine.isType(node, 'omdRationalNode')) {
|
|
973
|
-
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
974
|
-
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
975
|
-
|
|
976
|
-
if (SimplificationEngine.isBinaryOp(numerator, 'multiply') &&
|
|
977
|
-
SimplificationEngine.isType(denominator, 'omdConstantNode')) {
|
|
978
|
-
|
|
979
|
-
const constOp = SimplificationEngine.hasConstantOperand(numerator);
|
|
980
|
-
if (constOp && constOp.constant.getValue() === denominator.getValue()) {
|
|
981
|
-
return {
|
|
982
|
-
pattern: 'rational',
|
|
983
|
-
factor: constOp.constant.getValue(),
|
|
984
|
-
expression: constOp.other,
|
|
985
|
-
factorNode: constOp.constant,
|
|
986
|
-
denominatorNode: denominator
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
// Check for a*(x/a) pattern (multiplication with rational)
|
|
993
|
-
if (SimplificationEngine.isBinaryOp(node, 'multiply')) {
|
|
994
|
-
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
995
|
-
if (!constOp) return false;
|
|
996
|
-
|
|
997
|
-
const otherNode = constOp.other;
|
|
998
|
-
if (SimplificationEngine.isType(otherNode, 'omdRationalNode')) {
|
|
999
|
-
const denominator = SimplificationEngine.unwrapParentheses(otherNode.denominator);
|
|
1000
|
-
|
|
1001
|
-
if (SimplificationEngine.isType(denominator, 'omdConstantNode') &&
|
|
1002
|
-
constOp.constant.getValue() === denominator.getValue()) {
|
|
1003
|
-
|
|
1004
|
-
return {
|
|
1005
|
-
pattern: 'multiply',
|
|
1006
|
-
factor: constOp.constant.getValue(),
|
|
1007
|
-
expression: SimplificationEngine.unwrapParentheses(otherNode.numerator),
|
|
1008
|
-
factorNode: constOp.constant,
|
|
1009
|
-
denominatorNode: denominator
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return false;
|
|
1016
|
-
},
|
|
1017
|
-
(node, data) => {
|
|
1018
|
-
const { expression, factorNode, denominatorNode } = data;
|
|
1019
|
-
const newNode = expression.clone();
|
|
1020
|
-
|
|
1021
|
-
// Preserve provenance
|
|
1022
|
-
newNode.provenance.push(factorNode.id);
|
|
1023
|
-
newNode.provenance.push(denominatorNode.id);
|
|
1024
|
-
newNode.provenance.push(node.id);
|
|
1025
|
-
|
|
1026
|
-
return newNode;
|
|
1027
|
-
},
|
|
1028
|
-
(originalNode, ruleData, newNode) => {
|
|
1029
|
-
const { pattern, factor } = ruleData;
|
|
1030
|
-
if (pattern === 'rational') {
|
|
1031
|
-
return `Simplified multiplication and division: (${factor} × expression)/${factor} = expression`;
|
|
1032
|
-
} else {
|
|
1033
|
-
return `Simplified multiplication and division: ${factor} × (expression/${factor}) = expression`;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
)
|
|
1037
|
-
];
|
|
1
|
+
import { SimplificationEngine } from '../omdSimplificationEngine.js';
|
|
2
|
+
import * as utils from '../simplificationUtils.js';
|
|
3
|
+
import { getMultiplicationSymbol } from '../../config/omdConfigManager.js';
|
|
4
|
+
import { omdRationalNode } from '../../nodes/omdRationalNode.js';
|
|
5
|
+
|
|
6
|
+
// ===== BINARY EXPRESSION RULES =====
|
|
7
|
+
export const binaryRules = [
|
|
8
|
+
// Handle addition cases like a + (-a) = 0
|
|
9
|
+
SimplificationEngine.createRule("Opposite Term Cancellation",
|
|
10
|
+
(node) => {
|
|
11
|
+
// Only handle addition for now
|
|
12
|
+
if (!SimplificationEngine.isBinaryOp(node, 'add')) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check for constant + (-constant) patterns (2 + (-2) = 0)
|
|
17
|
+
if (node.left.isConstant()) {
|
|
18
|
+
const leftVal = node.left.getValue();
|
|
19
|
+
|
|
20
|
+
// Check if right side is a unary minus of a constant
|
|
21
|
+
if (SimplificationEngine.isType(node.right, 'omdUnaryExpressionNode') &&
|
|
22
|
+
node.right.operation === 'unaryMinus' &&
|
|
23
|
+
node.right.argument.isConstant()) {
|
|
24
|
+
const rightVal = node.right.argument.getValue();
|
|
25
|
+
if (leftVal === rightVal) {
|
|
26
|
+
return {
|
|
27
|
+
leftTerm: node.left,
|
|
28
|
+
rightTerm: node.right,
|
|
29
|
+
termType: 'constant',
|
|
30
|
+
leftValue: leftVal,
|
|
31
|
+
rightValue: -rightVal,
|
|
32
|
+
isNegatedRight: true
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check for direct opposite constants (rare case)
|
|
38
|
+
if (node.right.isConstant()) {
|
|
39
|
+
const rightVal = node.right.getValue();
|
|
40
|
+
if (leftVal === -rightVal) {
|
|
41
|
+
return {
|
|
42
|
+
leftTerm: node.left,
|
|
43
|
+
rightTerm: node.right,
|
|
44
|
+
termType: 'constant',
|
|
45
|
+
leftValue: leftVal,
|
|
46
|
+
rightValue: rightVal,
|
|
47
|
+
isNegatedRight: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if we have a + (-a) pattern for monomials
|
|
54
|
+
const leftMonomial = SimplificationEngine.isMonomial(node.left);
|
|
55
|
+
let rightMonomial = null;
|
|
56
|
+
let isNegatedRight = false;
|
|
57
|
+
|
|
58
|
+
// Check if right side is a unary minus
|
|
59
|
+
if (SimplificationEngine.isType(node.right, 'omdUnaryExpressionNode') &&
|
|
60
|
+
node.right.operation === 'unaryMinus') {
|
|
61
|
+
rightMonomial = SimplificationEngine.isMonomial(node.right.argument);
|
|
62
|
+
isNegatedRight = true;
|
|
63
|
+
} else {
|
|
64
|
+
rightMonomial = SimplificationEngine.isMonomial(node.right);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (leftMonomial && rightMonomial) {
|
|
68
|
+
// For a + (-a), check if left coefficient equals right coefficient
|
|
69
|
+
// For a + (-2a), check if left coefficient equals negative of right coefficient
|
|
70
|
+
let leftCoeff = leftMonomial.coefficient;
|
|
71
|
+
let rightCoeff = rightMonomial.coefficient;
|
|
72
|
+
|
|
73
|
+
if (isNegatedRight) {
|
|
74
|
+
rightCoeff = -rightCoeff; // Since it's wrapped in unary minus
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if they are the same variable with same power and opposite coefficients
|
|
78
|
+
if (leftMonomial.variable === rightMonomial.variable &&
|
|
79
|
+
leftMonomial.power === rightMonomial.power &&
|
|
80
|
+
leftCoeff === -rightCoeff) {
|
|
81
|
+
return {
|
|
82
|
+
leftTerm: node.left,
|
|
83
|
+
rightTerm: node.right,
|
|
84
|
+
termType: 'monomial',
|
|
85
|
+
variable: leftMonomial.variable,
|
|
86
|
+
power: leftMonomial.power,
|
|
87
|
+
leftCoeff: leftCoeff,
|
|
88
|
+
rightCoeff: rightCoeff,
|
|
89
|
+
isNegatedRight: isNegatedRight
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
},
|
|
96
|
+
(node, data) => {
|
|
97
|
+
const { leftTerm, rightTerm } = data;
|
|
98
|
+
const zeroNode = SimplificationEngine.createConstant(0, node.getFontSize());
|
|
99
|
+
|
|
100
|
+
// Preserve provenance from both terms
|
|
101
|
+
zeroNode.provenance.push(leftTerm.id);
|
|
102
|
+
zeroNode.provenance.push(rightTerm.id);
|
|
103
|
+
zeroNode.provenance.push(node.id);
|
|
104
|
+
|
|
105
|
+
return zeroNode;
|
|
106
|
+
},
|
|
107
|
+
(originalNode, ruleData, newNode) => {
|
|
108
|
+
const { termType } = ruleData;
|
|
109
|
+
|
|
110
|
+
if (termType === 'constant') {
|
|
111
|
+
const { leftValue, rightValue, isNegatedRight } = ruleData;
|
|
112
|
+
if (isNegatedRight) {
|
|
113
|
+
return `Cancelled opposite terms: ${leftValue} + (-${leftValue}) = 0`;
|
|
114
|
+
} else {
|
|
115
|
+
return `Cancelled opposite terms: ${leftValue} + ${rightValue} = 0`;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const { variable, power, leftCoeff, rightCoeff, isNegatedRight } = ruleData;
|
|
119
|
+
const powerStr = power !== 1 ? `^${power}` : '';
|
|
120
|
+
|
|
121
|
+
// Format the left term
|
|
122
|
+
const leftCoeffStr = leftCoeff === 1 ? '' : leftCoeff === -1 ? '-' : `${leftCoeff}`;
|
|
123
|
+
const leftTermStr = `${leftCoeffStr}${variable}${powerStr}`;
|
|
124
|
+
|
|
125
|
+
// Format the right term
|
|
126
|
+
let rightTermStr;
|
|
127
|
+
if (isNegatedRight) {
|
|
128
|
+
const innerCoeff = Math.abs(rightCoeff);
|
|
129
|
+
const innerCoeffStr = innerCoeff === 1 ? '' : `${innerCoeff}`;
|
|
130
|
+
rightTermStr = `(-${innerCoeffStr}${variable}${powerStr})`;
|
|
131
|
+
} else {
|
|
132
|
+
const rightCoeffStr = rightCoeff === 1 ? '' : rightCoeff === -1 ? '-' : `${rightCoeff}`;
|
|
133
|
+
rightTermStr = `${rightCoeffStr}${variable}${powerStr}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return `Cancelled opposite terms: ${leftTermStr} + ${rightTermStr} = 0`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
),
|
|
140
|
+
|
|
141
|
+
// Handle constant cancellation in multi-term sums (3x + 2 - 2 → 3x + 0)
|
|
142
|
+
SimplificationEngine.createRule("Cancel Constants in Sums",
|
|
143
|
+
(node) => {
|
|
144
|
+
if (!SimplificationEngine.isBinaryOp(node) ||
|
|
145
|
+
(node.operation !== 'add' && node.operation !== 'subtract')) return false;
|
|
146
|
+
|
|
147
|
+
// Flatten the sum to get all terms
|
|
148
|
+
const terms = [];
|
|
149
|
+
utils.flattenSum(node, terms);
|
|
150
|
+
|
|
151
|
+
// Only proceed if we have at least 3 terms (need non-constants + constants that cancel)
|
|
152
|
+
if (terms.length < 3) return false;
|
|
153
|
+
|
|
154
|
+
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
155
|
+
const nonConstantTerms = terms.filter(t => !t.node.isConstant());
|
|
156
|
+
|
|
157
|
+
// Need at least one non-constant term and at least 2 constants
|
|
158
|
+
if (nonConstantTerms.length === 0 || constantTerms.length < 2) return false;
|
|
159
|
+
|
|
160
|
+
// Check if any constants cancel out exactly
|
|
161
|
+
const cancellingPairs = [];
|
|
162
|
+
const usedIndices = new Set();
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < constantTerms.length; i++) {
|
|
165
|
+
if (usedIndices.has(i)) continue;
|
|
166
|
+
|
|
167
|
+
const term1 = constantTerms[i];
|
|
168
|
+
const val1 = term1.node.getValue() * term1.sign;
|
|
169
|
+
|
|
170
|
+
for (let j = i + 1; j < constantTerms.length; j++) {
|
|
171
|
+
if (usedIndices.has(j)) continue;
|
|
172
|
+
|
|
173
|
+
const term2 = constantTerms[j];
|
|
174
|
+
const val2 = term2.node.getValue() * term2.sign;
|
|
175
|
+
|
|
176
|
+
// Check if they cancel exactly
|
|
177
|
+
if (val1 + val2 === 0) {
|
|
178
|
+
cancellingPairs.push([term1, term2]);
|
|
179
|
+
usedIndices.add(i);
|
|
180
|
+
usedIndices.add(j);
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (cancellingPairs.length > 0) {
|
|
187
|
+
return {
|
|
188
|
+
terms: terms,
|
|
189
|
+
cancellingPairs: cancellingPairs,
|
|
190
|
+
constantTerms: constantTerms,
|
|
191
|
+
nonConstantTerms: nonConstantTerms
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return false;
|
|
196
|
+
},
|
|
197
|
+
(node, data) => {
|
|
198
|
+
const { terms, cancellingPairs, constantTerms, nonConstantTerms } = data;
|
|
199
|
+
|
|
200
|
+
// Start with non-constant terms
|
|
201
|
+
const finalTerms = [...nonConstantTerms];
|
|
202
|
+
|
|
203
|
+
// Keep track of which constant terms were cancelled
|
|
204
|
+
const cancelledTerms = new Set();
|
|
205
|
+
cancellingPairs.forEach(pair => {
|
|
206
|
+
pair.forEach(term => cancelledTerms.add(term));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Add back any constant terms that weren't cancelled
|
|
210
|
+
constantTerms.forEach(term => {
|
|
211
|
+
if (!cancelledTerms.has(term)) {
|
|
212
|
+
finalTerms.push(term);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Collect detailed provenance from all cancelled terms
|
|
217
|
+
let cancelledNodeIds = [];
|
|
218
|
+
if (cancellingPairs.length > 0) {
|
|
219
|
+
const zeroNode = SimplificationEngine.createConstant(0, node.getFontSize());
|
|
220
|
+
|
|
221
|
+
// Add provenance from all cancelled terms and their operators
|
|
222
|
+
cancellingPairs.forEach(pair => {
|
|
223
|
+
// Find the operator node between the pair
|
|
224
|
+
const [term1, term2] = pair;
|
|
225
|
+
const parent = terms.find(t =>
|
|
226
|
+
t.node.type === 'omdBinaryExpressionNode' &&
|
|
227
|
+
((t.node.left === term1.node && t.node.right === term2.node) ||
|
|
228
|
+
(t.node.left === term2.node && t.node.right === term1.node))
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Add provenance from the constants and the operator
|
|
232
|
+
pair.forEach(term => {
|
|
233
|
+
zeroNode.provenance.push(term.node.id);
|
|
234
|
+
cancelledNodeIds.push(term.node.id);
|
|
235
|
+
|
|
236
|
+
// If this term has an operator (e.g. the minus sign), include it
|
|
237
|
+
if (term.node.operation) {
|
|
238
|
+
zeroNode.provenance.push(term.node.operation.id);
|
|
239
|
+
cancelledNodeIds.push(term.node.operation.id);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Add provenance from the binary operation's operator
|
|
244
|
+
if (parent && parent.node.operation) {
|
|
245
|
+
zeroNode.provenance.push(parent.node.operation.id);
|
|
246
|
+
cancelledNodeIds.push(parent.node.operation.id);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
finalTerms.push({ node: zeroNode, sign: 1 });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Build the result tree
|
|
254
|
+
const result = utils.buildSumTree(finalTerms, node.getFontSize());
|
|
255
|
+
|
|
256
|
+
if (result) {
|
|
257
|
+
// Only preserve provenance from the cancelled terms
|
|
258
|
+
cancelledNodeIds.forEach(id => {
|
|
259
|
+
if (!result.provenance.includes(id)) {
|
|
260
|
+
result.provenance.push(id);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
},
|
|
267
|
+
(originalNode, ruleData, newNode) => {
|
|
268
|
+
const { cancellingPairs } = ruleData;
|
|
269
|
+
|
|
270
|
+
const cancellationDescriptions = cancellingPairs.map(pair => {
|
|
271
|
+
const [term1, term2] = pair;
|
|
272
|
+
const val1 = term1.node.getValue();
|
|
273
|
+
const val2 = term2.node.getValue();
|
|
274
|
+
const sign1 = term1.sign === 1 ? '+' : '-';
|
|
275
|
+
const sign2 = term2.sign === 1 ? '+' : '-';
|
|
276
|
+
|
|
277
|
+
return `${sign1} ${val1} ${sign2} ${val2} = 0`;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return `Cancelled constants in sum: ${cancellationDescriptions.join(', ')}`;
|
|
281
|
+
}
|
|
282
|
+
),
|
|
283
|
+
|
|
284
|
+
// Basic constant folding (works for both regular and rational constants)
|
|
285
|
+
SimplificationEngine.createConstantFoldRule("Add Constants", "add"),
|
|
286
|
+
SimplificationEngine.createConstantFoldRule("Subtract Constants", "subtract"),
|
|
287
|
+
SimplificationEngine.createConstantFoldRule("Multiply Constants", "multiply"),
|
|
288
|
+
SimplificationEngine.createConstantFoldRule("Divide Constants", "divide"),
|
|
289
|
+
|
|
290
|
+
// Identity operations
|
|
291
|
+
SimplificationEngine.createIdentityRule("Add Zero", "add", 0), // x + 0 → x, 0 + x → x
|
|
292
|
+
SimplificationEngine.createIdentityRule("Subtract Zero", "subtract", 0, 'right'), // x - 0 → x
|
|
293
|
+
SimplificationEngine.createIdentityRule("Multiply One", "multiply", 1), // x * 1 → x, 1 * x → x
|
|
294
|
+
SimplificationEngine.createIdentityRule("Divide One", "divide", 1, 'right'), // x / 1 → x
|
|
295
|
+
|
|
296
|
+
// Zero multiplication (anything times zero equals zero)
|
|
297
|
+
SimplificationEngine.createZeroMultiplicationRule(),
|
|
298
|
+
|
|
299
|
+
// Coefficient multiplication (2 * 3x → 6x)
|
|
300
|
+
SimplificationEngine.createRule("Combine Coefficients",
|
|
301
|
+
(node) => {
|
|
302
|
+
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
303
|
+
|
|
304
|
+
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
305
|
+
if (!constOp) return false;
|
|
306
|
+
|
|
307
|
+
const otherNode = constOp.other;
|
|
308
|
+
if (!SimplificationEngine.isBinaryOp(otherNode, 'multiply')) return false;
|
|
309
|
+
|
|
310
|
+
const innerConstOp = SimplificationEngine.hasConstantOperand(otherNode);
|
|
311
|
+
if (!innerConstOp) return false;
|
|
312
|
+
|
|
313
|
+
const outerConstant = constOp.constant.getValue();
|
|
314
|
+
const innerConstant = innerConstOp.constant.getValue();
|
|
315
|
+
const expression = innerConstOp.other;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
coefficient: outerConstant * innerConstant,
|
|
319
|
+
expression
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
(node, data) => {
|
|
323
|
+
const { coefficient, expression } = data;
|
|
324
|
+
const newNode = SimplificationEngine.createMultiplication(
|
|
325
|
+
SimplificationEngine.createConstant(coefficient, node.getFontSize()),
|
|
326
|
+
expression.clone(),
|
|
327
|
+
node.getFontSize()
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Preserve provenance from both operands
|
|
331
|
+
newNode.provenance.push(node.id);
|
|
332
|
+
if (newNode.left) {
|
|
333
|
+
newNode.left.provenance.push(node.left.id, node.right.id);
|
|
334
|
+
}
|
|
335
|
+
if (newNode.right) {
|
|
336
|
+
newNode.right.provenance.push(expression.id);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return newNode;
|
|
340
|
+
},
|
|
341
|
+
(originalNode, ruleData, newNode) => {
|
|
342
|
+
const { coefficient, expression } = ruleData;
|
|
343
|
+
const constOp = SimplificationEngine.hasConstantOperand(originalNode);
|
|
344
|
+
const innerConstOp = SimplificationEngine.hasConstantOperand(constOp.other);
|
|
345
|
+
const outerVal = constOp.constant.getValue();
|
|
346
|
+
const innerVal = innerConstOp.constant.getValue();
|
|
347
|
+
|
|
348
|
+
return `Combined coefficients: ${outerVal} ${getMultiplicationSymbol()} ${innerVal} = ${coefficient}`;
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
|
|
352
|
+
// Distributive property (2*(x+3) → 2x + 6)
|
|
353
|
+
SimplificationEngine.createRule("Distributive Property",
|
|
354
|
+
(node) => {
|
|
355
|
+
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
356
|
+
|
|
357
|
+
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
358
|
+
if (!constOp) return false;
|
|
359
|
+
|
|
360
|
+
const otherNode = constOp.other;
|
|
361
|
+
|
|
362
|
+
// Check if the other operand is a parenthesized sum/difference
|
|
363
|
+
let innerExpr = otherNode;
|
|
364
|
+
if (SimplificationEngine.isType(otherNode, 'omdParenthesisNode')) {
|
|
365
|
+
innerExpr = otherNode.expression;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!SimplificationEngine.isBinaryOp(innerExpr, 'add') &&
|
|
369
|
+
!SimplificationEngine.isBinaryOp(innerExpr, 'subtract')) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
constantNode: constOp.constant,
|
|
375
|
+
innerExpr: innerExpr,
|
|
376
|
+
originalInnerNode: otherNode
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
(node, data) => {
|
|
380
|
+
const { constantNode, innerExpr } = data;
|
|
381
|
+
const multiplier = constantNode.getValue();
|
|
382
|
+
const fontSize = node.getFontSize();
|
|
383
|
+
|
|
384
|
+
// Distribute the constant across the terms
|
|
385
|
+
const terms = [];
|
|
386
|
+
utils.flattenSum(innerExpr, terms);
|
|
387
|
+
|
|
388
|
+
const distributedTerms = terms.map(term => {
|
|
389
|
+
const newCoeff = multiplier * term.sign;
|
|
390
|
+
const distributedNode = SimplificationEngine.createBinaryOp(
|
|
391
|
+
SimplificationEngine.createConstant(Math.abs(newCoeff), fontSize),
|
|
392
|
+
'multiply',
|
|
393
|
+
term.node.clone(),
|
|
394
|
+
fontSize
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Preserve provenance
|
|
398
|
+
distributedNode.provenance.push(node.id, constantNode.id, term.node.id);
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
node: distributedNode,
|
|
402
|
+
sign: newCoeff >= 0 ? 1 : -1
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return utils.buildSumTree(distributedTerms, fontSize);
|
|
407
|
+
},
|
|
408
|
+
(originalNode, ruleData, newNode) => {
|
|
409
|
+
const { constantNode, innerExpr } = ruleData;
|
|
410
|
+
const multiplierStr = constantNode.toString();
|
|
411
|
+
const expressionStr = innerExpr.toString();
|
|
412
|
+
|
|
413
|
+
return `Applied distributive property: ${multiplierStr} ${getMultiplicationSymbol()} (${expressionStr})`;
|
|
414
|
+
}
|
|
415
|
+
),
|
|
416
|
+
|
|
417
|
+
// Expand polynomial multiplication like (3x+3)(2x+2) using FOIL/distributive property
|
|
418
|
+
SimplificationEngine.createRule("Expand Polynomial Multiplication",
|
|
419
|
+
(node) => {
|
|
420
|
+
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
421
|
+
|
|
422
|
+
// Both operands should be sums or differences (optionally parenthesized)
|
|
423
|
+
let leftExpr = SimplificationEngine.unwrapParentheses(node.left);
|
|
424
|
+
let rightExpr = SimplificationEngine.unwrapParentheses(node.right);
|
|
425
|
+
|
|
426
|
+
// Check if both sides are sums or differences
|
|
427
|
+
const leftIsSum = SimplificationEngine.isBinaryOp(leftExpr, 'add') ||
|
|
428
|
+
SimplificationEngine.isBinaryOp(leftExpr, 'subtract');
|
|
429
|
+
const rightIsSum = SimplificationEngine.isBinaryOp(rightExpr, 'add') ||
|
|
430
|
+
SimplificationEngine.isBinaryOp(rightExpr, 'subtract');
|
|
431
|
+
|
|
432
|
+
if (!leftIsSum || !rightIsSum) return false;
|
|
433
|
+
|
|
434
|
+
// Extract terms from both expressions
|
|
435
|
+
const leftTerms = [];
|
|
436
|
+
const rightTerms = [];
|
|
437
|
+
utils.flattenSum(leftExpr, leftTerms);
|
|
438
|
+
utils.flattenSum(rightExpr, rightTerms);
|
|
439
|
+
|
|
440
|
+
// Limit to reasonable number of terms to avoid explosion
|
|
441
|
+
if (leftTerms.length > 4 || rightTerms.length > 4) return false;
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
leftExpression: leftExpr,
|
|
445
|
+
rightExpression: rightExpr,
|
|
446
|
+
leftTerms: leftTerms,
|
|
447
|
+
rightTerms: rightTerms
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
(node, data) => {
|
|
451
|
+
const { leftTerms, rightTerms } = data;
|
|
452
|
+
const fontSize = node.getFontSize();
|
|
453
|
+
|
|
454
|
+
// Apply distributive property: each term in left multiplied by each term in right
|
|
455
|
+
const expandedTerms = [];
|
|
456
|
+
|
|
457
|
+
for (const leftTerm of leftTerms) {
|
|
458
|
+
for (const rightTerm of rightTerms) {
|
|
459
|
+
const productSign = leftTerm.sign * rightTerm.sign;
|
|
460
|
+
const productNode = SimplificationEngine.createBinaryOp(
|
|
461
|
+
leftTerm.node.clone(),
|
|
462
|
+
'multiply',
|
|
463
|
+
rightTerm.node.clone(),
|
|
464
|
+
fontSize
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// Use granular provenance
|
|
468
|
+
const allLeafNodes = [...(leftTerm.leafNodes || []), ...(rightTerm.leafNodes || [])];
|
|
469
|
+
allLeafNodes.forEach(leafNode => {
|
|
470
|
+
[productNode.left, productNode.right, productNode].forEach(part => {
|
|
471
|
+
if (part && !part.provenance.includes(leafNode.id)) {
|
|
472
|
+
part.provenance.push(leafNode.id);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
expandedTerms.push({
|
|
478
|
+
node: productNode,
|
|
479
|
+
sign: productSign,
|
|
480
|
+
leafNodes: allLeafNodes
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return utils.buildSumTree(expandedTerms, fontSize);
|
|
486
|
+
},
|
|
487
|
+
(originalNode, ruleData, newNode) => {
|
|
488
|
+
const { leftExpression, rightExpression } = ruleData;
|
|
489
|
+
const leftStr = utils.nodeToString(leftExpression);
|
|
490
|
+
const rightStr = utils.nodeToString(rightExpression);
|
|
491
|
+
return `Expanded polynomial multiplication: (${leftStr})(${rightStr})`;
|
|
492
|
+
}
|
|
493
|
+
),
|
|
494
|
+
|
|
495
|
+
// Multiply monomials (2x * 3x -> 6x^2, 3y^2 * 4y -> 12y^3)
|
|
496
|
+
SimplificationEngine.createRule("Multiply Monomials",
|
|
497
|
+
(node) => {
|
|
498
|
+
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
499
|
+
|
|
500
|
+
// Check if both operands are monomials
|
|
501
|
+
const leftMonomial = SimplificationEngine.isMonomial(node.left);
|
|
502
|
+
const rightMonomial = SimplificationEngine.isMonomial(node.right);
|
|
503
|
+
|
|
504
|
+
if (!leftMonomial || !rightMonomial) return false;
|
|
505
|
+
|
|
506
|
+
// Check if they have the same variable
|
|
507
|
+
if (leftMonomial.variable !== rightMonomial.variable) return false;
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
variable: leftMonomial.variable,
|
|
511
|
+
leftCoeff: leftMonomial.coefficient,
|
|
512
|
+
rightCoeff: rightMonomial.coefficient,
|
|
513
|
+
leftPower: leftMonomial.power,
|
|
514
|
+
rightPower: rightMonomial.power,
|
|
515
|
+
leftNode: node.left,
|
|
516
|
+
rightNode: node.right
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
(node, data) => {
|
|
520
|
+
const { variable, leftCoeff, rightCoeff, leftPower, rightPower, leftNode, rightNode } = data;
|
|
521
|
+
const fontSize = node.getFontSize();
|
|
522
|
+
|
|
523
|
+
// Calculate new coefficient and power
|
|
524
|
+
const newCoeff = leftCoeff * rightCoeff;
|
|
525
|
+
const newPower = leftPower + rightPower;
|
|
526
|
+
|
|
527
|
+
// Extract granular provenance
|
|
528
|
+
const leftProvenance = utils.extractMonomialProvenance(leftNode);
|
|
529
|
+
const rightProvenance = utils.extractMonomialProvenance(rightNode);
|
|
530
|
+
|
|
531
|
+
const coefficientProvenance = [
|
|
532
|
+
...leftProvenance.coefficientNodes.map(n => n.id),
|
|
533
|
+
...rightProvenance.coefficientNodes.map(n => n.id)
|
|
534
|
+
];
|
|
535
|
+
|
|
536
|
+
const variableProvenance = [
|
|
537
|
+
...leftProvenance.variableNodes.map(n => n.id),
|
|
538
|
+
...rightProvenance.variableNodes.map(n => n.id)
|
|
539
|
+
];
|
|
540
|
+
|
|
541
|
+
// Create the result with granular provenance
|
|
542
|
+
const result = utils.createMonomialWithGranularProvenance(
|
|
543
|
+
newCoeff,
|
|
544
|
+
variable,
|
|
545
|
+
newPower,
|
|
546
|
+
fontSize,
|
|
547
|
+
coefficientProvenance,
|
|
548
|
+
variableProvenance
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
result.provenance.push(node.id);
|
|
552
|
+
|
|
553
|
+
return result;
|
|
554
|
+
},
|
|
555
|
+
(originalNode, ruleData, newNode) => {
|
|
556
|
+
const { leftCoeff, rightCoeff, variable, leftPower, rightPower } = ruleData;
|
|
557
|
+
const newCoeff = leftCoeff * rightCoeff;
|
|
558
|
+
const newPower = leftPower + rightPower;
|
|
559
|
+
const leftStr = utils.nodeToString(ruleData.leftNode);
|
|
560
|
+
const rightStr = utils.nodeToString(ruleData.rightNode);
|
|
561
|
+
|
|
562
|
+
return `Multiplied monomials: ${leftStr} ${getMultiplicationSymbol()} ${rightStr} = ${newCoeff}${variable}${newPower > 1 ? `^${newPower}` : ''}`;
|
|
563
|
+
}
|
|
564
|
+
),
|
|
565
|
+
|
|
566
|
+
// Combine identical terms in multiplication to create powers (x*x -> x^2, y*y*y -> y^3)
|
|
567
|
+
SimplificationEngine.createRule("Combine Like Factors",
|
|
568
|
+
(node) => {
|
|
569
|
+
if (!SimplificationEngine.isBinaryOp(node, 'multiply')) return false;
|
|
570
|
+
|
|
571
|
+
// Check if both operands are identical variables or simple expressions
|
|
572
|
+
if (SimplificationEngine.isType(node.left, 'omdVariableNode') &&
|
|
573
|
+
SimplificationEngine.isType(node.right, 'omdVariableNode') &&
|
|
574
|
+
node.left.name === node.right.name) {
|
|
575
|
+
return {
|
|
576
|
+
variable: node.left.name,
|
|
577
|
+
leftNode: node.left,
|
|
578
|
+
rightNode: node.right,
|
|
579
|
+
power: 2
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Check for more complex patterns like x^2 * x -> x^3
|
|
584
|
+
let baseVar = null;
|
|
585
|
+
let leftPower = 1;
|
|
586
|
+
let rightPower = 1;
|
|
587
|
+
let leftNode = node.left;
|
|
588
|
+
let rightNode = node.right;
|
|
589
|
+
|
|
590
|
+
// Analyze left operand
|
|
591
|
+
if (SimplificationEngine.isType(node.left, 'omdVariableNode')) {
|
|
592
|
+
baseVar = node.left.name;
|
|
593
|
+
leftPower = 1;
|
|
594
|
+
} else if (SimplificationEngine.isType(node.left, 'omdPowerNode') &&
|
|
595
|
+
SimplificationEngine.isType(node.left.base, 'omdVariableNode') &&
|
|
596
|
+
node.left.exponent.isConstant()) {
|
|
597
|
+
baseVar = node.left.base.name;
|
|
598
|
+
leftPower = node.left.exponent.getValue();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Analyze right operand
|
|
602
|
+
if (SimplificationEngine.isType(node.right, 'omdVariableNode')) {
|
|
603
|
+
if (baseVar === node.right.name) {
|
|
604
|
+
rightPower = 1;
|
|
605
|
+
} else {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
} else if (SimplificationEngine.isType(node.right, 'omdPowerNode') &&
|
|
609
|
+
SimplificationEngine.isType(node.right.base, 'omdVariableNode') &&
|
|
610
|
+
node.right.exponent.isConstant()) {
|
|
611
|
+
if (baseVar === node.right.base.name) {
|
|
612
|
+
rightPower = node.right.exponent.getValue();
|
|
613
|
+
} else {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (baseVar && Number.isInteger(leftPower) && Number.isInteger(rightPower)) {
|
|
621
|
+
return {
|
|
622
|
+
variable: baseVar,
|
|
623
|
+
leftNode: leftNode,
|
|
624
|
+
rightNode: rightNode,
|
|
625
|
+
power: leftPower + rightPower
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return false;
|
|
630
|
+
},
|
|
631
|
+
(node, data) => {
|
|
632
|
+
const { variable, power, leftNode, rightNode } = data;
|
|
633
|
+
const fontSize = node.getFontSize();
|
|
634
|
+
|
|
635
|
+
let result;
|
|
636
|
+
if (power === 1) {
|
|
637
|
+
result = SimplificationEngine.createMonomial(1, variable, 1, fontSize);
|
|
638
|
+
} else {
|
|
639
|
+
const variableNode = SimplificationEngine.createMonomial(1, variable, 1, fontSize);
|
|
640
|
+
result = utils.createPowerTerm(variableNode, power, fontSize);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Preserve provenance from both original factors
|
|
644
|
+
result.provenance.push(leftNode.id, rightNode.id, node.id);
|
|
645
|
+
|
|
646
|
+
return result;
|
|
647
|
+
},
|
|
648
|
+
(originalNode, ruleData, newNode) => {
|
|
649
|
+
const { variable, power } = ruleData;
|
|
650
|
+
const leftStr = utils.nodeToString(ruleData.leftNode);
|
|
651
|
+
const rightStr = utils.nodeToString(ruleData.rightNode);
|
|
652
|
+
|
|
653
|
+
return `Combined like factors: ${leftStr} ${getMultiplicationSymbol()} ${rightStr} = ${variable}${power > 1 ? `^${power}` : ''}`;
|
|
654
|
+
}
|
|
655
|
+
),
|
|
656
|
+
|
|
657
|
+
// Complex sum folding (x + 2 + 3 → x + 5)
|
|
658
|
+
SimplificationEngine.createRule("Combine Multiple Constants in Sums",
|
|
659
|
+
(node) => {
|
|
660
|
+
if (!SimplificationEngine.isBinaryOp(node) ||
|
|
661
|
+
(node.operation !== 'add' && node.operation !== 'subtract')) return false;
|
|
662
|
+
|
|
663
|
+
// Flatten the sum
|
|
664
|
+
const terms = [];
|
|
665
|
+
utils.flattenSum(node, terms);
|
|
666
|
+
|
|
667
|
+
const constantCount = terms.filter(t => t.node.isConstant()).length;
|
|
668
|
+
if (constantCount <= 1) return false;
|
|
669
|
+
|
|
670
|
+
// Calculate what the combined constant would be
|
|
671
|
+
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
672
|
+
let totalNum = 0, totalDen = 1;
|
|
673
|
+
for (const term of constantTerms) {
|
|
674
|
+
const { num, den } = term.node.getRationalValue();
|
|
675
|
+
const newTotalNum = (totalNum * den) + (num * term.sign * totalDen);
|
|
676
|
+
const newTotalDen = totalDen * den;
|
|
677
|
+
totalNum = newTotalNum;
|
|
678
|
+
totalDen = newTotalDen;
|
|
679
|
+
const commonDivisor = utils.gcd(Math.abs(totalNum), Math.abs(totalDen));
|
|
680
|
+
totalNum /= commonDivisor;
|
|
681
|
+
totalDen /= commonDivisor;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Don't handle cases where the result would be zero - let cancellation rules handle those
|
|
685
|
+
if (totalNum === 0) return false;
|
|
686
|
+
|
|
687
|
+
return { terms };
|
|
688
|
+
},
|
|
689
|
+
(node, data) => {
|
|
690
|
+
const { terms } = data;
|
|
691
|
+
|
|
692
|
+
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
693
|
+
const otherTerms = terms.filter(t => !t.node.isConstant());
|
|
694
|
+
|
|
695
|
+
// Get the actual constant nodes for provenance
|
|
696
|
+
const constantNodes = constantTerms.map(t => t.node);
|
|
697
|
+
|
|
698
|
+
// Combine all constants using rational arithmetic
|
|
699
|
+
let totalNum = 0, totalDen = 1;
|
|
700
|
+
for (const term of constantTerms) {
|
|
701
|
+
const { num, den } = term.node.getRationalValue();
|
|
702
|
+
const newTotalNum = (totalNum * den) + (num * term.sign * totalDen);
|
|
703
|
+
const newTotalDen = totalDen * den;
|
|
704
|
+
totalNum = newTotalNum;
|
|
705
|
+
totalDen = newTotalDen;
|
|
706
|
+
const commonDivisor = utils.gcd(Math.abs(totalNum), Math.abs(totalDen));
|
|
707
|
+
totalNum /= commonDivisor;
|
|
708
|
+
totalDen /= commonDivisor;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Add combined constant back to terms if non-zero
|
|
712
|
+
let finalTerms = [...otherTerms];
|
|
713
|
+
|
|
714
|
+
if (totalNum !== 0 || finalTerms.length === 0) {
|
|
715
|
+
if (totalDen < 0) { totalNum = -totalNum; totalDen = -totalDen; }
|
|
716
|
+
const sign = totalNum >= 0 ? 1 : -1;
|
|
717
|
+
const absNum = Math.abs(totalNum);
|
|
718
|
+
|
|
719
|
+
let newNode;
|
|
720
|
+
if (totalDen === 1) {
|
|
721
|
+
// Create a simple constant with automatic provenance tracking
|
|
722
|
+
newNode = SimplificationEngine.createConstant(absNum, node.getFontSize(), ...constantNodes);
|
|
723
|
+
} else {
|
|
724
|
+
// Create a rational node
|
|
725
|
+
newNode = new omdRationalNode({
|
|
726
|
+
type: 'OperatorNode',
|
|
727
|
+
fn: 'divide',
|
|
728
|
+
args: [
|
|
729
|
+
{ type: 'ConstantNode', value: absNum },
|
|
730
|
+
{ type: 'ConstantNode', value: totalDen }
|
|
731
|
+
]
|
|
732
|
+
});
|
|
733
|
+
newNode.setFontSize(node.getFontSize());
|
|
734
|
+
// Apply provenance for rational nodes manually using the same approach
|
|
735
|
+
constantNodes.forEach(sourceNode => {
|
|
736
|
+
if (sourceNode.id && !newNode.provenance.includes(sourceNode.id)) {
|
|
737
|
+
newNode.provenance.push(sourceNode.id);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
finalTerms.push({ node: newNode, sign: sign });
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (finalTerms.length === 0) {
|
|
746
|
+
const zeroNode = SimplificationEngine.createConstant(0, node.getFontSize(), ...constantNodes);
|
|
747
|
+
return zeroNode;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const result = utils.buildSumTree(finalTerms, node.getFontSize());
|
|
751
|
+
|
|
752
|
+
if(result) {
|
|
753
|
+
constantNodes.forEach(sourceNode => {
|
|
754
|
+
if (sourceNode.id && !result.provenance.includes(sourceNode.id)) {
|
|
755
|
+
result.provenance.push(sourceNode.id);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
result.provenance.push(node.id);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return result;
|
|
762
|
+
},
|
|
763
|
+
(originalNode, ruleData, newNode) => {
|
|
764
|
+
const { terms } = ruleData;
|
|
765
|
+
const constantTerms = terms.filter(t => t.node.isConstant());
|
|
766
|
+
|
|
767
|
+
// Calculate the combined value
|
|
768
|
+
let totalNum = 0, totalDen = 1;
|
|
769
|
+
for (const term of constantTerms) {
|
|
770
|
+
const { num, den } = term.node.getRationalValue();
|
|
771
|
+
const newTotalNum = (totalNum * den) + (num * term.sign * totalDen);
|
|
772
|
+
const newTotalDen = totalDen * den;
|
|
773
|
+
totalNum = newTotalNum;
|
|
774
|
+
totalDen = newTotalDen;
|
|
775
|
+
const commonDivisor = utils.gcd(Math.abs(totalNum), Math.abs(totalDen));
|
|
776
|
+
totalNum /= commonDivisor;
|
|
777
|
+
totalDen /= commonDivisor;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const constantStrings = constantTerms.map(t => {
|
|
781
|
+
const valueStr = utils.nodeToString(t.node);
|
|
782
|
+
return t.sign === 1 ? `+ ${valueStr}` : `- ${valueStr}`;
|
|
783
|
+
});
|
|
784
|
+
// For the first term, if it's positive, remove the leading "+ ".
|
|
785
|
+
if (constantStrings.length > 0 && constantStrings[0].startsWith('+ ')) {
|
|
786
|
+
constantStrings[0] = constantStrings[0].substring(2);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const calculation = constantStrings.join(' ');
|
|
790
|
+
const resultStr = totalDen === 1 ? `${totalNum}` : `${totalNum}/${totalDen}`;
|
|
791
|
+
|
|
792
|
+
let message = `Combining the constant terms: ${calculation} = ${resultStr}. `;
|
|
793
|
+
|
|
794
|
+
if (totalNum === 0) {
|
|
795
|
+
message += `Since the constants sum to zero, they are replaced by 0.`;
|
|
796
|
+
} else {
|
|
797
|
+
message += `The constants are replaced by their sum.`;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return message;
|
|
801
|
+
}
|
|
802
|
+
),
|
|
803
|
+
|
|
804
|
+
// Combine like terms (monomials with same variable and power)
|
|
805
|
+
SimplificationEngine.createRule("Combine Like Terms",
|
|
806
|
+
(node) => {
|
|
807
|
+
if (!SimplificationEngine.isBinaryOp(node, 'add') && !SimplificationEngine.isBinaryOp(node, 'subtract')) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const terms = [];
|
|
812
|
+
utils.flattenSum(node, terms);
|
|
813
|
+
|
|
814
|
+
const likeTermGroups = new Map();
|
|
815
|
+
const otherTerms = [];
|
|
816
|
+
|
|
817
|
+
for (const term of terms) {
|
|
818
|
+
const monomialInfo = SimplificationEngine.isMonomial(term.node);
|
|
819
|
+
|
|
820
|
+
if (monomialInfo) {
|
|
821
|
+
const key = `${monomialInfo.variable}^${monomialInfo.power}`;
|
|
822
|
+
if (!likeTermGroups.has(key)) {
|
|
823
|
+
likeTermGroups.set(key, []);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
likeTermGroups.get(key).push({
|
|
827
|
+
term,
|
|
828
|
+
monomialInfo,
|
|
829
|
+
originalNodeId: term.node.id
|
|
830
|
+
});
|
|
831
|
+
} else {
|
|
832
|
+
otherTerms.push(term);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Check if we have like terms to combine
|
|
837
|
+
const foundLikeTerms = Array.from(likeTermGroups.values()).some(group => group.length > 1);
|
|
838
|
+
|
|
839
|
+
return foundLikeTerms ? { likeTermGroups, otherTerms } : false;
|
|
840
|
+
},
|
|
841
|
+
(node, data) => {
|
|
842
|
+
const { likeTermGroups, otherTerms } = data;
|
|
843
|
+
// Start with a copy of the original terms array (to preserve order)
|
|
844
|
+
let allNewTerms = [...otherTerms];
|
|
845
|
+
// We'll build a map from node id to its index in the original terms array
|
|
846
|
+
const originalTerms = [];
|
|
847
|
+
utils.flattenSum(node, originalTerms);
|
|
848
|
+
|
|
849
|
+
// For each group of like terms
|
|
850
|
+
for (const [key, termGroup] of likeTermGroups) {
|
|
851
|
+
if (termGroup.length === 1) {
|
|
852
|
+
// Find the index of this term in the original terms array
|
|
853
|
+
const idx = originalTerms.findIndex(t => t.node.id === termGroup[0].term.node.id);
|
|
854
|
+
if (idx !== -1) {
|
|
855
|
+
allNewTerms.splice(idx, 0, termGroup[0].term);
|
|
856
|
+
} else {
|
|
857
|
+
allNewTerms.push(termGroup[0].term);
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
// Combine multiple like terms
|
|
861
|
+
let totalCoeff = 0;
|
|
862
|
+
const coefficientProvenance = [];
|
|
863
|
+
const variableProvenance = [];
|
|
864
|
+
const likeTermIds = termGroup.map(t => t.term.node.id);
|
|
865
|
+
for (const termData of termGroup) {
|
|
866
|
+
const coeff = termData.monomialInfo.coefficient * termData.term.sign;
|
|
867
|
+
totalCoeff += coeff;
|
|
868
|
+
// Extract granular provenance
|
|
869
|
+
const monomialProvenance = utils.extractMonomialProvenance(termData.term.node);
|
|
870
|
+
monomialProvenance.coefficientNodes.forEach(coeffNode => {
|
|
871
|
+
if (!coefficientProvenance.includes(coeffNode.id)) {
|
|
872
|
+
coefficientProvenance.push(coeffNode.id);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
monomialProvenance.variableNodes.forEach(varNode => {
|
|
876
|
+
if (!variableProvenance.includes(varNode.id)) {
|
|
877
|
+
variableProvenance.push(varNode.id);
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
// Create combined term if coefficient is non-zero
|
|
882
|
+
if (totalCoeff !== 0) {
|
|
883
|
+
const firstTerm = termGroup[0];
|
|
884
|
+
const newMonomial = utils.createMonomialWithGranularProvenance(
|
|
885
|
+
totalCoeff,
|
|
886
|
+
firstTerm.monomialInfo.variable,
|
|
887
|
+
firstTerm.monomialInfo.power,
|
|
888
|
+
node.getFontSize(),
|
|
889
|
+
coefficientProvenance,
|
|
890
|
+
variableProvenance
|
|
891
|
+
);
|
|
892
|
+
// Find the leftmost index of any like term in the original terms array
|
|
893
|
+
const leftmostIdx = originalTerms.findIndex(t => likeTermIds.includes(t.node.id));
|
|
894
|
+
// Remove all like terms from allNewTerms (by node id)
|
|
895
|
+
allNewTerms = allNewTerms.filter(t => !likeTermIds.includes(t.node.id));
|
|
896
|
+
// Insert the new combined term at the leftmost index
|
|
897
|
+
if (leftmostIdx !== -1) {
|
|
898
|
+
allNewTerms.splice(leftmostIdx, 0, { node: newMonomial, sign: 1 });
|
|
899
|
+
} else {
|
|
900
|
+
allNewTerms.push({ node: newMonomial, sign: 1 });
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
// If the sum is zero, just remove all like terms
|
|
904
|
+
allNewTerms = allNewTerms.filter(t => !likeTermIds.includes(t.node.id));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Handle zero case
|
|
910
|
+
if (allNewTerms.length === 0) {
|
|
911
|
+
return SimplificationEngine.createConstant(0, node.getFontSize());
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return utils.buildSumTree(allNewTerms, node.getFontSize());
|
|
915
|
+
},
|
|
916
|
+
(originalNode, ruleData, newNode) => {
|
|
917
|
+
const { likeTermGroups } = ruleData;
|
|
918
|
+
const combinations = [];
|
|
919
|
+
|
|
920
|
+
for (const [key, termGroup] of likeTermGroups) {
|
|
921
|
+
if (termGroup.length > 1) {
|
|
922
|
+
let totalCoeff = 0;
|
|
923
|
+
const termDetails = [];
|
|
924
|
+
|
|
925
|
+
for (const termData of termGroup) {
|
|
926
|
+
const coeff = termData.monomialInfo.coefficient * termData.term.sign;
|
|
927
|
+
totalCoeff += coeff;
|
|
928
|
+
|
|
929
|
+
// Format individual terms for the explanation
|
|
930
|
+
const variable = termData.monomialInfo.variable;
|
|
931
|
+
const power = termData.monomialInfo.power;
|
|
932
|
+
const powerStr = power !== 1 ? `^${power}` : '';
|
|
933
|
+
|
|
934
|
+
if (coeff === 1) {
|
|
935
|
+
termDetails.push(`${variable}${powerStr}`);
|
|
936
|
+
} else if (coeff === -1) {
|
|
937
|
+
termDetails.push(`-${variable}${powerStr}`);
|
|
938
|
+
} else {
|
|
939
|
+
termDetails.push(`${coeff}${variable}${powerStr}`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const variable = termGroup[0].monomialInfo.variable;
|
|
944
|
+
const power = termGroup[0].monomialInfo.power;
|
|
945
|
+
const powerStr = power !== 1 ? `^${power}` : '';
|
|
946
|
+
|
|
947
|
+
if (totalCoeff === 0) {
|
|
948
|
+
combinations.push(`${termDetails.join(' + ').replace('+ -', '- ')} = 0 (like terms cancelled)`);
|
|
949
|
+
} else {
|
|
950
|
+
const resultStr = totalCoeff === 1 ? `${variable}${powerStr}` :
|
|
951
|
+
totalCoeff === -1 ? `-${variable}${powerStr}` :
|
|
952
|
+
`${totalCoeff}${variable}${powerStr}`;
|
|
953
|
+
combinations.push(`${termDetails.join(' + ').replace('+ -', '- ')} = ${resultStr}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (combinations.length === 0) {
|
|
959
|
+
return "No like terms were found to combine";
|
|
960
|
+
} else if (combinations.length === 1) {
|
|
961
|
+
return `Combined like terms: ${combinations[0]}`;
|
|
962
|
+
} else {
|
|
963
|
+
return `Combined like terms: ${combinations.join('; ')}`;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
),
|
|
967
|
+
|
|
968
|
+
// Multiply then divide by same factor: (a*x)/a → x or a*(x/a) → x
|
|
969
|
+
SimplificationEngine.createRule("Multiply Divide Same Factor",
|
|
970
|
+
(node) => {
|
|
971
|
+
// Check for (a*x)/a pattern (rational node with multiplication in numerator)
|
|
972
|
+
if (SimplificationEngine.isType(node, 'omdRationalNode')) {
|
|
973
|
+
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
|
|
974
|
+
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
|
|
975
|
+
|
|
976
|
+
if (SimplificationEngine.isBinaryOp(numerator, 'multiply') &&
|
|
977
|
+
SimplificationEngine.isType(denominator, 'omdConstantNode')) {
|
|
978
|
+
|
|
979
|
+
const constOp = SimplificationEngine.hasConstantOperand(numerator);
|
|
980
|
+
if (constOp && constOp.constant.getValue() === denominator.getValue()) {
|
|
981
|
+
return {
|
|
982
|
+
pattern: 'rational',
|
|
983
|
+
factor: constOp.constant.getValue(),
|
|
984
|
+
expression: constOp.other,
|
|
985
|
+
factorNode: constOp.constant,
|
|
986
|
+
denominatorNode: denominator
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Check for a*(x/a) pattern (multiplication with rational)
|
|
993
|
+
if (SimplificationEngine.isBinaryOp(node, 'multiply')) {
|
|
994
|
+
const constOp = SimplificationEngine.hasConstantOperand(node);
|
|
995
|
+
if (!constOp) return false;
|
|
996
|
+
|
|
997
|
+
const otherNode = constOp.other;
|
|
998
|
+
if (SimplificationEngine.isType(otherNode, 'omdRationalNode')) {
|
|
999
|
+
const denominator = SimplificationEngine.unwrapParentheses(otherNode.denominator);
|
|
1000
|
+
|
|
1001
|
+
if (SimplificationEngine.isType(denominator, 'omdConstantNode') &&
|
|
1002
|
+
constOp.constant.getValue() === denominator.getValue()) {
|
|
1003
|
+
|
|
1004
|
+
return {
|
|
1005
|
+
pattern: 'multiply',
|
|
1006
|
+
factor: constOp.constant.getValue(),
|
|
1007
|
+
expression: SimplificationEngine.unwrapParentheses(otherNode.numerator),
|
|
1008
|
+
factorNode: constOp.constant,
|
|
1009
|
+
denominatorNode: denominator
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return false;
|
|
1016
|
+
},
|
|
1017
|
+
(node, data) => {
|
|
1018
|
+
const { expression, factorNode, denominatorNode } = data;
|
|
1019
|
+
const newNode = expression.clone();
|
|
1020
|
+
|
|
1021
|
+
// Preserve provenance
|
|
1022
|
+
newNode.provenance.push(factorNode.id);
|
|
1023
|
+
newNode.provenance.push(denominatorNode.id);
|
|
1024
|
+
newNode.provenance.push(node.id);
|
|
1025
|
+
|
|
1026
|
+
return newNode;
|
|
1027
|
+
},
|
|
1028
|
+
(originalNode, ruleData, newNode) => {
|
|
1029
|
+
const { pattern, factor } = ruleData;
|
|
1030
|
+
if (pattern === 'rational') {
|
|
1031
|
+
return `Simplified multiplication and division: (${factor} × expression)/${factor} = expression`;
|
|
1032
|
+
} else {
|
|
1033
|
+
return `Simplified multiplication and division: ${factor} × (expression/${factor}) = expression`;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
)
|
|
1037
|
+
];
|