UNPKG

nerdamer-prime

Version:
1,225 lines (1,127 loc) 98.8 kB
/* * Author : Martin Donk * Website : http://www.nerdamer.com * Email : martin.r.donk@gmail.com * Source : https://github.com/jiggzson/nerdamer */ // Type imports for JSDoc ====================================================== // These typedefs provide type aliases for the interfaces defined in index.d.ts. // They enable proper type checking when working with the classes defined in this file. // // Usage patterns: // - For return types: @returns {NerdamerSymbolType} // - For parameters: @param {NerdamerSymbolType} symbol // - For variable declarations: /** @type {NerdamerSymbolType} */ /** * Core type aliases from index.d.ts * * @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType * * @typedef {import('./index').NerdamerCore.Frac} FracType * * @typedef {import('./index').NerdamerCore.Vector} VectorType * * @typedef {import('./index').NerdamerCore.Matrix} MatrixType * * @typedef {import('./index').NerdamerCore.Parser} ParserType * * @typedef {import('./index').NerdamerCore.Settings} SettingsType * * @typedef {import('./index').NerdamerExpression} ExpressionType * * @typedef {typeof import('./index')} NerdamerType * * Constructor types (for factory functions) * * @typedef {import('./index').NerdamerCore.SymbolConstructor} SymbolConstructor * * @typedef {import('./index').NerdamerCore.VectorConstructor} VectorConstructor * * Module types * * @typedef {import('./index').NerdamerCore.AlgebraModule} AlgebraModuleType * * @typedef {import('./index').NerdamerCore.CalculusModule} CalculusModuleType * * @typedef {import('./index').NerdamerCore.FactorSubModule} FactorSubModuleType * * @typedef {import('./index').NerdamerCore.SimplifySubModule} SimplifySubModuleType * * @typedef {import('./index').NerdamerCore.IntegrationSubModule} IntegrationSubModuleType * * @typedef {import('./index').NerdamerCore.AlgebraClassesSubModule} AlgebraClassesSubModuleType * * @typedef {import('./index').NerdamerCore.DecomposeResultObject} DecomposeResultType * * @typedef {import('./index').NerdamerCore.SolveModule} SolveModuleType * * Utility types * * @typedef {import('./index').NerdamerCore.Utils} UtilsInterface * * @typedef {import('./index').NerdamerCore.Build} BuildInterface * * @typedef {import('big-integer').BigInteger} BigIntegerType * * Equation instance type * * @typedef {import('./index').NerdamerCore.EquationInstance} EquationInstanceType * * Solution result types * * @typedef {import('./index').NerdamerCore.SystemSolutionResult} SystemSolutionResultType * * @typedef {import('./index').NerdamerCore.SystemSolutionValue} SystemSolutionValueType * * @typedef {import('./index').NerdamerCore.CircleSolutionResult} CircleSolutionResultType * * @typedef {(NerdamerSymbolType | EquationInstanceType | string)[]} SolveEquationArray */ // Check if nerdamer exists globally (browser) or needs to be required (Node.js) let nerdamer = typeof globalThis !== 'undefined' && globalThis.nerdamer ? globalThis.nerdamer : undefined; if (typeof module !== 'undefined' && nerdamer === undefined) { nerdamer = require('./nerdamer.core.js'); require('./Calculus.js'); require('./Algebra.js'); } /** @returns {SolveModuleType} */ (function initSolveModule() { // Handle imports const core = nerdamer.getCore(); const _ = core.PARSER; /** @type {AlgebraModuleType} */ const _A = /** @type {AlgebraModuleType} */ (core.Algebra); /** @type {CalculusModuleType} */ const _C = /** @type {CalculusModuleType} */ (core.Calculus); const { integration } = /** @type {{ integration: IntegrationSubModuleType }} */ (_C); const { decompose_arg: explode } = integration; const { Factor, Simplify, Classes: AlgebraClasses, } = /** @type {{ Factor: FactorSubModuleType; Simplify: SimplifySubModuleType; Classes: AlgebraClassesSubModuleType }} */ ( _A ); const { evaluate, remove, format, knownVariable, isSymbol, variables, range } = core.Utils; const { build } = core.Build; const { NerdamerSymbol } = core; const { S, PL, CB, CP, FN } = core.groups; const { Settings } = core; const { isArray } = core.Utils; // The search radius for the roots core.Settings.SOLVE_RADIUS = 1000; // The maximum number to fish for on each side of the zero core.Settings.ROOTS_PER_SIDE = 10; // Covert the number to multiples of pi if possible core.Settings.make_pi_conversions = false; // The step size core.Settings.STEP_SIZE = 0.1; // The epsilon size core.Settings.EPSILON = 2e-13; // The maximum iterations for Newton's method core.Settings.MAX_NEWTON_ITERATIONS = 200; // The epsilon used in Newton's iteration // core.Settings.NEWTON_EPSILON = Number.EPSILON * 2; core.Settings.NEWTON_EPSILON = 2e-15; // The maximum number of time non-linear solve tries another jump point core.Settings.MAX_NON_LINEAR_TRIES = 12; // The amount of iterations the function will start to jump at core.Settings.NON_LINEAR_JUMP_AT = 50; // The size of the jump core.Settings.NON_LINEAR_JUMP_SIZE = 100; // The original starting point for nonlinear solving core.Settings.NON_LINEAR_START = 0.01; // When points are generated as starting points for Newton's method, they are sliced into small // slices to make sure that we have convergence on the right point. This defines the // size of the slice core.Settings.NEWTON_SLICES = 200; // The distance in which two solutions are deemed the same core.Settings.SOLUTION_PROXIMITY = 1e-14; // Indicate wheter to filter the solutions are not core.Settings.FILTER_SOLUTIONS = true; // The maximum number of recursive calls core.Settings.MAX_SOLVE_DEPTH = 10; // The tolerance that's considered close enough to zero core.Settings.ZERO_EPSILON = 1e-9; // The maximum iteration for the bisection method incase of some JS strangeness core.Settings.MAX_BISECTION_ITER = 2000; // The tolerance for the bisection method core.Settings.BI_SECTION_EPSILON = 1e-12; core.NerdamerSymbol.prototype.hasTrig = function hasTrig() { return this.containsFunction(['cos', 'sin', 'tan', 'cot', 'csc', 'sec']); }; core.NerdamerSymbol.prototype.hasNegativeTerms = function hasNegativeTerms() { if (this.isComposite()) { for (const x in this.symbols) { if (!Object.hasOwn(this.symbols, x)) { continue; } const sym = this.symbols[x]; if ((sym.group === PL && sym.hasNegativeTerms()) || this.symbols[x].power.lessThan(0)) { return true; } } } return false; }; /* Nerdamer version 0.7.x and up allows us to make better use of operator overloading * As such we can have this data type be supported completely outside of the core. * This is an equation that has a left hand side and a right hand side */ /** * Equation class representing LHS = RHS. * * @class * @param {NerdamerSymbolType} lhs - The left hand side symbol * @param {NerdamerSymbolType} rhs - The right hand side symbol */ function Equation(lhs, rhs) { if ( (rhs.isConstant() && lhs.isConstant() && !lhs.equals(rhs)) || (lhs.equals(core.Settings.IMAGINARY) && rhs.isConstant(true)) || (rhs.equals(core.Settings.IMAGINARY) && lhs.isConstant(true)) ) { throw new core.exceptions.NerdamerValueError(`${lhs.toString()} does not equal ${rhs.toString()}`); } /** @type {NerdamerSymbolType} */ this.LHS = lhs; // Left hand side /** @type {NerdamerSymbolType} */ this.RHS = rhs; // Right and side } // UTILS ##!! Equation.prototype = { toString() { return `${this.LHS.toString()}=${this.RHS.toString()}`; }, text(option) { return `${this.LHS.text(option)}=${this.RHS.text(option)}`; }, /** * Brings the equation to LHS (sets RHS to zero). * * @param {boolean} [expand] - Whether to expand the result * @returns {NerdamerSymbolType} The LHS with RHS subtracted */ toLHS(expand) { expand = !!expand; const eqn = this.removeDenom(); let a = eqn.LHS; let b = eqn.RHS; if (a.isConstant(true) && !b.isConstant(true)) { // Swap them to avoid confusing parser and cause an infinite loop [a, b] = [b, a]; } const _t = /** @type {NerdamerSymbolType} */ (_.subtract(a, b)); /** @type {NerdamerSymbolType} */ let retval = expand ? /** @type {NerdamerSymbolType} */ (_.expand(_t)) : _t; // Quick workaround for issue #636 // This basically borrows the removeDenom method from the Equation class. // TODO: Make this function a stand-alone function retval = new Equation(retval, new NerdamerSymbol(0)).removeDenom().LHS; return retval; }, /** * Removes denominators from both sides. * * @returns {Equation} Equation with denominators removed */ removeDenom() { let a = this.LHS.clone(); let b = this.RHS.clone(); // Remove the denominator on both sides const den = /** @type {NerdamerSymbolType} */ (_.multiply(a.getDenom(), b.getDenom())); a = /** @type {NerdamerSymbolType} */ (_.expand(_.multiply(a, den.clone()))); b = /** @type {NerdamerSymbolType} */ (_.expand(_.multiply(b, den))); // Swap the groups if (b.group === CP && b.group !== CP) { const t = a; a = b; b = t; // Swap } // Scan to eliminate denominators if (a.group === CB) { let t = new NerdamerSymbol(a.multiplier); /** @type {NerdamerSymbolType} */ let newRHS = b.clone(); a.each(y => { if (y.power.lessThan(0)) { newRHS = /** @type {NerdamerSymbolType} */ (_.divide(newRHS, y)); } else { t = /** @type {NerdamerSymbolType} */ (_.multiply(t, y)); } }); a = t; b = newRHS; } else if (a.group === CP) { // The logic: loop through each and if it has a denominator then multiply it out on both ends // and then start over for (const x in a.symbols) { if (!Object.hasOwn(a.symbols, x)) { continue; } const sym = a.symbols[x]; if (sym.group === CB) { for (const y in sym.symbols) { if (!Object.hasOwn(sym.symbols, y)) { continue; } const sym2 = sym.symbols[y]; if (sym2.power.lessThan(0)) { const result = new Equation( /** @type {NerdamerSymbolType} */ ( _.expand(_.multiply(sym2.clone().toLinear(), a)) ), /** @type {NerdamerSymbolType} */ (_.expand(_.multiply(sym2.clone().toLinear(), b))) ); return result; } } } } } return new Equation(a, b); }, /** * Creates a copy of this equation. * * @returns {Equation} */ clone() { return new Equation(this.LHS.clone(), this.RHS.clone()); }, /** * Substitutes a value for a variable on both sides. * * @param {NerdamerSymbolType} x - Variable to replace * @param {NerdamerSymbolType} y - Value to substitute * @returns {Equation} */ sub(x, y) { const clone = this.clone(); clone.LHS = clone.LHS.sub(x.clone(), y.clone()); clone.RHS = clone.RHS.sub(x.clone(), y.clone()); return clone; }, /** * Checks if the equation evaluates to zero. * * @returns {boolean} */ isZero() { return core.Utils.evaluate(this.toLHS()).equals(0); }, /** * Returns LaTeX representation. * * @param {string} [option] * @returns {string} */ latex(option) { return [this.LHS.latex(option), this.RHS.latex(option)].join('='); }, }; // Overwrite the equals function /** * Creates an Equation from two symbols. This extends the parser's equals function to return Equation objects. * * @param {NerdamerSymbolType} a * @param {NerdamerSymbolType} b * @returns {Equation} */ // @ts-ignore - Overriding parser.equals to return Equation instead of Symbol _.equals = function equals(a, b) { return new Equation(a, b); }; // Extend simplify (function extendSimplifyForEquations() { const simplify = _.functions.simplify[0]; _.functions.simplify[0] = function simplifyWithEquationSupport(symbol) { if (symbol instanceof Equation) { symbol.LHS = simplify(symbol.LHS); symbol.RHS = simplify(symbol.RHS); return symbol; } // Just call the original simplify return simplify(symbol); }; })(); /** * Sets two expressions equal * * @param {NerdamerSymbolType} symbol * @returns {Equation} */ core.Expression.prototype.equals = function equals(symbol) { if (symbol instanceof core.Expression) { symbol = symbol.symbol; } // Grab the symbol if it's an expression const eq = new Equation(this.symbol, symbol); return eq; }; core.Expression.prototype.solveFor = function solveFor(x) { core.Utils.armTimeout(); try { const { symbol } = this; if (this.symbol instanceof Equation) { // Exit right away if we already have the answer // check the LHS if (this.symbol.LHS.isConstant() && this.symbol.RHS.equals(x)) { return [new core.Expression(this.symbol.LHS)]; } // Check the RHS if (this.symbol.RHS.isConstant() && this.symbol.LHS.equals(x)) { return [new core.Expression(this.symbol.RHS)]; } } const terms = solve(symbol, x); const result = terms.map(term => { term = /** @type {NerdamerSymbolType} */ ( Simplify.simplify(/** @type {NerdamerSymbolType} */ (_.parse(term))) ); const expr = new core.Expression(term); return expr; }); return result; } finally { core.Utils.disarmTimeout(); } }; core.Expression.prototype.expand = function expand() { if (this.symbol instanceof Equation) { const clone = this.symbol.clone(); clone.RHS = /** @type {NerdamerSymbolType} */ (_.expand(clone.RHS)); clone.LHS = /** @type {NerdamerSymbolType} */ (_.expand(clone.LHS)); return new core.Expression(clone); } return new core.Expression(_.expand(/** @type {NerdamerSymbolType} */ (this.symbol))); }; // eslint-disable-next-line func-names -- naming this 'variables' would shadow the imported variables utility core.Expression.prototype.variables = function () { if (this.symbol instanceof Equation) { return core.Utils.arrayUnique( core.Utils.variables(this.symbol.LHS).concat(core.Utils.variables(this.symbol.RHS)) ); } return core.Utils.variables(this.symbol); }; const setEq = function setEq(a, b) { return _.equals(a, b); }; // Link the Equation class back to the core core.Equation = Equation; // Loops through an array and attempts to fails a test. Stops if manages to fail. const checkAll = (core.Utils.checkAll = function checkAll(args, test) { for (let i = 0; i < args.length; i++) { if (test(args[i])) { return false; } } return true; }); // Version solve /** @type {SolveModuleType} */ const __ = (core.Solve = { version: '2.0.3', /** @type {NerdamerSymbolType[]} */ solutions: [], solve(eq, variable) { const save = Settings.PARSE2NUMBER; Settings.PARSE2NUMBER = false; const solution = solve(eq, String(variable)); Settings.PARSE2NUMBER = save; return new core.Vector(solution); // Return new core.Vector(solve(eq.toString(), variable ? variable.toString() : variable)); }, /** * Brings the equation to LHS. A string can be supplied which will be converted to an Equation * * @param {Equation | string | NerdamerSymbolType} eqn * @param {boolean} [expand] * @returns {NerdamerSymbolType} */ toLHS(eqn, expand) { if (isSymbol(eqn)) { return eqn; } // If it's an equation then call its toLHS function instead if (!(eqn instanceof Equation)) { const eqnStr = /** @type {string} */ (eqn); const es = eqnStr.split('='); // Convert falsey values to zero es[1] ||= '0'; eqn = new Equation( /** @type {NerdamerSymbolType} */ (_.parse(es[0])), /** @type {NerdamerSymbolType} */ (_.parse(es[1])) ); } return eqn.toLHS(expand); }, // GetSystemVariables: function(eqns) { // vars = variables(eqns[0], null, null, true); // // //get all variables // for (let i = 1, l=eqns.length; i < l; i++) // vars = vars.concat(variables(eqns[i])); // //remove duplicates // vars = core.Utils.arrayUnique(vars).sort(); // // //done // return vars; // }, /** * Solve a set of circle equations. * * @param {NerdamerSymbolType[]} eqns * @param {string[]} vars * @returns {Array | object} */ solveCircle(eqns, vars) { // Convert the variables to symbols const svars = vars.map(x => /** @type {NerdamerSymbolType} */ (_.parse(x))); /** @type {number[][]} */ const deg = []; /** @type {CircleSolutionResultType} */ let solutions = []; // Get the degree for the equations for (let i = 0; i < eqns.length; i++) { /** @type {number[]} */ const d = []; for (let j = 0; j < svars.length; j++) { d.push(Number(_A.degree(eqns[i], svars[j]))); } // Store the total degree d.push(/** @type {number} */ (core.Utils.arraySum(d, true))); deg.push(d); } let a = eqns[0]; let b = eqns[1]; if (deg[0][2] > deg[1][2]) { [b, a] = [a, b]; [deg[1], deg[0]] = [deg[0], deg[1]]; } // Only solve it's truly a circle if (deg[0][0] === 1 && deg[0][2] === 2 && deg[1][0] === 2 && deg[1][2] === 4) { // For clarity we'll refer to the variables as x and y const x = vars[0]; const y = vars[1]; // We can now get the two points for y const yPoints = solve( /** @type {NerdamerSymbolType} */ ( _.parse(b, knownVariable(x, solve(/** @type {NerdamerSymbolType} */ (_.parse(a)), x)[0])) ), y ).map(pt => pt.toString()); // Since we now know y we can get the two x points from the first equation const xPoints = [ solve(/** @type {NerdamerSymbolType} */ (_.parse(a, knownVariable(y, yPoints[0]))))[0].toString(), ]; if (yPoints[1]) { xPoints.push( solve( /** @type {NerdamerSymbolType} */ (_.parse(a, knownVariable(y, yPoints[1]))) )[0].toString() ); } if (Settings.SOLUTIONS_AS_OBJECT) { /** @type {Record<string, string[]>} */ const solObj = {}; solObj[x] = xPoints; solObj[y] = yPoints; solutions = solObj; } else { yPoints.unshift(y); xPoints.unshift(x); solutions = [xPoints, yPoints]; } } return solutions; }, /** * Solve a system of nonlinear equations * * @param {NerdamerSymbolType[]} eqns The array of equations * @param {number} [tries] The maximum number of tries * @param {number} [start] The starting point where to start looking for solutions * @returns {SystemSolutionResultType | []} */ solveNonLinearSystem(eqns, tries, start) { if (tries < 0) { return []; // Can't find a solution } start = typeof start === 'undefined' ? core.Settings.NON_LINEAR_START : start; // The maximum number of times to jump const maxTries = core.Settings.MAX_NON_LINEAR_TRIES; // Halfway through the tries const halfway = Math.floor(maxTries / 2); // Initialize the number of tries to 10 if not specified tries = typeof tries === 'undefined' ? maxTries : tries; // A point at which we check to see if we're converging. By inspection it seems that we can // use around 20 iterations to see if we're converging. If not then we retry a jump of x const jumpAt = core.Settings.NON_LINEAR_JUMP_AT; // We jump by this many points at each pivot point const jump = core.Settings.NON_LINEAR_JUMP_SIZE; // Used to check if we actually found a solution or if we gave up. Assume we will find a solution. let found = true; const createSubs = function (vars, matrix) { return vars.map((x, i) => Number(matrix.get(i, 0))); }; const vars = core.Utils.arrayGetVariables(eqns); const jacobian = core.Matrix.jacobian(eqns, vars, x => build(x, vars), true); const maxIter = core.Settings.MAX_NEWTON_ITERATIONS; let o; let y; let iters; let xn1; let norm; let lnorm; let xn; let d; const fEqns = eqns.map(eq => build(eq, vars)); // Note: J stores compiled functions, not symbols. We use Matrix for its iteration // capabilities, but elements are actually compiled functions `(...args: number[]) => number` // The type system expects NerdamerSymbol but we're deliberately storing functions. const J = jacobian.map( (/** @type {NerdamerSymbolType} */ e) => /** @type {NerdamerSymbolType} */ (/** @type {unknown} */ (build(e, vars))), true ); // Initial values xn1 = core.Matrix.cMatrix(0, vars); // Initialize the c matrix with something close to 0. let c = core.Matrix.cMatrix(start, vars); iters = 0; // Start of algorithm do { // If we've reached the max iterations then exit if (iters > maxIter) { found = false; break; } // Set the substitution object o = createSubs(vars, c); // Set xn xn = c.clone(); // Capture current values for use in callbacks const currentO = o; const currentC = c; // Make all the substitutions for each of the equations fEqns.forEach((f, i) => { currentC.set(i, 0, f(...currentO)); }); let m = new core.Matrix(); // J actually contains compiled functions, cast to access them /** @type {{ each: (fn: (element: unknown, row: number, col: number) => void) => void }} */ ( /** @type {unknown} */ (J) ).each((fn, i, j) => { const ans = /** @type {(...args: number[]) => number} */ (fn)(...currentO); m.set(i, j, ans); }); m = m.invert(); // Preform the elimination y = /** @type {MatrixType} */ (_.multiply(m, c)).negate(); // The callback is to avoid overflow in the coeffient denonimator // it converts it to a decimal and then back to a fraction. Some precision // is lost be it's better than overflow. d = y.subtract(xn1, x => _.parse(Number(x))); xn1 = xn.add(y, x => _.parse(Number(x))); // Move c is now xn1 c = xn1; // Get the norm // the expectation is that we're converging to some answer as this point regardless of where we start // this may have to be adjusted at some point because of erroneous assumptions if (iters >= jumpAt) { // Check the norm. If the norm is greater than one then it's time to try another point if (Number(norm) > 1) { // Reset the start point at halway if (tries === halfway) { start = 0; } const sign = tries > halfway ? 1 : -1; // Which side are we incrementing // we increment +n at one side and -n at the other. const n = (tries % Math.floor(halfway)) + 1; // Adjust the start point start += sign * n * jump; // Call restart return __.solveNonLinearSystem(eqns, --tries, start); } } lnorm = norm; iters++; norm = d.max(); // Exit early. Revisit if we get bugs if (Number(norm) === Number(lnorm)) { break; } } while (Number(norm) >= Number.EPSILON); // Return a blank set if nothing was found; if (!found) { return []; } // Return c since that's the answer return /** @type {SystemSolutionResultType | []} */ ( __.systemSolutions(c, vars, true, x => core.Utils.round(Number(x), 14)) ); }, /** * Converts solution results to the appropriate format based on Settings.SOLUTIONS_AS_OBJECT. * * @param {MatrixType} result The result matrix * @param {string[]} vars The variable names * @param {boolean} [expandResult] Whether to expand the result * @param {Function} [callback] Optional callback to transform each solution value * @returns {SystemSolutionResultType} */ systemSolutions(result, vars, expandResult, callback) { if (core.Settings.SOLUTIONS_AS_OBJECT) { /** @type {Record<string, SystemSolutionValueType>} */ const solutions = {}; result.each((e, idx) => { /** @type {SystemSolutionValueType} */ let solution = /** @type {string | number} */ ((expandResult ? _.expand(e) : e).valueOf()); if (callback) { solution = callback.call(e, solution); } solutions[vars[idx]] = solution; }); return solutions; } /** @type {[string, SystemSolutionValueType][]} */ const solutions = []; result.each((e, idx) => { /** @type {SystemSolutionValueType} */ let solution = /** @type {string | number} */ ((expandResult ? _.expand(e) : e).valueOf()); if (callback) { solution = callback.call(e, solution); } solutions.push([vars[idx], solution]); }); return solutions; }, /** * Solves a system of equations by substitution. This is useful when no distinct solution exists. e.g. a line, * plane, etc. * * @param {Array} eqns * @returns {CircleSolutionResultType | []} */ solveSystemBySubstitution(eqns) { // Assume at least 2 equations. The function variables will just return an empty array if undefined is provided const varsA = variables(eqns[0]); const varsB = variables(eqns[1]); // Check if it's a circle equation if (eqns.length === 2 && varsA.length === 2 && core.Utils.arrayEqual(varsA, varsB)) { return /** @type {CircleSolutionResultType | []} */ (__.solveCircle(eqns, varsA)); } return []; // Return an empty set }, // https://www.lakeheadu.ca/sites/default/files/uploads/77/docs/RemaniFinal.pdf /** * Solves a systems of equations * * @param {Array} eqns An array of equations * @param {Array} varArray An array of variables * @returns {Array | object} */ solveSystem(eqns, varArray) { // Check if a varArray was specified // nerdamer.clearVars();// this deleted ALL variables: not what we want // parse all the equations to LHS. Remember that they come in as strings for (let i = 0; i < eqns.length; i++) { eqns[i] = __.toLHS(eqns[i]); } const l = eqns.length; let m = new core.Matrix(); const c = new core.Matrix(); let expandResult = false; let vars; if (typeof varArray === 'undefined') { // Check to make sure that all the equations are linear if (!_A.allLinear(eqns)) { try { return __.solveNonLinearSystem(eqns); } catch (e) { if (e.message === 'timeout') { throw e; } if (e instanceof core.exceptions.DivisionByZero) { return __.solveSystemBySubstitution(eqns); } } } vars = core.Utils.arrayGetVariables(eqns); // If the system only has one variable then we solve for the first one and // then test the remaining equations with that solution. If any of the remaining // equation fails then the system has no solution if (vars.length === 1) { let n = 0; let sol; let e; do { e = eqns[n].clone(); if (n > 0) { e = e.sub(vars[0], sol[0]); } sol = solve(e, vars[0]); // Skip the first one if (n === 0) { continue; } } while (++n < eqns.length); // Format the output let solutions; if (Settings.SOLUTIONS_AS_OBJECT) { solutions = {}; solutions[vars[0]] = sol; } else if (sol.length === 0) { solutions = sol; // No solutions } else { solutions = [vars[0], sol]; } return solutions; } // Deal with redundant equations as expressed in #562 // The fix is to remove all but the number of equations equal to the number // of variables. We then solve those and then evaluate the remaining equations // with those solutions. If the all equal true then those are just redundant // equations and we can return the solution set. if (vars.length < eqns.length) { const reduced = []; const n = eqns.length; for (let i = 0; i < n - 1; i++) { reduced.push(_.parse(eqns[i])); } /** @type {Record<string, NerdamerSymbolType | string | number>} */ const knowns = {}; const solutions = __.solveSystem(reduced, vars); // The solutions may have come back as an array if (Array.isArray(solutions)) { solutions.forEach(sol => { // For substitution, we only use single-value solutions (not arrays) if (!Array.isArray(sol[1])) { knowns[sol[0]] = sol[1]; } }); } else { // Filter out array solutions for substitution for (const key of Object.keys(solutions)) { const val = solutions[key]; if (!Array.isArray(val)) { knowns[key] = val; } } } // Start by assuming they will all evaluate to zero. If even one fails // then all zero will be false let allZero = true; // Check if the last solution evalutes to zero given these solutions for (let i = n - 1; i < n; i++) { if (!(/** @type {NerdamerSymbolType} */ (_.parse(eqns[i], knowns)).equals(0))) { allZero = false; } } if (allZero) { return solutions; } } // Deletes only the variables of the linear equations in the nerdamer namespace for (let i = 0; i < vars.length; i++) { nerdamer.setVar(vars[i], 'delete'); } // TODO: move this to cMatrix or something similar // populate the matrix for (let i = 0; i < l; i++) { const e = eqns[i]; // Store the expression // Iterate over the columns for (let j = 0; j < vars.length; j++) { const v = vars[j]; let coeffs = []; e.each(x => { if (x.contains(v)) { coeffs = coeffs.concat(x.coeffs()); } }); const cf = core.Utils.arraySum(coeffs); m.set(i, j, cf); } // Strip the variables from the symbol so we're left with only the zeroth coefficient // start with the symbol and remove each variable and its coefficient let num = e.clone(); vars.forEach(varName => { num = num.stripVar(varName, true); }); c.set(i, 0, num.negate()); } } else { /** * The idea is that we loop through each equation and then expand it. Afterwards we loop through each * term and see if and check to see if it matches one of the variables. When a match is found we mark * it. No other match should be found for that term. If it is we stop since it's not linear. */ vars = varArray; expandResult = true; for (let i = 0; i < l; i++) { // Prefill c.set(i, 0, new NerdamerSymbol(0)); const e = /** @type {NerdamerSymbolType[]} */ ( /** @type {NerdamerSymbolType} */ (_.expand(eqns[i])).collectSummandSymbols() ); // Expand and store // go trough each of the variables for (let j = 0; j < varArray.length; j++) { m.set(i, j, new NerdamerSymbol(0)); const v = varArray[j]; // Go through the terms and sort the variables for (let k = 0; k < e.length; k++) { const term = e[k]; let check = false; for (let z = 0; z < varArray.length; z++) { // Check to see if terms contain multiple variables if (term.contains(varArray[z])) { if (check) { core.Utils.err(`Multiple variables found for term ${term}`); } check = true; } } // We made sure that every term contains one variable so it's safe to assume that if the // variable is found then the remainder is the coefficient. if (term.contains(v)) { const tparts = /** @type {(NerdamerSymbolType | VectorType | MatrixType)[]} */ ( explode(remove(e, k), v) ); k--; // Issue #52: decrement k to hit this spot in the array e again next loop m.set(i, j, _.add(m.get(i, j), /** @type {NerdamerSymbolType} */ (tparts[0]))); } } } // All the remaining terms go to the c matrix for (let k = 0; k < e.length; k++) { c.set(i, 0, _.add(c.get(i, 0), e[k])); } } // Consider case (a+b)*I+u } // Check if the system has a distinct solution if (vars.length !== eqns.length || m.determinant().equals(0)) { // Solve the system by hand // return __.solveSystemBySubstitution(eqns, vars, m, c); throw new core.exceptions.SolveError('System does not have a distinct solution'); } // Use M^-1*c to solve system m = m.invert(); const result = m.multiply(c); // Correct the sign as per issue #410 if (core.Utils.isArray(varArray)) { result.each(x => x.negate()); } return __.systemSolutions(result, vars, expandResult); }, /** * The quadratic function but only one side. * * @param {NerdamerSymbolType} c * @param {NerdamerSymbolType} b * @param {NerdamerSymbolType} a * @returns {(NerdamerSymbolType | VectorType | MatrixType)[]} */ quad(c, b, a) { let discriminant = _.subtract( _.pow(b.clone(), new NerdamerSymbol(2)), _.multiply(_.multiply(a.clone(), c.clone()), new NerdamerSymbol(4)) ); /* B^2 - 4ac*/ // Fix for #608 discriminant = /** @type {NerdamerSymbolType} */ (_.expand(discriminant)); const det = /** @type {NerdamerSymbolType} */ (_.pow(discriminant, new NerdamerSymbol(0.5))); const den = /** @type {NerdamerSymbolType} */ ( _.parse(/** @type {NerdamerSymbolType} */ (_.multiply(new NerdamerSymbol(2), a.clone()))) ); const retval = [ _.parse(format('(-({0})+({1}))/({2})', b, det, den)), _.parse(format('(-({0})-({1}))/({2})', b, det, den)), ]; return retval; }, /** * The cubic equation * http://math.stackexchange.com/questions/61725/is-there-a-systematic-way-of-solving-cubic-equations * * @param {NerdamerSymbolType} dO * @param {NerdamerSymbolType} cO * @param {NerdamerSymbolType} bO * @param {NerdamerSymbolType} aO * @returns {Array} */ cubic(dO, cO, bO, aO) { // Convert everything to text const a = aO.text(); const b = bO.text(); const c = cO.text(); const d = dO.text(); const t = `(-(${b})^3/(27*(${a})^3)+(${b})*(${c})/(6*(${a})^2)-(${d})/(2*(${a})))`; const u = `((${c})/(3*(${a}))-(${b})^2/(9*(${a})^2))`; const v = `(${b})/(3*(${a}))`; const x = `((${t})+sqrt((${t})^2+(${u})^3))^(1/3)+((${t})-sqrt((${t})^2+(${u})^3))^(1/3)-(${v})`; // Convert a to one const w = '1/2+sqrt(3)/2*i'; // Cube root of unity return [_.parse(x), _.parse(`(${x})(${w})`), _.parse(`(${x})(${w})^2`)]; // https://www.wikihow.com/Solve-a-Cubic-Equation method 3 // const delta0 = `(${b})^2-(3*(${a})(${c}))`; // _.parse(delta0); // const delta1 = `2(${b})^3-(9*(${a})(${b})(${c}))+27((${a})^2)(${d})`; // _.parse(delta1); // // const delta = `(${delta1})^2-(4*(${delta0})^3)/(-27(${a})^2)`; // const C = `((sqrt((${delta1})^2-(4*(${delta0})^3))+(${delta1}))/2)^(1/3)`; // _.parse(C); // const u = `(-1+sqrt(-3))/2` // _.parse(u); // const result = [] // for (let n = 1; n <=3; n++) { // let x = `-((${b})+ (${u})^${n}*(${C})+(${delta0})/((${u})^${n}*(${C})))/(3(${a}))`; // console.log(x); // console.log(x.substring(168)); // result.push(_.parse(x)); // } // return result.map((x)=>_.parse(x)) }, /** * The quartic equation * * @param {NerdamerSymbolType} e * @param {NerdamerSymbolType} d * @param {NerdamerSymbolType} c * @param {NerdamerSymbolType} b * @param {NerdamerSymbolType} a * @returns {Array} */ quartic(e, d, c, b, a) { /** @type {Record<string, number>} */ const scope = {}; core.Utils.arrayUnique( variables(a).concat(variables(b)).concat(variables(c)).concat(variables(d)).concat(variables(e)) ).forEach(x => { scope[x] = 1; }); const aStr = a.toString(); const bStr = b.toString(); const cStr = c.toString(); const dStr = d.toString(); const eStr = e.toString(); let _D; /* Var D = core.Utils.block('PARSE2NUMBER', function() { return _.parse(format("256*({0})^3*({4})^3-192*({0})^2*({1})*({3})*({4})^2-128*({0})^2*({2})^2*({4})^2+144*({0})^2*({2})*({3})^2*({4})"+ "-27*({0})^2*({3})^4+144*({0})*({1})^2*({2})*({4})^2-6*({0})*({1})^2*({3})^2*({4})-80*({0})*({1})*({2})^2*({3})*({4})+18*({0})*({1})*({2})*({3})^3"+ "+16*({0})*({2})^4*({4})-4*({0})*({2})^3*({3})^2-27*({1})^4*({4})^2+18*({1})^3*({2})*({3})*({4})-4*({1})^3*({3})^3-4*({1})^2*({2})^3*({4})+({1})^2*({2})^2*({3})^2", aStr, bStr, cStr, dStr, eStr), scope); });*/ const p = _.parse(format('(8*({0})*({2})-3*({1})^2)/(8*({0})^2)', aStr, bStr, cStr)).toString(); // A, b, c const q = _.parse( format('(({1})^3-4*({0})*({1})*({2})+8*({0})^2*({3}))/(8*({0})^3)', aStr, bStr, cStr, dStr) ).toString(); // A, b, c, d, e const D0 = _.parse(format('12*({0})*({4})-3*({1})*({3})+({2})^2', aStr, bStr, cStr, dStr, eStr)).toString(); // A, b, c, d, e const D1 = _.parse( format( '2*({2})^3-9*({1})*({2})*({3})+27*({1})^2*({4})+27*({0})*({3})^2-72*({0})*({2})*({4})', aStr, bStr, cStr, dStr, eStr ) ).toString(); // A, b, c, d, e const Q = _.parse(format('((({1})+(({1})^2-4*({0})^3)^(1/2))/2)^(1/3)', D0, D1)).toString(); // D0, D1 const quarticS = _.parse( format('(1/2)*(-(2/3)*({1})+(1/(3*({0}))*(({2})+(({3})/({2})))))^(1/2)', aStr, p, Q, D0) ).toString(); // A, p, Q, D0 const x1 = _.parse( format( '-(({1})/(4*({0})))-({4})+(1/2)*sqrt(-4*({4})^2-2*({2})+(({3})/({4})))', aStr, bStr, p, q, quarticS ) ); // A, b, p, q, S const x2 = _.parse( format( '-(({1})/(4*({0})))-({4})-(1/2)*sqrt(-4*({4})^2-2*({2})+(({3})/({4})))', aStr, bStr, p, q, quarticS ) ); // A, b, p, q, S const x3 = _.parse( format( '-(({1})/(4*({0})))+({4})+(1/2)*sqrt(-4*({4})^2-2*({2})-(({3})/({4})))', aStr, bStr, p, q, quarticS ) ); // A, b, p, q, S const x4 = _.parse( format( '-(({1})/(4*({0})))+({4})-(1/2)*sqrt(-4*({4})^2-2*({2})-(({3})/({4})))', aStr, bStr, p, q, quarticS ) ); // A, b, p, q, S return [x1, x2, x3, x4]; }, /** * Breaks the equation up in its factors and tries to solve the smaller parts * * @param {NerdamerSymbolType} symbol * @param {string} solveFor * @returns {Array} */ divideAndConquer(symbol, solveFor) { let sols = []; // See if we can solve the factors const factors = Factor.factorInner(symbol); if (factors.group === CB) { factors.each(x => { x = NerdamerSymbol.unwrapPARENS(x); sols = sols.concat(solve(x, solveFor)); }); } return sols; }, /** * Attempts to solve the equation assuming it's a polynomial with numeric coefficients * * @param {NerdamerSymbolType} eq * @param {string} solveFor * @returns {Array} */ csolve(eq, solveFor) { return core.Utils.block( 'IGNORE_E', () => { let p; let pn; let n; let pf; let r; let _theta; let sr; let _sp; const roots = []; const f = /** @type {DecomposeResultType} */ (core.Utils.decompose_fn(eq, solveFor, true)); if (f.x.group === S) { p = _.parse(f.x.power); pn = Number(p); n = _.pow(_.divide(f.b.negate(), f.a), /** @type {NerdamerSymbolType} */ (p).invert()); pf = NerdamerSymbol.toPolarFormArray(/** @type {NerdamerSymbolType} */ (n)); r = pf[0]; _theta = pf[1]; sr = r.toString(); _sp = p.toString(); let k; let root; let str;