import {
SystemOfEquations,
SystemSolution
} from '../../types-advanced-math.js';
import { MatrixOperations } from './MatrixOperations.js';
/**
* Solver for systems of linear equations
* Handles parsing, matrix conversion, and solution generation
*/
export class EquationSystemSolver {
private matrixOps: MatrixOperations;
constructor() {
this.matrixOps = new MatrixOperations();
}
/**
* Solves a system of linear equations
* @param equations Array of equation strings (e.g., ["x + y = 10", "2x - y = 5"])
* @returns Solution to the system
*/
solveSystem(equations: string[]): SystemSolution {
try {
// Parse the equations into matrix form
const { coefficientMatrix, constantVector, variables } = this.parseSystemToMatrix(equations);
// Check for special cases
if (variables.length === 0) {
return {
type: 'none',
explanation: 'No variables found in the system of equations.'
};
}
// Apply Gaussian elimination
const { reducedMatrix, rank, hasSolution } = this.matrixOps.gaussianElimination(
coefficientMatrix,
constantVector
);
// Determine solution type
if (!hasSolution) {
return {
type: 'none',
explanation: 'The system has no solution. The equations are inconsistent.'
};
}
if (rank < variables.length) {
// System has infinite solutions
return this.generateParametricSolution(reducedMatrix, variables, rank);
}
// System has a unique solution
return this.generateUniqueSolution(reducedMatrix, variables);
} catch (error) {
// Return error as a "no solution" case with explanation
return {
type: 'none',
explanation: `Error solving system: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Parses a system of equations into matrix form
* @param equations Array of equation strings
* @returns Coefficient matrix, constant vector, and variables
*/
private parseSystemToMatrix(equations: string[]): {
coefficientMatrix: number[][],
constantVector: number[],
variables: string[]
} {
// Extract all variables from the equations
const variableSet = new Set<string>();
for (const equation of equations) {
const matches = equation.match(/[a-zA-Z]+/g) || [];
matches.forEach(match => variableSet.add(match));
}
const variables = Array.from(variableSet).sort();
// Initialize coefficient matrix and constant vector
const coefficientMatrix: number[][] = [];
const constantVector: number[] = [];
// Parse each equation
for (const equation of equations) {
const { variableCoefficients, constant } = this.parseEquation(equation, variables);
coefficientMatrix.push(variableCoefficients);
constantVector.push(constant);
}
return { coefficientMatrix, constantVector, variables };
}
/**
* Parses a single equation into coefficients
* @param equation The equation string
* @param variables Array of variables
* @returns Coefficients for variables and constant term
*/
private parseEquation(equation: string, variables: string[]): {
variableCoefficients: number[],
constant: number
} {
// Split by equals sign
const sides = equation.split('=');
if (sides.length !== 2) {
throw new Error(`Invalid equation format: ${equation}`);
}
const leftSide = sides[0].trim();
const rightSide = sides[1].trim();
// Move everything to the left side
const combined = `${leftSide} - (${rightSide})`;
// Extract coefficients
return this.extractCoefficients(combined, variables);
}
/**
* Extracts coefficients from an expression
* @param expression The expression string
* @param variables Array of variables
* @returns Coefficients for variables and constant term
*/
private extractCoefficients(expression: string, variables: string[]): {
variableCoefficients: number[],
constant: number
} {
// Initialize coefficients with zeros
const variableCoefficients = Array(variables.length).fill(0);
let constant = 0;
// Normalize expression
const normalized = this.normalizeExpression(expression);
// Split into terms
const terms = this.splitIntoTerms(normalized);
// Process each term
for (const term of terms) {
const { coefficient, variable: termVariable } = this.parseTerm(term);
if (termVariable) {
const index = variables.indexOf(termVariable);
if (index !== -1) {
variableCoefficients[index] += coefficient;
} else {
throw new Error(`Unknown variable: ${termVariable}`);
}
} else {
// This is a constant term
constant -= coefficient; // Negative because we moved to left side
}
}
return { variableCoefficients, constant };
}
/**
* Normalizes an expression by handling parentheses and spaces
* @param expression The expression to normalize
* @returns Normalized expression
*/
private normalizeExpression(expression: string): string {
// Remove spaces and handle implicit multiplication
let normalized = expression.replace(/\s+/g, '');
// Handle parentheses with distributive property
while (normalized.includes('(')) {
normalized = this.expandParentheses(normalized);
}
// Add spaces around operators for easier parsing
return normalized
.replace(/\+/g, ' + ')
.replace(/\-/g, ' - ')
.replace(/\s+/g, ' ')
.trim();
}
/**
* Expands parentheses in an expression
* @param expression Expression with parentheses
* @returns Expression with parentheses expanded
*/
private expandParentheses(expression: string): string {
// Find innermost parentheses
const match = expression.match(/([+-]?\d*\.?\d*)\s*\(\s*([^()]+)\s*\)/);
if (!match) return expression;
const [full, factor, inner] = match;
const multiplier = factor ? parseFloat(factor) || (factor === '-' ? -1 : 1) : 1;
// Split inner expression into terms
const innerTerms = inner.match(/[+-]?[^+-]+/g) || [];
const expandedTerms = innerTerms.map(term => {
const termValue = parseFloat(term) || 1;
const hasVariable = /[a-zA-Z]/.test(term);
const variable = hasVariable ? term.match(/[a-zA-Z]+/)?.[0] || '' : '';
const coefficient = hasVariable ? parseFloat(term) || (term.startsWith('-') ? -1 : 1) : termValue;
const newCoefficient = multiplier * coefficient;
if (hasVariable) {
return newCoefficient === 1 ? variable :
newCoefficient === -1 ? `-${variable}` :
`${newCoefficient}${variable}`;
} else {
return newCoefficient.toString();
}
});
const expanded = expandedTerms.join(' + ').replace(/\+ -/g, '- ');
return expression.replace(full, expanded);
}
/**
* Splits a normalized expression into terms
* @param expression The normalized expression
* @returns Array of terms
*/
private splitIntoTerms(expression: string): string[] {
const tokens = expression.split(' ');
const terms: string[] = [];
let currentTerm = '';
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token === '+' || token === '-') {
if (currentTerm) terms.push(currentTerm);
currentTerm = token === '-' ? '-' : '';
} else {
currentTerm += token;
}
}
if (currentTerm) terms.push(currentTerm);
return terms.filter(t => t && t !== '+');
}
/**
* Parses a single term to extract coefficient and variable
* @param term The term to parse
* @returns Coefficient and variable
*/
private parseTerm(term: string): { coefficient: number, variable: string | null } {
// Remove leading + if present
term = term.replace(/^\+/, '');
// Check for variable
const variableMatch = term.match(/[a-zA-Z]+/);
const variable = variableMatch ? variableMatch[0] : null;
if (variable) {
// Extract coefficient
const coefficientStr = term.replace(variable, '').trim();
const coefficient = !coefficientStr ? 1 :
coefficientStr === '-' ? -1 :
parseFloat(coefficientStr);
return { coefficient, variable };
} else {
// Pure number
return { coefficient: parseFloat(term), variable: null };
}
}
/**
* Generates a unique solution from a reduced matrix
* @param reducedMatrix The reduced matrix
* @param variables Array of variable names
* @returns Solution object
*/
private generateUniqueSolution(reducedMatrix: number[][], variables: string[]): SystemSolution {
const solutions = new Map<string, number>();
const numCols = variables.length;
for (let row = 0; row < Math.min(reducedMatrix.length, numCols); row++) {
// Find the leading coefficient
let col = 0;
while (col < numCols && Math.abs(reducedMatrix[row][col]) < 1e-10) {
col++;
}
if (col < numCols) {
solutions.set(variables[col], reducedMatrix[row][numCols]);
}
}
// Build explanation
let explanation = 'The system has a unique solution:\n';
for (const [variable, value] of solutions) {
explanation += `${variable} = ${this.formatNumber(value)}\n`;
}
return {
type: 'unique',
solutions,
explanation
};
}
/**
* Generates a parametric solution for an under-determined system
* @param reducedMatrix The reduced matrix
* @param variables Array of variable names
* @param rank The rank of the matrix
* @returns Solution object
*/
private generateParametricSolution(
reducedMatrix: number[][],
variables: string[],
rank: number
): SystemSolution {
const parameterization = new Map<string, string>();
const numCols = variables.length;
// Determine which variables are free (parameters)
const basicVariables: number[] = [];
const freeVariables: string[] = [];
for (let row = 0; row < rank; row++) {
// Find leading variable in this row
for (let col = 0; col < numCols; col++) {
if (Math.abs(reducedMatrix[row][col]) > 1e-10) {
basicVariables.push(col);
break;
}
}
}
// Free variables are those not in basic variables
for (let col = 0; col < numCols; col++) {
if (!basicVariables.includes(col)) {
freeVariables.push(variables[col]);
parameterization.set(variables[col], variables[col]);
}
}
// Express basic variables in terms of free variables
for (let i = 0; i < basicVariables.length && i < rank; i++) {
const col = basicVariables[i];
const variable = variables[col];
// Start with the constant term
let expression = this.formatNumber(reducedMatrix[i][numCols]);
// Subtract terms for other variables
for (let j = col + 1; j < numCols; j++) {
if (Math.abs(reducedMatrix[i][j]) > 1e-10) {
const coeff = -reducedMatrix[i][j];
const term = this.formatTerm(coeff, variables[j]);
expression += term;
}
}
parameterization.set(variable, expression);
}
// Build explanation
let explanation = 'The system has infinitely many solutions. ';
explanation += `Free variables: ${freeVariables.join(', ')}.\n\n`;
explanation += 'General solution:\n';
for (const [variable, expr] of parameterization) {
explanation += `${variable} = ${expr}\n`;
}
return {
type: 'infinite',
parameterization,
explanation
};
}
/**
* Formats a number for display
* @param num The number to format
* @returns Formatted string
*/
private formatNumber(num: number): string {
const rounded = Math.round(num * 1e10) / 1e10;
if (rounded === 0) return '0';
return rounded === Math.round(rounded) ? rounded.toString() : rounded.toFixed(6).replace(/\.?0+$/, '');
}
/**
* Formats a term with coefficient and variable
* @param coefficient The coefficient
* @param variable The variable
* @returns Formatted term string
*/
private formatTerm(coefficient: number, variable: string): string {
if (Math.abs(coefficient) < 1e-10) return '';
if (coefficient === 1) {
return ` + ${variable}`;
} else if (coefficient === -1) {
return ` - ${variable}`;
} else if (coefficient > 0) {
return ` + ${this.formatNumber(coefficient)}${variable}`;
} else {
return ` - ${this.formatNumber(-coefficient)}${variable}`;
}
}
}