@teachinglab/omd
Version:
omd
476 lines (403 loc) • 20.5 kB
JavaScript
import { SimplificationEngine } from '../omdSimplificationEngine.js';
import * as utils from '../simplificationUtils.js';
// ===== RATIONAL NODE RULES =====
export const rationalRules = [
// Simplify x/x = 1 (variable divided by itself)
SimplificationEngine.createRule("Variable Self Division",
(node) => {
if (node.type !== 'omdRationalNode') {
return false;
}
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
// Check if both numerator and denominator are the same variable
if (SimplificationEngine.isType(numerator, 'omdVariableNode') &&
SimplificationEngine.isType(denominator, 'omdVariableNode') &&
numerator.name === denominator.name) {
return {
variable: numerator.name,
numeratorNode: numerator,
denominatorNode: denominator
};
}
return false;
},
(node, data) => {
const newNode = SimplificationEngine.createConstant(1, node.getFontSize());
newNode.provenance.push(data.numeratorNode.id);
newNode.provenance.push(data.denominatorNode.id);
newNode.provenance.push(node.id);
return newNode;
},
(originalNode, ruleData, newNode) => {
const { variable } = ruleData;
return `Simplified variable self-division: ${variable}/${variable} = 1`;
}
),
// Simplify x^n/x = x^(n-1) (power divided by base)
SimplificationEngine.createRule("Power Base Division",
(node) => {
if (node.type !== 'omdRationalNode') {
return false;
}
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
// Check if numerator is a power and denominator is the same variable
if (SimplificationEngine.isType(numerator, 'omdPowerNode') &&
SimplificationEngine.isType(denominator, 'omdVariableNode') &&
SimplificationEngine.isType(numerator.base, 'omdVariableNode') &&
numerator.base.name === denominator.name &&
numerator.exponent.isConstant()) {
const exponent = numerator.exponent.getValue();
const newExponent = exponent - 1;
return {
variable: numerator.base.name,
originalExponent: exponent,
newExponent: newExponent,
baseNode: numerator.base,
powerNode: numerator,
denominatorNode: denominator
};
}
return false;
},
(node, data) => {
const { variable, newExponent, baseNode, powerNode, denominatorNode } = data;
const fontSize = node.getFontSize();
let newNode;
if (newExponent === 0) {
// x^1/x = x^0 = 1
newNode = SimplificationEngine.createConstant(1, fontSize);
} else if (newExponent === 1) {
// x^2/x = x^1 = x
newNode = baseNode.clone();
} else {
// x^n/x = x^(n-1)
newNode = utils.createPowerTerm(baseNode.clone(), newExponent, fontSize);
}
// Preserve provenance
newNode.provenance.push(powerNode.id);
newNode.provenance.push(denominatorNode.id);
newNode.provenance.push(node.id);
return newNode;
},
(originalNode, ruleData, newNode) => {
const { variable, originalExponent, newExponent } = ruleData;
if (newExponent === 0) {
return `Simplified power division: ${variable}^${originalExponent}/${variable} = 1`;
} else if (newExponent === 1) {
return `Simplified power division: ${variable}^${originalExponent}/${variable} = ${variable}`;
} else {
return `Simplified power division: ${variable}^${originalExponent}/${variable} = ${variable}^${newExponent}`;
}
}
),
// Simplify cx/x = c (monomial divided by its variable)
SimplificationEngine.createRule("Monomial Variable Division",
(node) => {
if (node.type !== 'omdRationalNode') {
return false;
}
const numerator = SimplificationEngine.unwrapParentheses(node.numerator);
const denominator = SimplificationEngine.unwrapParentheses(node.denominator);
// Check if numerator is a monomial and denominator is the same variable
const monomialInfo = SimplificationEngine.isMonomial(numerator);
if (monomialInfo &&
SimplificationEngine.isType(denominator, 'omdVariableNode') &&
monomialInfo.variable === denominator.name &&
monomialInfo.power === 1) {
return {
coefficient: monomialInfo.coefficient,
variable: monomialInfo.variable,
numeratorNode: numerator,
denominatorNode: denominator
};
}
return false;
},
(node, data) => {
const { coefficient } = data;
const fontSize = node.getFontSize();
const newNode = SimplificationEngine.createConstant(coefficient, fontSize);
newNode.provenance.push(data.numeratorNode.id);
newNode.provenance.push(data.denominatorNode.id);
newNode.provenance.push(node.id);
return newNode;
},
(originalNode, ruleData, newNode) => {
const { coefficient, variable } = ruleData;
return `Simplified monomial division: ${coefficient}${variable}/${variable} = ${coefficient}`;
}
),
// Simplify fractions (e.g., 6/8 → 3/4, 4/2 → 2)
SimplificationEngine.createRule("Simplify Fraction",
(node) => {
if (node.type !== 'omdRationalNode') {
return false;
}
const numerator = node.numerator;
const denominator = node.denominator;
if (!numerator.isConstant() || !denominator.isConstant()) {
return false;
}
const num = numerator.getValue();
const den = denominator.getValue();
if (den === 0) return false;
const gcd = utils.gcd(Math.abs(num), Math.abs(den));
// Check if we can simplify
if (gcd > 1 || den < 0) {
return {
originalNum: num,
originalDen: den,
gcd: gcd,
simplifiedNum: den < 0 ? -num / gcd : num / gcd,
simplifiedDen: Math.abs(den) / gcd
};
}
return false;
},
(node, data) => {
const { simplifiedNum, simplifiedDen } = data;
if (simplifiedDen === 1) {
// Fraction reduces to whole number
const newNode = SimplificationEngine.createConstant(simplifiedNum, node.getFontSize(), node.numerator, node.denominator);
newNode.provenance.push(node.id);
return newNode;
} else {
// Create simplified fraction
const newNode = SimplificationEngine.rational(simplifiedNum, simplifiedDen, node.getFontSize());
_preserveComponentProvenance(newNode, node.numerator, node.denominator);
newNode.provenance.push(node.id);
return newNode;
}
},
(originalNode, ruleData, newNode) => {
const { originalNum, originalDen, simplifiedNum, simplifiedDen, gcd } = ruleData;
if (originalDen < 0 && gcd > 1) {
return `Simplified fraction: ${originalNum}/${originalDen} = ${simplifiedNum}/${simplifiedDen} (corrected sign and reduced by GCD ${gcd})`;
} else if (simplifiedDen === 1) {
return `Simplified fraction to whole number: ${originalNum}/${originalDen} = ${simplifiedNum}`;
} else if (gcd > 1) {
return `Simplified fraction: ${originalNum}/${originalDen} = ${simplifiedNum}/${simplifiedDen} (reduced by GCD ${gcd})`;
} else {
return `Corrected fraction sign: ${originalNum}/${originalDen} = ${simplifiedNum}/${simplifiedDen}`;
}
}
),
// Multiplication in numerator over constant denominator ((4x)/3 → 4/3*x)
SimplificationEngine.createRule("Simplify Multiplication in Rational",
(node) => {
let numerator = node.numerator;
if (!node.denominator.isConstant()) return false;
const denominator = node.denominator.getValue();
if (denominator === 0) return false;
// Unwrap parentheses and check for multiplication
numerator = SimplificationEngine.unwrapParentheses(numerator);
if (!SimplificationEngine.isBinaryOp(numerator, 'multiply')) return false;
const constOp = SimplificationEngine.hasConstantOperand(numerator);
if (!constOp) return false;
const numeratorCoeff = constOp.constant.getValue();
return {
numeratorCoeff: numeratorCoeff,
denominator: denominator,
expression: constOp.other
};
},
(node, data) => {
const { numeratorCoeff, denominator, expression } = data;
// Create rational coefficient: numeratorCoeff/denominator
const rationalCoeff = SimplificationEngine.rational(numeratorCoeff, denominator, node.getFontSize());
// Preserve lineage from the multiplication components
_preserveMultiplicationProvenance(rationalCoeff, node);
// Create multiplication: (numeratorCoeff/denominator) * expression
const newNode = SimplificationEngine.createBinaryOp(rationalCoeff, 'multiply', expression.clone(), node.getFontSize());
// The new expression node inherits from its direct components.
if (newNode.right) {
newNode.right.provenance.push(expression.id);
}
// Preserve lineage from the overall rational node
newNode.provenance.push(node.id);
return newNode;
},
(originalNode, ruleData, newNode) => {
const { numeratorCoeff, denominator } = ruleData;
const commonDivisor = utils.gcd(Math.abs(numeratorCoeff), Math.abs(denominator));
if (commonDivisor === 1) {
// This case is more about restructuring (e.g., (2x)/3 -> 2/3 * x), not cancellation.
return `Separated the coefficient from the variable, changing the fraction into a multiplication.`;
}
const simplifiedNum = numeratorCoeff / commonDivisor;
const simplifiedDen = denominator / commonDivisor;
let message = `The numerator and denominator share a common factor of ${commonDivisor}. `;
message += `Dividing both by ${commonDivisor} (${numeratorCoeff} ÷ ${commonDivisor} = ${simplifiedNum}, and ${denominator} ÷ ${commonDivisor} = ${simplifiedDen}) simplifies the fraction.`;
return message;
},
'rational'
),
// Distribute division over addition/subtraction ((4*x+6)/3 → 4/3*x + 2)
SimplificationEngine.createRule("Simplify Rational Division",
(node) => {
if (node.type !== 'omdRationalNode') {
return false;
}
let numerator = node.numerator;
const denominator = node.denominator;
if (!denominator.isConstant()) {
return false;
}
// Unwrap parentheses to check the underlying structure
const originalNumerator = numerator;
numerator = SimplificationEngine.unwrapParentheses(numerator);
if (!SimplificationEngine.isBinaryOp(numerator, 'add') &&
!SimplificationEngine.isBinaryOp(numerator, 'subtract')) {
return false;
}
// Flatten the sum in the numerator
const terms = [];
utils.flattenSum(numerator, terms);
return {
terms: terms,
denominator: denominator.getValue()
};
},
(node, data) => {
const { terms, denominator } = data;
const fontSize = node.getFontSize();
const distributedTerms = terms.map(term => {
const termValue = term.node.isConstant() ? term.node.getValue() : 1;
const numeratorValue = termValue * term.sign;
if (term.node.isConstant()) {
// For constants, create a simplified fraction or integer
const gcd = utils.gcd(Math.abs(numeratorValue), Math.abs(denominator));
const simplifiedNum = numeratorValue / gcd;
const simplifiedDen = denominator / gcd;
if (simplifiedDen === 1) {
const constantNode = SimplificationEngine.createConstant(simplifiedNum, fontSize);
_preserveDistributionProvenance(constantNode, term.node, node.denominator);
return { node: constantNode, sign: 1 };
} else {
const rationalNode = SimplificationEngine.rational(Math.abs(simplifiedNum), simplifiedDen, fontSize);
_preserveDistributionProvenance(rationalNode, term.node, node.denominator);
return { node: rationalNode, sign: simplifiedNum >= 0 ? 1 : -1 };
}
} else {
// For non-constants, create coefficient * term / denominator
if (numeratorValue === denominator) {
// Coefficient cancels with denominator
const newNode = term.node.clone();
newNode.provenance.push(term.node.id);
return { node: newNode, sign: 1 };
} else {
const rationalCoeff = SimplificationEngine.rational(Math.abs(numeratorValue), denominator, fontSize);
const multiplicationNode = SimplificationEngine.createBinaryOp(rationalCoeff, 'multiply', term.node.clone(), fontSize);
_preserveDistributionProvenance(rationalCoeff, term.node, node.denominator);
multiplicationNode.provenance.push(node.id);
return { node: multiplicationNode, sign: numeratorValue >= 0 ? 1 : -1 };
}
}
});
return utils.buildSumTree(distributedTerms, fontSize);
},
(originalNode, ruleData, newNode) => {
const { terms, denominator } = ruleData;
const numeratorStr = utils.nodeToString(originalNode.numerator);
const simplifiedTerms = terms.map(term => {
const coeff = (term.node.isConstant() ? term.node.getValue() : 1) * term.sign;
const gcd = utils.gcd(Math.abs(coeff), Math.abs(denominator));
const newNum = coeff / gcd;
const newDen = denominator / gcd;
const variablePart = term.node.isConstant() ? '' : utils.nodeToString(term.node);
if (newDen === 1) return `${newNum}${variablePart}`;
return `(${newNum}/${newDen})${variablePart}`;
}).join(' + ');
// Don't add extra parentheses if numerator already has them
const displayNumerator = numeratorStr.startsWith('(') && numeratorStr.endsWith(')') ?
numeratorStr : `(${numeratorStr})`;
return `Distributed division: ${displayNumerator}/${denominator} = ${simplifiedTerms}`;
},
'rational'
)
];
// ===== HELPER FUNCTIONS FOR PROVENANCE =====
// These helper functions consolidate the repetitive provenance logic
/**
* Preserves provenance from numerator and denominator components
*/
function _preserveComponentProvenance(newNode, numerator, denominator) {
if (numerator?.provenance) {
numerator.provenance.forEach(id => {
if (!newNode.provenance.includes(id)) newNode.provenance.push(id);
});
} else if (numerator) {
newNode.provenance.push(numerator.id);
}
if (denominator?.provenance) {
denominator.provenance.forEach(id => {
if (!newNode.provenance.includes(id)) newNode.provenance.push(id);
});
} else if (denominator) {
newNode.provenance.push(denominator.id);
}
}
/**
* Preserves provenance from multiplication components in rational nodes
*/
function _preserveMultiplicationProvenance(rationalCoeff, originalNode) {
// Handle numerator constant provenance
const numeratorConstant = originalNode.numerator?.left?.isConstant() ?
originalNode.numerator.left : originalNode.numerator?.right;
if (numeratorConstant?.provenance) {
numeratorConstant.provenance.forEach(id => {
if (!rationalCoeff.provenance.includes(id)) {
rationalCoeff.provenance.push(id);
}
});
} else if (numeratorConstant) {
rationalCoeff.provenance.push(numeratorConstant.id);
}
// Handle denominator provenance
if (originalNode.denominator?.provenance) {
originalNode.denominator.provenance.forEach(id => {
if (!rationalCoeff.provenance.includes(id)) {
rationalCoeff.provenance.push(id);
}
});
} else if (originalNode.denominator) {
rationalCoeff.provenance.push(originalNode.denominator.id);
}
}
/**
* Preserves provenance for distributed rational terms
*/
function _preserveDistributionProvenance(rationalNode, termNode, denominatorNode) {
// Numerator should preserve lineage from the original term
if (rationalNode.numerator && termNode) {
rationalNode.numerator.provenance = [];
if (termNode.provenance?.length > 0) {
termNode.provenance.forEach(id => {
rationalNode.numerator.provenance.push(id);
});
} else {
rationalNode.numerator.provenance.push(termNode.id);
}
}
// Denominator should preserve lineage from the original denominator
if (rationalNode.denominator && denominatorNode) {
rationalNode.denominator.provenance = [];
if (denominatorNode.provenance?.length > 0) {
denominatorNode.provenance.forEach(id => {
rationalNode.denominator.provenance.push(id);
});
} else {
rationalNode.denominator.provenance.push(denominatorNode.id);
}
}
// Preserve term's provenance on the rational node itself
if (termNode.provenance?.length > 0) {
termNode.provenance.forEach(id => {
if (!rationalNode.provenance.includes(id)) {
rationalNode.provenance.push(id);
}
});
}
}