@teachinglab/omd
Version:
omd
1,056 lines (919 loc) • 39.3 kB
JavaScript
import { omdConstantNode } from "../nodes/omdConstantNode.js";
import { omdBinaryExpressionNode } from "../nodes/omdBinaryExpressionNode.js";
import { omdNode } from "../nodes/omdNode.js";
import { omdRationalNode } from "../nodes/omdRationalNode.js";
import { omdUnaryExpressionNode } from "../nodes/omdUnaryExpressionNode.js";
import { omdPowerNode } from "../nodes/omdPowerNode.js";
import { omdVariableNode } from "../nodes/omdVariableNode.js";
import { SimplificationEngine } from "./omdSimplificationEngine.js";
import { getMultiplicationSymbol } from '../config/omdConfigManager.js';
// ===== MANUAL PROVENANCE HELPER FUNCTIONS =====
/**
* Manually applies provenance from source nodes to a target node
* @param {omdNode} targetNode - The node to apply provenance to
* @param {...omdNode} sourceNodes - The source nodes to collect provenance from
* @returns {omdNode} The target node with applied provenance
*/
export function applyProvenance(targetNode, ...sourceNodes) {
if (!targetNode) return targetNode;
targetNode.provenance = targetNode.provenance || [];
for (const sourceNode of sourceNodes) {
if (!sourceNode) continue;
// Add the source node's ID
if (sourceNode.id && !targetNode.provenance.includes(sourceNode.id)) {
targetNode.provenance.push(sourceNode.id);
}
// Add all of the source node's provenance
if (sourceNode.provenance && Array.isArray(sourceNode.provenance)) {
sourceNode.provenance.forEach(id => {
if (id && !targetNode.provenance.includes(id)) {
targetNode.provenance.push(id);
}
});
}
// Recursively collect from children
collectChildIds(sourceNode, targetNode.provenance);
}
return targetNode;
}
/**
* Recursively collects IDs from child nodes
* @param {omdNode} node - The node to collect from
* @param {Array} idSet - The array to add IDs to
*/
function collectChildIds(node, idSet) {
if (!node || !idSet) return;
// Visit common child properties
const childProps = ['left', 'right', 'base', 'exponent', 'argument', 'expression',
'numerator', 'denominator', 'content'];
for (const prop of childProps) {
const child = node[prop];
if (child && child.id && !idSet.includes(child.id)) {
idSet.push(child.id);
// Recursively collect from grandchildren
collectChildIds(child, idSet);
}
}
// Handle array properties like args
if (node.args && Array.isArray(node.args)) {
node.args.forEach(arg => {
if (arg && arg.id && !idSet.includes(arg.id)) {
idSet.push(arg.id);
collectChildIds(arg, idSet);
}
});
}
}
/**
* Calculates the greatest common divisor of two numbers.
* @param {number} a
* @param {number} b
* @returns {number} The GCD of a and b.
*/
export function gcd(a, b) {
a = Math.abs(a);
b = Math.abs(b);
while(b) {
[a, b] = [b, a % b];
}
return a;
}
/**
* Applies a mathematical operation to two numbers.
* @param {string} op - The operator ('add', 'subtract', 'multiply', 'divide', or symbolic representation).
* @param {number} left - The left operand.
* @param {number} right - The right operand.
* @returns {number|null} The result of the operation, or null if the operation is invalid (e.g., division by zero).
*/
export function _applyOperator(op, left, right) {
switch (op) {
case 'add':
return left + right;
case 'subtract':
return left - right;
case 'multiply':
return left * right;
case 'divide':
return right !== 0 ? left / right : null;
default:
return null;
}
}
/**
* Creates a new, fully initialized `omdConstantNode`.
* @param {number} value - The numerical value for the constant.
* @param {number} fontSize - The font size to render the node with.
* @returns {omdConstantNode} The newly created constant node.
*/
export function createConstantNode(value, fontSize) {
const newConstantAST = { type: 'ConstantNode', value: value, clone: function () { return { ...this }; } };
const newConstantNode = new omdConstantNode(newConstantAST);
newConstantNode.setFontSize(fontSize);
newConstantNode.initialize();
return newConstantNode;
}
/**
* Helper to safely replace an old node with a new node in the tree.
* Handles both root node replacement and child node replacement.
* @param {omdNode} oldNode - The node to be replaced.
* @param {omdNode} newNode - The new node to insert.
* @param {omdNode} currentRoot - The current root of the entire expression tree.
* @returns {{success: boolean, newRoot: omdNode}} The result of the operation.
*/
export function _replaceNodeInTree(oldNode, newNode, currentRoot) {
if (oldNode === currentRoot) {
return { success: true, newRoot: newNode };
}
// This ensures layout updates are triggered during the replacement itself to maintain visual consistency.
const success = oldNode.replaceWith(newNode, { updateLayout: true });
// Update the astNodeData for the new node and propagate changes upwards
if (success) {
newNode.astNodeData = newNode.toMathJSNode();
let current = newNode.parent;
while (current) {
// Only update astNodeData for actual omdNodes
if (current instanceof omdNode) {
current.astNodeData = current.toMathJSNode();
} else {
// Stop traversing if we encounter a non-omdNode parent (like jsvgContainer)
break;
}
current = current.parent;
}
}
return { success: success, newRoot: currentRoot };
}
/**
* Extracts all leaf nodes (constants and variables) from an expression tree
* for granular provenance tracking
*/
export function extractLeafNodes(node) {
const leafNodes = [];
function traverse(n) {
if (!n) return;
// If it's a leaf node (constant or variable), add it
if (n.type === 'omdConstantNode' ||
n.type === 'omdVariableNode') {
leafNodes.push(n);
return;
}
// Recursively traverse child nodes
if (n.left) traverse(n.left);
if (n.right) traverse(n.right);
if (n.base) traverse(n.base);
if (n.exponent) traverse(n.exponent);
if (n.argument) traverse(n.argument);
if (n.expression) traverse(n.expression);
if (n.numerator) traverse(n.numerator);
if (n.denominator) traverse(n.denominator);
}
traverse(node);
return leafNodes;
}
/**
* Recursively flattens a tree of additions and subtractions into a single list of terms.
* Each term is an object containing the node and its sign (1 for addition, -1 for subtraction).
* For example, `a - (b + c)` becomes `[{node: a, sign: 1}, {node: b, sign: -1}, {node: c, sign: -1}]`.
* @param {omdNode} node - The current node in the expression tree to process.
* @param {Array<Object>} terms - An array to accumulate the flattened terms. This array is modified by the function.
*/
export function flattenSum(node, terms) {
const op = node.operation;
if (node.type === 'omdBinaryExpressionNode' && (op === 'add' || op === '+')) {
flattenSum(node.left, terms);
flattenSum(node.right, terms);
} else if (node.type === 'omdBinaryExpressionNode' && (op === 'subtract' || op === '-')) {
flattenSum(node.left, terms);
const rightTerms = [];
flattenSum(node.right, rightTerms);
rightTerms.forEach(t => { t.sign *= -1; }); // Invert the sign for all terms on the right of a minus.
terms.push(...rightTerms);
} else {
// This is a leaf node in the sum (could be a variable, a multiplication, etc.)
// Extract leaf nodes for granular provenance tracking
const leafNodes = extractLeafNodes(node);
terms.push({
node: node,
sign: 1,
leafNodes: leafNodes // Add leaf nodes for provenance
});
}
}
export function buildSumTree(terms, fontSize) {
if (terms.length === 0) return createConstantNode(0, fontSize);
// Sort terms to handle subtractions gracefully
terms.sort((a, b) => b.sign - a.sign);
// If the expression starts with a negative term (e.g., -a + b), we prepend a '0'
if (terms.length > 1 && terms[0].sign === -1) {
if (terms[0].node.type === 'omdConstantNode') {
const first = terms.shift();
first.sign = 1;
terms.push(first);
} else {
terms.unshift({node: createConstantNode(0, fontSize), sign: 1});
}
}
// Build the tree from left to right
let firstTerm = terms.shift();
let currentTree = firstTerm.node;
// Ensure the first node is properly formed - clone it if needed
if (!currentTree || typeof currentTree.updateLayoutUpwards !== 'function') {
const originalId = currentTree.id;
currentTree = currentTree.clone();
currentTree.provenance = currentTree.provenance || [];
if (originalId && !currentTree.provenance.includes(originalId)) {
currentTree.provenance.push(originalId);
}
currentTree.setFontSize(fontSize);
currentTree.initialize();
}
// If there's only one term, return it directly (it should already have correct provenance)
if (terms.length === 0) {
return currentTree;
}
while (terms.length > 0) {
const term = terms.shift();
const opSymbol = term.sign === 1 ? '+' : '-';
const opFn = term.sign === 1 ? 'add' : 'subtract';
let termNode = term.node;
if (!termNode || typeof termNode.updateLayoutUpwards !== 'function') {
const originalId = termNode.id;
termNode = termNode.clone();
termNode.provenance = termNode.provenance || [];
if (originalId && !termNode.provenance.includes(originalId)) {
termNode.provenance.push(originalId);
}
termNode.setFontSize(fontSize);
termNode.initialize();
}
// Preserve expansion provenance metadata if it exists
if (term.expansionProvenance) {
// Add expansion metadata to the term node for later reference
termNode.expansionProvenance = termNode.expansionProvenance || [];
termNode.expansionProvenance.push(term.expansionProvenance);
// Ensure all expansion source IDs are in the term's provenance
[term.expansionProvenance.leftSource, term.expansionProvenance.rightSource, term.expansionProvenance.originalMultiplication].forEach(id => {
if (id && !termNode.provenance.includes(id)) {
termNode.provenance.push(id);
}
});
}
// Create new binary expression
const newAST = {
type: 'OperatorNode',
op: opSymbol,
fn: opFn,
args: [currentTree.toMathJSNode(), termNode.toMathJSNode()],
clone: function () { return { ...this, args: this.args.map(arg => arg.clone()) }; }
};
const newNode = new omdBinaryExpressionNode(newAST);
newNode.setFontSize(fontSize);
newNode.initialize();
// Binary operation nodes should inherit provenance from their operands
// This allows tracking which terms contributed to the final expression
const leftProvenance = currentTree.provenance || [];
const rightProvenance = termNode.provenance || [];
// Add provenance from both left and right operands
[...leftProvenance, ...rightProvenance, currentTree.id, termNode.id].forEach(id => {
if (id && !newNode.provenance.includes(id)) {
newNode.provenance.push(id);
}
});
// Preserve expansion provenance metadata in the binary expression
if (termNode.expansionProvenance || currentTree.expansionProvenance) {
newNode.expansionProvenance = newNode.expansionProvenance || [];
if (termNode.expansionProvenance) {
newNode.expansionProvenance.push(...termNode.expansionProvenance);
}
if (currentTree.expansionProvenance) {
newNode.expansionProvenance.push(...currentTree.expansionProvenance);
}
}
currentTree = newNode;
}
return currentTree;
}
/**
* Creates a rational node (fraction) safely with proper initialization
* @param {number} numerator - The numerator value
* @param {number} denominator - The denominator value
* @param {number} fontSize - The font size for the node
* @returns {omdNode} A properly initialized rational or constant node
*/
export function createRationalNode(numerator, denominator, fontSize) {
// If denominator is 1, just return a constant
if (denominator === 1) {
return createConstantNode(numerator, fontSize);
}
// Create AST for the rational node
const ast = {
type: 'OperatorNode',
op: '/',
fn: 'divide',
args: [
{ type: 'ConstantNode', value: Math.abs(numerator), clone: function() { return {...this}; } },
{ type: 'ConstantNode', value: denominator, clone: function() { return {...this}; } }
],
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
};
let node = new omdRationalNode(ast);
// Handle negative fractions by wrapping in unary minus
if (numerator < 0) {
const unaryAST = {
type: 'OperatorNode',
op: '-',
fn: 'unaryMinus',
args: [node.toMathJSNode()],
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
};
node = new omdUnaryExpressionNode(unaryAST);
}
node.setFontSize(fontSize);
node.initialize();
return node;
}
/**
* Converts any omdNode into a human-readable string representation.
* @param {omdNode} node - The node to convert.
* @returns {string} A string representation of the node.
*/
export function nodeToString(node) {
if (!node) return '';
// Operation name to symbol mapping
const operationSymbols = {
'add': '+',
'subtract': '-',
'multiply': getMultiplicationSymbol(),
'divide': '÷',
'unaryMinus': '-',
'unaryPlus': '+',
'pow': '^'
};
// Check for a getValue method (constants) - but only if actually constant
if (typeof node.getValue === 'function' && node.isConstant && node.isConstant()) {
return node.getValue().toString();
}
// Check for a name property (variables)
if (node.name) {
return node.name;
}
// Handle binary expressions recursively
if (node.type === 'omdBinaryExpressionNode') {
const left = nodeToString(node.left);
const right = nodeToString(node.right);
// Handle cases where node.op might be null (implicit multiplication)
let op;
if (node.op && node.op.opName) {
op = operationSymbols[node.op.opName] || node.op.opName;
} else if (node.operation) {
op = operationSymbols[node.operation] || node.operation;
} else {
// For implicit multiplication, use empty string
op = ''; // This handles cases like "2x" where it's implicit multiplication
}
// Only add parentheses for explicit operations, not implicit multiplication
return op ? `${left} ${op} ${right}` : `${left}${right}`;
}
// Handle unary expressions
if (node.type === 'omdUnaryExpressionNode') {
const arg = nodeToString(node.argument);
let op;
if (node.op && node.op.opName) {
op = operationSymbols[node.op.opName] || node.op.opName;
} else if (node.operation) {
op = operationSymbols[node.operation] || node.operation;
} else {
op = '-'; // Default for unary
}
// Only add parentheses around the argument if it's a complex expression
if (arg.includes(' ') || arg.includes('+') || arg.includes('-') || arg.includes(getMultiplicationSymbol()) || arg.includes('÷')) {
return `${op}(${arg})`;
}
return `${op}${arg}`;
}
// Handle rational nodes
if (node.type === 'omdRationalNode') {
const num = nodeToString(node.numerator);
const den = nodeToString(node.denominator);
return `(${num}/${den})`;
}
// Handle parenthesis nodes
if (node.type === 'omdParenthesisNode') {
const content = nodeToString(node.content || node.expression);
// Only add parentheses if the content doesn't already start and end with them
if (content.startsWith('(') && content.endsWith(')')) {
return content;
}
return `(${content})`;
}
// Handle power nodes
if (node.type === 'omdPowerNode') {
const base = nodeToString(node.base);
const exp = nodeToString(node.exponent);
return `${base}^${exp}`;
}
// Handle sqrt nodes
if (node.type === 'omdSqrtNode') {
const arg = nodeToString(node.argument);
return `√(${arg})`;
}
// Handle function nodes
if (node.type === 'omdFunctionNode') {
const functionName = node.functionName || node.name || 'f';
if (node.argNodes && node.argNodes.length > 0) {
const args = node.argNodes.map(arg => nodeToString(arg)).join(', ');
return `${functionName}(${args})`;
} else if (node.args && node.args.length > 0) {
const args = node.args.map(arg => nodeToString(arg)).join(', ');
return `${functionName}(${args})`;
}
return `${functionName}()`;
}
// Handle equation nodes
if (node.type === 'omdEquationNode') {
const left = nodeToString(node.left);
const right = nodeToString(node.right);
return `${left} = ${right}`;
}
// Fallback for other node types - try toString method first
if (typeof node.toString === 'function') {
try {
return node.toString();
} catch (e) {
// Continue to next fallback
}
}
// Use math.js as final fallback
if (node.toMathJSNode) {
try {
return math.parse(node.toMathJSNode()).toString();
} catch (e) {
// Continue to final fallback
}
}
// Use node name if available, otherwise use constructor name as last resort
return node.name || node.type || '[unknown]';
}
// ===== POLYNOMIAL EXPANSION HELPER FUNCTIONS =====
/**
* Expands a polynomial power using multinomial expansion
* For example: (a + b)^2 = a^2 + 2ab + b^2
* @param {Array} terms - Array of {node, sign} objects representing the polynomial terms
* @param {number} exponent - The power to expand to
* @param {number} fontSize - Font size for new nodes
* @returns {Array} Array of {node, sign} objects representing the expanded terms
*/
export function expandPolynomialPower(terms, exponent, fontSize) {
if (exponent === 1) {
return terms;
}
if (exponent === 2) {
return expandBinomialSquare(terms, fontSize);
}
if (exponent === 3) {
return expandBinomialCube(terms, fontSize);
}
if (exponent === 4) {
return expandBinomialFourth(terms, fontSize);
}
// For higher powers, use recursive multiplication
let result = terms;
for (let i = 1; i < exponent; i++) {
result = multiplyTermArrays(result, terms, fontSize);
}
return result;
}
/**
* Expands (a + b + ...)^2 using the multinomial theorem
*/
export function expandBinomialSquare(terms, fontSize) {
const expandedTerms = [];
// Square terms: a^2, b^2, ...
for (const term of terms) {
const squaredNode = SimplificationEngine.createBinaryOp(
term.node.clone(),
'multiply',
term.node.clone(),
fontSize
);
// Use granular provenance from leaf nodes
const leafNodes = term.leafNodes || [];
leafNodes.forEach(leafNode => {
[squaredNode.left, squaredNode.right, squaredNode].forEach(part => {
if (part && !part.provenance.includes(leafNode.id)) {
part.provenance.push(leafNode.id);
}
});
});
expandedTerms.push({
node: squaredNode,
sign: term.sign * term.sign // Always positive since we're squaring
});
}
// Cross terms: 2ab, 2ac, 2bc, ...
for (let i = 0; i < terms.length; i++) {
for (let j = i + 1; j < terms.length; j++) {
const term1 = terms[i];
const term2 = terms[j];
// Create 2 * term1 * term2
const coefficient2 = SimplificationEngine.createConstant(2, fontSize);
const product1 = SimplificationEngine.createBinaryOp(
coefficient2,
'multiply',
term1.node.clone(),
fontSize
);
const finalProduct = SimplificationEngine.createBinaryOp(
product1,
'multiply',
term2.node.clone(),
fontSize
);
// Use granular provenance from both terms
const allLeafNodes = [...(term1.leafNodes || []), ...(term2.leafNodes || [])];
allLeafNodes.forEach(leafNode => {
[product1.right, finalProduct.right, finalProduct].forEach(part => {
if (part && !part.provenance.includes(leafNode.id)) {
part.provenance.push(leafNode.id);
}
});
});
expandedTerms.push({
node: finalProduct,
sign: term1.sign * term2.sign
});
}
}
return expandedTerms;
}
/**
* Expands (a + b + ...)^3 for binomial/trinomial cases
*/
export function expandBinomialCube(terms, fontSize) {
if (terms.length === 2) {
const [term1, term2] = terms;
const expandedTerms = [];
// Use the efficient helper functions with provenance
expandedTerms.push({
node: createPowerTermWithProvenance(term1.node, 3, fontSize, term1.leafNodes),
sign: Math.pow(term1.sign, 3)
});
expandedTerms.push({
node: createCoefficientProductTermWithProvenance(3, term1.node, 2, term2.node, 1, fontSize, term1.leafNodes, term2.leafNodes),
sign: Math.pow(term1.sign, 2) * term2.sign
});
expandedTerms.push({
node: createCoefficientProductTermWithProvenance(3, term1.node, 1, term2.node, 2, fontSize, term1.leafNodes, term2.leafNodes),
sign: term1.sign * Math.pow(term2.sign, 2)
});
expandedTerms.push({
node: createPowerTermWithProvenance(term2.node, 3, fontSize, term2.leafNodes),
sign: Math.pow(term2.sign, 3)
});
return expandedTerms;
}
// For more than 2 terms, use general multiplication
return multiplyTermArrays(expandBinomialSquare(terms, fontSize), terms, fontSize);
}
/**
* Expands (a + b)^4 = a^4 + 4a^3b + 6a^2b^2 + 4ab^3 + b^4
*/
export function expandBinomialFourth(terms, fontSize) {
if (terms.length === 2) {
const [term1, term2] = terms;
const expandedTerms = [];
// Use the efficient helper functions with provenance
expandedTerms.push({
node: createPowerTermWithProvenance(term1.node, 4, fontSize, term1.leafNodes),
sign: Math.pow(term1.sign, 4)
});
expandedTerms.push({
node: createCoefficientProductTermWithProvenance(4, term1.node, 3, term2.node, 1, fontSize, term1.leafNodes, term2.leafNodes),
sign: Math.pow(term1.sign, 3) * term2.sign
});
expandedTerms.push({
node: createCoefficientProductTermWithProvenance(6, term1.node, 2, term2.node, 2, fontSize, term1.leafNodes, term2.leafNodes),
sign: Math.pow(term1.sign, 2) * Math.pow(term2.sign, 2)
});
expandedTerms.push({
node: createCoefficientProductTermWithProvenance(4, term1.node, 1, term2.node, 3, fontSize, term1.leafNodes, term2.leafNodes),
sign: term1.sign * Math.pow(term2.sign, 3)
});
expandedTerms.push({
node: createPowerTermWithProvenance(term2.node, 4, fontSize, term2.leafNodes),
sign: Math.pow(term2.sign, 4)
});
return expandedTerms;
}
// For more than 2 terms, use general multiplication
const cubed = expandBinomialCube(terms, fontSize);
return multiplyTermArrays(cubed, terms, fontSize);
}
/**
* Creates a term like x^n with granular provenance tracking
*/
export function createPowerTermWithProvenance(baseNode, power, fontSize, leafNodes) {
if (power === 1) {
const cloned = baseNode.clone();
// Add leaf node provenance
if (leafNodes && leafNodes.length > 0) {
leafNodes.forEach(leafNode => {
if (!cloned.provenance.includes(leafNode.id)) {
cloned.provenance.push(leafNode.id);
}
});
}
return cloned;
}
const powerConstant = SimplificationEngine.createConstant(power, fontSize);
// Create power node AST structure
const powerAST = {
type: 'OperatorNode',
op: '^',
fn: 'pow',
args: [baseNode.toMathJSNode(), powerConstant.toMathJSNode()],
clone: function() {
return {
...this,
args: this.args.map(arg => arg.clone())
};
}
};
const powerNode = new omdPowerNode(powerAST);
powerNode.setFontSize(fontSize);
powerNode.initialize();
// Add leaf node provenance
if (leafNodes && leafNodes.length > 0) {
leafNodes.forEach(leafNode => {
if (!powerNode.base.provenance.includes(leafNode.id)) {
powerNode.base.provenance.push(leafNode.id);
}
if (!powerNode.provenance.includes(leafNode.id)) {
powerNode.provenance.push(leafNode.id);
}
});
} else {
powerNode.base.provenance.push(baseNode.id);
powerNode.provenance.push(baseNode.id);
}
return powerNode;
}
/**
* Creates a term like c * a^p1 * b^p2 with granular provenance tracking
*/
export function createCoefficientProductTermWithProvenance(coefficient, node1, power1, node2, power2, fontSize, leafNodes1, leafNodes2) {
let result = SimplificationEngine.createConstant(coefficient, fontSize);
// Multiply by node1^power1
if (power1 > 0) {
const term1 = createPowerTermWithProvenance(node1, power1, fontSize, leafNodes1);
result = SimplificationEngine.createBinaryOp(result, 'multiply', term1, fontSize);
// Add leaf node provenance to the result
if (leafNodes1 && leafNodes1.length > 0) {
leafNodes1.forEach(leafNode => {
if (!result.provenance.includes(leafNode.id)) {
result.provenance.push(leafNode.id);
}
});
}
}
// Multiply by node2^power2
if (power2 > 0) {
const term2 = createPowerTermWithProvenance(node2, power2, fontSize, leafNodes2);
result = SimplificationEngine.createBinaryOp(result, 'multiply', term2, fontSize);
// Add leaf node provenance to the result
if (leafNodes2 && leafNodes2.length > 0) {
leafNodes2.forEach(leafNode => {
if (!result.provenance.includes(leafNode.id)) {
result.provenance.push(leafNode.id);
}
});
}
}
return result;
}
/**
* Creates a term like x^n
*/
export function createPowerTerm(baseNode, power, fontSize) {
if (power === 1) {
return baseNode.clone();
}
const powerConstant = SimplificationEngine.createConstant(power, fontSize);
// Create power node AST structure
const powerAST = {
type: 'OperatorNode',
op: '^',
fn: 'pow',
args: [baseNode.toMathJSNode(), powerConstant.toMathJSNode()],
clone: function() {
return {
...this,
args: this.args.map(arg => arg.clone())
};
}
};
const powerNode = new omdPowerNode(powerAST);
powerNode.setFontSize(fontSize);
powerNode.initialize();
return powerNode;
}
/**
* Creates a term like c * a^p1 * b^p2
*/
export function createCoefficientProductTerm(coefficient, node1, power1, node2, power2, fontSize) {
let result = SimplificationEngine.createConstant(coefficient, fontSize);
// Multiply by node1^power1
if (power1 > 0) {
const term1 = createPowerTerm(node1, power1, fontSize);
result = SimplificationEngine.createBinaryOp(result, 'multiply', term1, fontSize);
}
// Multiply by node2^power2
if (power2 > 0) {
const term2 = createPowerTerm(node2, power2, fontSize);
result = SimplificationEngine.createBinaryOp(result, 'multiply', term2, fontSize);
}
return result;
}
/**
* Multiplies two arrays of terms (for general polynomial multiplication)
*/
export function multiplyTermArrays(terms1, terms2, fontSize) {
const result = [];
for (const term1 of terms1) {
for (const term2 of terms2) {
const productNode = SimplificationEngine.createBinaryOp(
term1.node.clone(),
'multiply',
term2.node.clone(),
fontSize
);
// Add granular provenance from leaf nodes of both terms
const allLeafNodes = [...(term1.leafNodes || []), ...(term2.leafNodes || [])];
allLeafNodes.forEach(leafNode => {
[productNode.left, productNode.right, productNode].forEach(part => {
if (part && !part.provenance.includes(leafNode.id)) {
part.provenance.push(leafNode.id);
}
});
});
result.push({
node: productNode,
sign: term1.sign * term2.sign,
leafNodes: allLeafNodes
});
}
}
return result;
}
/**
* Creates a monomial with granular provenance tracking for coefficients and variables separately
*/
export function createMonomialWithGranularProvenance(coefficient, variable, power, fontSize, coefficientProvenance = [], variableProvenance = []) {
// Create variable node
const variableAST = { type: 'SymbolNode', name: variable, clone: function() { return {...this}; } };
const variableNode = new (SimplificationEngine.getNodeClass('omdVariableNode'))(variableAST);
variableNode.setFontSize(fontSize);
variableNode.initialize();
// Set variable-specific provenance
if (variableProvenance && variableProvenance.length > 0) {
variableNode.provenance = [...variableProvenance];
}
let termNode = variableNode;
// Add power if not 1
if (power !== 1) {
const powerAST = {
type: 'OperatorNode',
op: '^',
fn: 'pow',
args: [variableAST, { type: 'ConstantNode', value: power, clone: function() { return {...this}; } }],
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
};
termNode = new (SimplificationEngine.getNodeClass('omdPowerNode'))(powerAST);
termNode.setFontSize(fontSize);
termNode.initialize();
// Set variable provenance on the base, power gets no special provenance
if (termNode.base && variableProvenance && variableProvenance.length > 0) {
termNode.base.provenance = [...variableProvenance];
}
}
let result;
if (coefficient === 1) {
result = termNode;
} else if (coefficient === -1) {
// Create unary minus
const unaryAST = {
type: 'OperatorNode',
op: '-',
fn: 'unaryMinus',
args: [termNode.toMathJSNode()],
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
};
result = new (SimplificationEngine.getNodeClass('omdUnaryExpressionNode'))(unaryAST);
result.setFontSize(fontSize);
result.initialize();
// The argument preserves variable provenance
if (result.argument && variableProvenance && variableProvenance.length > 0) {
result.argument.provenance = [...variableProvenance];
}
} else {
// Create coefficient * term
const coeffNode = SimplificationEngine.createConstant(Math.abs(coefficient), fontSize);
// Set coefficient-specific provenance
if (coefficientProvenance && coefficientProvenance.length > 0) {
coeffNode.provenance = [...coefficientProvenance];
}
const multiplicationNode = SimplificationEngine.createBinaryOp(coeffNode, 'multiply', termNode, fontSize);
// Apply granular provenance: coefficient to left, variable to right
if (multiplicationNode.left && coefficientProvenance && coefficientProvenance.length > 0) {
multiplicationNode.left.provenance = [...coefficientProvenance];
}
if (multiplicationNode.right && variableProvenance && variableProvenance.length > 0) {
multiplicationNode.right.provenance = [...variableProvenance];
}
// Wrap in unary minus if coefficient is negative
if (coefficient < 0) {
const unaryAST = {
type: 'OperatorNode',
op: '-',
fn: 'unaryMinus',
args: [multiplicationNode.toMathJSNode()],
clone: function() { return { ...this, args: this.args.map(arg => arg.clone()) }; }
};
result = new (SimplificationEngine.getNodeClass('omdUnaryExpressionNode'))(unaryAST);
result.setFontSize(fontSize);
result.initialize();
// Preserve granular provenance within the unary minus
if (result.argument) {
if (result.argument.left && coefficientProvenance && coefficientProvenance.length > 0) {
result.argument.left.provenance = [...coefficientProvenance];
}
if (result.argument.right && variableProvenance && variableProvenance.length > 0) {
result.argument.right.provenance = [...variableProvenance];
}
}
} else {
result = multiplicationNode;
}
}
return result;
}
/**
* Extracts coefficient and variable leaf nodes separately from a monomial term
* for granular provenance tracking in like term combination
*/
export function extractMonomialProvenance(termNode) {
const coefficientNodes = [];
const variableNodes = [];
function traverse(node, isInCoefficient = false) {
if (!node) return;
// If we find a variable node, it goes to variables
if (node.type === 'omdVariableNode') {
variableNodes.push(node);
return;
}
// If we find a constant node
if (node.type === 'omdConstantNode') {
// If we're in a power expression, this is likely an exponent, not a coefficient
if (node.parent && node.parent.type === 'omdPowerNode' && node.parent.exponent === node) {
// Skip exponents for provenance purposes
return;
}
// Otherwise, it's a coefficient
coefficientNodes.push(node);
return;
}
// For power nodes, traverse base for variables
if (node.type === 'omdPowerNode') {
if (node.base) traverse(node.base, false);
// Don't traverse exponent for provenance
return;
}
// For binary operations, determine what we're looking at
if (node.type === 'omdBinaryExpressionNode') {
if (node.operation === 'multiply') {
// In multiplication, left is often coefficient, right is often variable
if (node.left) traverse(node.left, true);
if (node.right) traverse(node.right, false);
} else {
// For other operations, traverse both sides
if (node.left) traverse(node.left, isInCoefficient);
if (node.right) traverse(node.right, isInCoefficient);
}
return;
}
// For unary operations, traverse the argument
if (node.type === 'omdUnaryExpressionNode') {
if (node.argument) traverse(node.argument, isInCoefficient);
return;
}
// For other node types, try to traverse child properties
['left', 'right', 'base', 'exponent', 'argument', 'expression', 'numerator', 'denominator'].forEach(prop => {
if (node[prop]) traverse(node[prop], isInCoefficient);
});
}
traverse(termNode);
return {
coefficientNodes,
variableNodes
};
}