An equation-solving library written purely in Dart.

The `equations`

package is used to solve numerical analysis problems with ease. It's purely written in Dart, meaning it has no platform-specific dependencies and doesn't require Flutter to work. You can use `equations`

, for example, in a Dart CLI project or a Flutter cross-platform application. Here's a summary of what you can do with this package:

- solve polynomial equations with
`Algebraic`

types; - solve nonlinear equations with
`Nonlinear`

types; - solve linear systems of equations with
`SystemSolver`

types; - evaluate integrals with
`NumericalIntegration`

types; - interpolate data points with
`Interpolation`

types.

In addition, you can also find utilities to work with:

- Real and complex matrices, using the
`Matrix<T>`

types; - Complex number, using the
`Complex`

type; - Fractions, using the
`Fraction`

and`MixedFraction`

types.

This package has a type-safe API and requires Dart 3.0 (or higher versions). There is a demo project created with Flutter that shows an example of how this library could be used to develop a numerical analysis application :rocket:

Equation Solver - Flutter web demo

The source code of the Flutter application can be found at `example/flutter_example/`

. Visit the official pub.dev documentation for details about methods signatures and inline documentation.

# Algebraic (Polynomial equations)

Use one of the following classes to find the roots of a polynomial equation (also known as "algebraic equation"). You can use both complex numbers and fractions as coefficients.

Solver name | Equation | Params field |
---|---|---|

`Constant` |
f(x) = a |
a ∈ C |

`Linear` |
f(x) = ax + b |
a, b ∈ C |

`Quadratic` |
f(x) = ax^{2} + bx + c |
a, b, c ∈ C |

`Cubic` |
f(x) = ax^{3} + bx^{2} + cx + d |
a, b, c, d ∈ C |

`Quartic` |
f(x) = ax^{4} + bx^{3} + cx^{2} + dx + e |
a, b, c, d, e ∈ C |

`DurandKerner` |
Any polynomial P(x_{i}) where x_{i} are coefficients |
x_{i} ∈ C |

There's a formula for polynomials up to the fourth degree, as explained by the Galois theory. Roots of polynomials whose degree is 5 or higher must be found using DurandKerner's method (or any other root-finding algorithm). For this reason, we suggest the following approach:

- use
`Linear`

to find the roots of a polynomial whose degree is 1; - use
`Quadratic`

to find the roots of a polynomial whose degree is 2; - use
`Cubic`

to find the roots of a polynomial whose degree is 3; - use
`Quartic`

to find the roots of a polynomial whose degree is 4; - use
`DurandKerner`

to find the roots of a polynomial whose degree is 5 or higher.

Since `DurandKerner`

works with any polynomial, you could also use it (for example) to solve a cubic equation. However, `DurandKerner`

internally uses loops, derivatives, and other mechanics to approximate the actual roots. When the degree is 4 or lower, prefer working with `Quartic`

, `Cubic`

, `Quadratic`

or `Linear`

because they use direct formulas to find the roots (and thus they're more precise). Here's an example of how to find the roots of a cubic:

```
// f(x) = (2-3i)x^3 + 6/5ix^2 - (-5+i)x - (9+6i)
final equation = Cubic(
a: Complex(2, -3),
b: Complex.fromImaginaryFraction(Fraction(6, 5)),
c: Complex(5, -1),
d: Complex(-9, -6)
);
final degree = equation.degree; // 3
final isReal = equation.isRealEquation; // false
final discr = equation.discriminant(); // -31299.688 + 27460.192i
// f(x) = (2 - 3i)x^3 + 1.2ix^2 + (5 - 1i)x + (-9 - 6i)
print('$equation');
// f(x) = (2 - 3i)x^3 + 6/5ix^2 + (5 - 1i)x + (-9 - 6i)
print(equation.toStringWithFractions());
/*
* Prints the roots:
*
* x1 = 0.348906207844 - 1.734303423032i
* x2 = -1.083892638909 + 0.961044482775
* x3 = 1.011909507988 + 0.588643555642
* */
for (final root in equation.solutions()) {
print(root);
}
```

Alternatively, you could have used `DurandKerner`

to solve the same equation:

```
// f(x) = (2-3i)x^3 + 6/5ix^2 - (-5+i)x - (9+6i)
final equation = DurandKerner(
coefficients: [
Complex(2, -3),
Complex.fromImaginaryFraction(Fraction(6, 5)),
Complex(5, -1),
Complex(-9, -6),
]
);
/*
* Prints the roots of the equation:
*
* x1 = 1.0119095 + 0.5886435
* x2 = 0.3489062 - 1.7343034i
* x3 = -1.0838926 + 0.9610444
* */
for (final root in equation.solutions()) {
print(root);
}
```

As we've already pointed out, both ways are equivalent. However, `DurandKerner`

is computationally slower than `Cubic`

and doesn't always guarantee to converge to the correct roots. Use `DurandKerner`

only when the degree of your polynomial is greater or equal to 5.

```
final quadratic = Algebraic.from(const [
Complex(2, -3),
Complex.i(),
Complex(1, 6)
]);
final quartic = Algebraic.fromReal(const [
1, -2, 3, -4, 5
]);
```

The factory constructor `Algebraic.from()`

automatically returns the best type of `Algebraic`

according to the number of parameters you've passed.

# Nonlinear equations

Use one of the following classes, representing a root-finding algorithm, to find a root of an equation. Only real numbers are allowed. This package supports the following root-finding methods:

Solver name | Params field |
---|---|

`Bisection` |
a, b ∈ R |

`Chords` |
a, b ∈ R |

`Netwon` |
x_{0} ∈ R |

`Secant` |
a, b ∈ R |

`Steffensen` |
x_{0} ∈ R |

`Brent` |
a, b ∈ R |

`RegulaFalsi` |
a, b ∈ R |

Expressions are parsed using petitparser: a fast, stable and well-tested grammar parser. Here's a simple example of how you can find the roots of an equation using Newton's method:

```
final newton = Newton("2*x+cos(x)", -1, maxSteps: 5);
final steps = newton.maxSteps; // 5
final tol = newton.tolerance; // 1.0e-10
final fx = newton.function; // 2*x+cos(x)
final guess = newton.x0; // -1
final solutions = newton.solve();
final convergence = solutions.convergence.round(); // 2
final solutions = solutions.efficiency.round(); // 1
/*
* The getter `solutions.guesses` returns the list of values computed by the algorithm
*
* -0.4862880170389824
* -0.45041860473199363
* -0.45018362150211116
* -0.4501836112948736
* -0.45018361129487355
*/
final List<double> guesses = solutions.guesses;
```

Certain algorithms don't always guarantee to converge to the correct root so carefully read the documentation before choosing the method.

# Systems of equations

Use one of the following classes to solve systems of linear equations. Only real coefficients are allowed (so `double`

is ok, but `Complex`

isn't) and you must define `N`

equations in `N`

variables (so **square** matrices only are allowed). This package supports the following algorithms:

Solver name | Iterative method |
---|---|

`CholeskySolver` |
:x: |

`GaussianElimination` |
:x: |

`GaussSeidelSolver` |
:heavy_check_mark: |

`JacobiSolver` |
:heavy_check_mark: |

`LUSolver` |
:x: |

`SORSolver` |
:heavy_check_mark: |

These solvers are used to find the `x`

in the `Ax = b`

equation. Methods require, at least, the system matrix `A`

and the known values vector `b`

. Iterative methods may require additional parameters such as an initial guess or a particular configuration value. For example:

```
// Solve a system using LU decomposition
final luSolver = LUSolver(
equations: const [
[7, -2, 1],
[14, -7, -3],
[-7, 11, 18]
],
constants: const [12, 17, 5]
);
final solutions = luSolver.solve(); // [-1, 4, 3]
final determinant = luSolver.determinant(); // -84.0
```

If you just want to work with matrices (for operations, decompositions, eigenvalues, etc...) you can consider using either `RealMatrix`

(to work with `double`

) or `ComplexMatrix`

(to work with `Complex`

). Both are subclasses of `Matrix<T>`

so they have the same public API:

```
final matrixA = RealMatrix.fromData(
columns: 2,
rows: 2,
data: const [
[2, 6],
[-5, 0]
]
);
final matrixB = RealMatrix.fromData(
columns: 2,
rows: 2,
data: const [
[-4, 1],
[7, -3],
]
);
final sum = matrixA + matrixB;
final sub = matrixA - matrixB;
final mul = matrixA * matrixB;
final div = matrixA / matrixB;
final lu = matrixA.luDecomposition();
final cholesky = matrixA.choleskyDecomposition();
final cholesky = matrixA.choleskyDecomposition();
final qr = matrixA.qrDecomposition();
final svd = matrixA.singleValueDecomposition();
final det = matrixA.determinant();
final rank = matrixA.rank();
final eigenvalues = matrixA.eigenvalues();
final characPolynomial = matrixA.characteristicPolynomial();
```

You can use `toString()`

to print the matrix contents. The `toStringAugmented()`

method prints the augmented matrix (the matrix + one extra column with the known values vector). For example:

```
final lu = LUSolver(
equations: const [
[7, -2, 1],
[14, -7, -3],
[-7, 11, 18]
],
constants: const [12, 17, 5]
);
/*
* Output with 'toString':
*
* [7.0, -2.0, 1.0]
* [14.0, -7.0, -3.0]
* [-7.0, 11.0, 18.0]
*/
print("$lu");
/*
* Output with 'toStringAugmented':
*
* [7.0, -2.0, 1.0 | 12.0]
* [14.0, -7.0, -3.0 | 17.0]
* [-7.0, 11.0, 18.0 | 5.0]
*/
print("${lu.toStringAugmented()}");
```

The `ComplexMatrix`

has the same API and the same usage as `RealMatrix`

with the only difference that it works with complex numbers rather then real numbers.

# Numerical integration

The "**numerical integration**" term refers to a group of algorithms for calculating the numerical value of a definite integral. The function must be continuous within the integration bounds. This package currently supports the following algorithms:

`MidpointRule`

`SimpsonRule`

`TrapezoidalRule`

`AdaptiveQuadrature`

Other than the integration bounds (called `lowerBound`

and `lowerBound`

), some classes may also have an optional `intervals`

parameter. It already has a good default value but of course you can change it! For example:

```
const simpson = SimpsonRule(
function: 'sin(x)*e^x',
lowerBound: 2,
upperBound: 4,
);
// Calculating the value of...
//
// ∫ sin(x) * e^x dx
//
// ... between 2 and 4.
final results = simpson.integrate();
// Prints '-7.713'
print('${results.result.toStringAsFixed(3)}');
// Prints '32'
print('${results.guesses.length}');
```

Midpoint, trapezoidal and Simpson methods have the `intervals`

parameter while the adaptive quadrature method doesn't (because it doesn't need it). The `integrate()`

function returns an `IntegralResults`

which simply is a wrapper for 2 values:

`result`

: the value of the definite integral evaluated within`lowerBound`

and`lowerBound`

,`guesses`

: the various intermediate values that brought to the final result.

# Interpolation

This package can also perform linear, polynomial or spline interpolations using the `Interpolation`

types. You just need to give a few points in the constructor and then use `compute(double x)`

to interpolate the value. The package currently supports the following algorithms:

`LinearInterpolation`

`PolynomialInterpolation`

`NewtonInterpolation`

`SplineInterpolation`

You'll always find the `compute(double x)`

method in any `Interpolation`

type, but some classes may have additional methods that others haven't. For example:

```
const newton = NewtonInterpolation(
nodes: [
InterpolationNode(x: 45, y: 0.7071),
InterpolationNode(x: 50, y: 0.7660),
InterpolationNode(x: 55, y: 0.8192),
InterpolationNode(x: 60, y: 0.8660),
],
);
// Prints 0.788
final y = newton.compute(52);
print(y.toStringAsFixed(3));
// Prints the following:
//
// [0.7071, 0.05890000000000006, -0.005700000000000038, -0.0007000000000000339]
// [0.766, 0.053200000000000025, -0.006400000000000072, 0.0]
// [0.8192, 0.04679999999999995, 0.0, 0.0]
// [0.866, 0.0, 0.0, 0.0]
print('\n${newton.forwardDifferenceTable()}');
```

Since the Newton interpolation algorithm internally builds the "divided differences table", the API exposes two methods (`forwardDifferenceTable()`

and `backwardDifferenceTable()`

) to print those tables. Of course, you won't find `forwardDifferenceTable()`

in other interpolation types because they just don't use it. By default, `NewtonInterpolation`

uses the forward difference method but if you want the backward one, just pass `forwardDifference: false`

in the constructor.

```
const polynomial = PolynomialInterpolation(
nodes: [
InterpolationNode(x: 0, y: -1),
InterpolationNode(x: 1, y: 1),
InterpolationNode(x: 4, y: 1),
],
);
// Prints -4.54
final y = polynomial.compute(-1.15);
print(y.toStringAsFixed(2));
// Prints -0.5x^2 + 2.5x + -1
print('\n${polynomial.buildPolynomial()}');
```

This is another example with a different interpolation strategy. The `buildPolynomial()`

method returns the interpolation polynomial (as an `Algebraic`

type) internally used to interpolate `x`

.

# Expressions parsing

You can use the `ExpressionParser`

type to either parse numerical expressions or evaluate mathematical functions with the `x`

variable:

```
const parser = ExpressionParser();
print(parser.evaluate('5*3-4')); // 11
print(parser.evaluate('sqrt(49)+10')); // 17
print(parser.evaluate('pi')); // 3.1415926535
print(parser.evaluateOn('6*x + 4', 3)); // 22
print(parser.evaluateOn('sqrt(x) - 3', 81)); // 6
```

This type is internally used by the library to evaluate nonlinear functions.

## Libraries

- equations
- This package is used to solve numerical analysis problems, such as: