Welcome, fellow Dart and Flutter developers! Today we’re going to explore an equation solving library called equations. Dart is shipped with the dart:math package but it only features basic operations on numbers, it’s not a scientific library. Let’s see what we can do with this new package.
This package is written with pure Dart code so you can use it outside of a Flutter project as well. It’s an equation solving library which also exposes some very useful utilities to work with matrices and complex numbers.
There is an online demo that shows what you can do with the package. It’s been built with Flutter so you could easily build the project in your machine and run it on mobile or desktop as well.
As we’ve already stated, the main purpose of this package is solving equations. If you want to find the solutions (or better, the roots) of a polynomial equations, you should use one of these classes:
Linear: for linear polynomials like 3x - 5 = 0
Quadratic: for quadratic polynomials like x² + 3x - 2 = 0
Cubic: for cubic polynomials like 2x³ + x² + 2/5 = 0
Quartic: for quartic polynomials like x⁴ - 8x³ + x = 0
DurandKerner: for any polynomial whose degree is 5 or higher, like 5x⁷ +6x³ + 3 = 0
Unless you can reduce it to a lower degree, when the polynomial is of fifth degree or higher you must use the DurandKerner type. Here’s a simple example which also uses the pi (3.14) constant:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// f(x) = 2x^4 - 5/2x^3 + pi = 0
final equation = Quartic.realEquation(
a: 2,
b: 5 / 2,
e: pi,
);
// f(x) = 2x^4 + 2.5x^3 + 3.141592653589793
print("$equation");
/*
* Prints the roots of the equation:
*
* x = -1.1890748945644725 + 0.6370130733016783i
* x = -1.1890748945644725 - 0.6370130733016783i
* x = 0.5640748945644724 + 0.7382709096288316i
* x = 0.5640748945644724 - 0.7382709096288316i
* */
for (final root in equation.solutions()) {
print(root);
}
You can also use complex numbers.
// f(x) = (-3 + 4i)x^4 - x^3 + 6ix + 2 = 0
final equation = Quartic(
a: Complex(-3, 4) b: Complex.fromReal(-1),
d: Complex.fromImaginary(6),
e: Complex.fromReal(2),
);
If you want to solve any other kind of equation you should use NonLinear types. They are a series of root-finding algorithms that try to guess a solution with a given initial value. The most popular one is Newton’s method:
1
2
3
4
5
6
7
8
9
10
final newton = Newton(
function: ‘e * x + sin(x)’,
x0: -1,
maxSteps: 8,
);
final solutions = newton.solve();
for (final guess in solutions) {
print(‘$guess’);
}
There are other algorithms such as bisection, Steffensen, secant, chords, and many more. Even if you could solve polynomials using a root finding algorithm, you won’t get all of the roots so you should prefer Algebraic types.
This package is able to solve linear systems of equations of N variables in N unknowns. There are various solvers implementing different algorithms but all of the classes use the same API.
1
2
3
4
5
6
7
8
9
10
final luSolver = LUSolver(
equations: const [
[1, 3, 1],
[4, 0, -1],
[1, -1, -1]
],
constants: const [3, 1, 5]
);
final solutions = luSolver.solve(); // [-4, 8, -17]
The system is solved using the typical LU decomposition algorithm but there are many more available: Cholesky, SOR, Jacobi, Gauss and Gauss-Seidel. Along with systems, you can also use RealMatrix or ComplexMatrix types to analyze, respectively, real and complex matrices:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* | -3+i -1-4i |
* | 7 i |
*
*/
final matrix = ComplexMatrix.fromData(
columns: 2,
rows: 2,
data: const [
[Complex(-3, 1), Complex(-1, -4)],
[Complex.fromReal(4), Complex.i()],
],
);
final transpose = matrix.transpose();
final cofactors = matrix.cofactorMatrix();
final inverse = matrix.inverse();
final trace = matrix.trace();
final rank = matrix.rank();
final charPol = matrix.characteristicPolynomial();
final eigenvalues = matrix.eigenvalues();
The API of the RealMatrix type is identical but it works with double rather than Complex. Matrices can be multiplied, divided, added, subtracted and even decomposed with various algorithms:
LU decomposition
Cholesky decomposition
QR decomposition
Single value decomposition
Eigendecomposition
You can compute the numerical value of a definite integral using some well-known integration algorithms: Simpson method, trapezoid rule, and midpoint rule.
1
2
3
4
5
6
7
8
// ∫ cos(x) + x^2 dx in [2;3]
const simpson = SimpsonRule(
function: 'cos(x) + x^2',
lowerBound: 2,
upperBound: 3,
);
final result = simpson.integrate(); // 5.5652
In the same way, you can also use the TrapezoidalRule and MidpointRule types. The integrate() method returns the result and a list of intermediate steps (the guesses) the algorithm produced to find the solution.
The last, but not least, major feature of this package is data interpolation. You can use interpolation types to interpolate two or more points using different strategies:
NewtonInterpolation
LinearInterpolation
SplineInterpolation
PolynomialInterpolation
Again, the various classes all share the same API plus some specific method for certain types. For example, the NewtonInterpolation type has forwardDifferenceTable() and backwardDifferenceTable() methods to compute forward and backward differences tables. Here’s an example:
1
2
3
4
5
6
7
8
9
10
11
12
const poly = PolynomialInterpolation(
nodes: [
InterpolationNode(x: -2, y: -2),
InterpolationNode(x: 3, y: 4),
InterpolationNode(x: 5, y: 6),
],
);
final y = poly.compute(6);
print(y.toStringAsFixed(2)); // 6.91
print('\n{poly.buildPolynomial()}'); // -0.02x^2 + 1.22x + 0.57
In case of PolynomialInterpolation, the buildPolynomial() method returns an Algebraic type representing the interpolation polynomial generated by the given points.
In the next article we will talk about golden tests in Flutter. They add more reliability to widget testing and provide a quick preview to the developer of how a widget will look once rendered. That’s all for now, stay tuned!