Flutter线性代数计算插件dart_lapack的使用
Flutter线性代数计算插件dart_lapack的使用
dart_lapack
Pure Dart implementation of LAPACK (Linear Algebra PACKage).
关于
Dart LAPACK 是一个纯 Dart 实现的手动转换自原始的来自 Netlib 的 LAPACK(用 Fortran 编写)。
它基于从 2024 年 1 月 25 日(大约为 LAPACK 版本 3.12.0)的 LAPACK ref 50d68905 分支。
该包包含以下内容:
- LAPACK 的 Dart 实现及其测试套件;
- Dart 实现的基本线性代数子程序(BLAS 的 Level 1、2 和 3),以及其测试套件;
- 转换自 Fortran 的 Fortran 内置子程序;
- 矩阵、数组和值类型的封装基础实现。
由于 Dart 仅原生支持双精度浮点数(Dart 官方文档),因此此包不包括单精度实数和单精度复数的 LAPACK 路由。
安装
在 pubspec.yaml
文件中添加依赖:
dependencies:
dart_lapack: ^x.x.x
然后运行以下命令安装依赖:
dart pub add dart_lapack
或者在 Flutter 项目中使用:
flutter pub add dart_lapack
使用
首先导入 dart_lapack
包:
import 'package:dart_lapack/lapack.dart';
示例代码
以下是一个完整的示例代码,展示如何使用 dart_lapack
来拟合椭圆。
示例代码:椭圆拟合
// Copyright (c) 2024 Guilherme Lepsch. All rights reserved. Use of this
// source code is governed by a MIT license that can be found in the
// [LICENSE file](https://github.com/lepsch/dart_lapack/blob/main/example/LICENSE).
import 'dart:io';
import 'dart:math' as math;
import 'package:dart_lapack/lapack.dart' as lapack;
/// Least Squares fitting of Ellipses
/// Based from Python version: https://github.com/bdhammel/least-squares-ellipse-fitting/
///
/// Fit the given [samples] to an ellipse.
///
/// Return the estimated coefficients for the Least squares fit to the elliptical
/// data containing the values [a,b,c,d,f,g] corresponding to Eqn 1 (*)
/// ax**2 + bxy + cy**2 + dx + ey + f
///
/// References
/// ----------
/// (*) Halir R., Flusser J. 'Numerically Stable Direct Least Squares Fitting of Ellipses'
/// (**) [Weisstein, Eric W. "Ellipse." From MathWorld--A Wolfram Web Resource](http://mathworld.wolfram.com/Ellipse.html)
/// (***) https://mathworld.wolfram.com/InverseCotangent.html
List<double> fit(List<(double, double)> samples) {
assert(samples.length >= 5,
'Got ${samples.length} samples, 5 or more required.');
// 提取 x-y 对
final x = lapack.Array<double>.fromData(
samples.map((sample) => sample.$1).toList());
final y = lapack.Array<double>.fromData(
samples.map((sample) => sample.$2).toList());
final D1 = lapack.Matrix<double>(samples.length, 3);
final D2 = lapack.Matrix<double>(samples.length, 3);
for (var i = 1; i <= samples.length; i++) {
// 设计矩阵的二次部分 [eqn. 15] from (*)
D1[i][1] = x[i] * x[i];
D1[i][2] = x[i] * y[i];
D1[i][3] = y[i] * y[i];
// 设计矩阵的一次部分 [eqn. 16] from (*)
D2[i][1] = x[i];
D2[i][2] = y[i];
D2[i][3] = 1;
}
// 形成散射矩阵 [eqn. 17] from (*)
final S1 = lapack.Matrix<double>(3, 3);
final S2 = lapack.Matrix<double>(3, 3);
final S3 = lapack.Matrix<double>(3, 3);
// S1 = D1^(T) * D1
lapack.dgemm('Transpose', 'N', 3, 3, samples.length, 1, D1, D1.ld, D1, D1.ld,
0, S1, S1.ld);
// S2 = D1^(T) * D2
lapack.dgemm('Transpose', 'N', 3, 3, samples.length, 1, D1, D1.ld, D2, D2.ld,
0, S2, S2.ld);
// S3 = D2^(T) * D2
lapack.dgemm('Transpose', 'N', 3, 3, samples.length, 1, D2, D2.ld, D2, D2.ld,
0, S3, S3.ld);
// 约束矩阵 [eqn. 18]
final C1 = lapack.Matrix.fromList([
[0.0, 0.0, 2.0],
[0.0, -1.0, 0.0],
[2.0, 0.0, 0.0]
]);
// 减少后的散射矩阵 [eqn. 29]
// M = C1^(-1) * (S1 - S2 * S3^(-1) * S2^(T))
final S11 = S1.copy();
lapack.dgemm(
'N', 'Transpose', 3, 3, 3, -1, dot(S2, inv(S3)), 3, S2, 3, 1, S11, 3);
final M = dot(inv(C1), S11);
// M*|a b c >=l|a b c >. 找到这个方程的特征值和特征向量 [eqn. 28]
const N = 3;
final WR = lapack.Array<double>(N);
final WI = lapack.Array<double>(N);
final VL = lapack.Matrix<double>(N, N);
final eigvec = lapack.Matrix<double>(N, N);
final WORK = lapack.Array<double>(4 * N);
final INFO = lapack.Box(0);
lapack.dgeev('N', 'V', N, M, M.ld, WR, WI, VL, VL.ld, eigvec, eigvec.ld, WORK,
4 * N, INFO);
// 特征向量必须满足约束条件 4ac - b^2 才有效。
final cond =
ScalarMultiply(4) * eigvec.row(1) * eigvec.row(3) - pow(eigvec.row(2), 2);
final col = cond.indexWhere((x) => x > 0);
final a1 = eigvec.col(col);
// |d f g> = -S3^(-1) * S2^(T)*|a b c> [eqn. 24]
final tmp = lapack.Matrix<double>(N, N);
lapack.dgemm('N', 'Transpose', 3, 3, 3, 1, inv(-S3), 3, S2, 3, 1, tmp, 3);
final a2 = lapack.Array<double>(N);
lapack.dgemm(
'N', 'N', 3, 1, 3, 1, tmp, 3, a1.asMatrix(3), 3, 1, a2.asMatrix(3), 3);
// 特征向量 |a b c d f g>
// 描述椭圆的系数列表 [a,b,c,d,e,f]
// 对应于 (*) 中的 ax**2 + bxy + cy**2 + dx + ey + f
return a1 + a2;
}
lapack.Matrix<double> inv(lapack.Matrix<double> A) {
assert(A.dimensions.$1 == A.dimensions.$2);
final ld = A.ld;
final AInv = A.copy();
final IPIV = lapack.Array<int>(ld);
final WORK = lapack.Array<double>(ld);
final INFO = lapack.Box(0);
lapack.dgetrf(ld, ld, AInv, ld, IPIV, INFO);
assert(INFO.value == 0, 'Matrix is numerically singular');
lapack.dgetri(ld, AInv, ld, IPIV, WORK, ld, INFO);
assert(INFO.value == 0, 'Matrix inversion failed');
return AInv;
}
lapack.Matrix<double> dot(lapack.Matrix<double> A, lapack.Matrix<double> B) {
assert(A.dimensions.$2 == B.dimensions.$1);
final K = A.dimensions.$2;
final C = lapack.Matrix<double>(A.dimensions.$1, B.dimensions.$2);
lapack.dgemm('N', 'N', A.dimensions.$1, B.dimensions.$2, K, 1, A, A.ld, B,
B.ld, 0, C, C.ld);
return C;
}
lapack.Array<double> pow(lapack.Array<double> a, num rhs) {
final array = lapack.Array<double>(a.length);
for (var i = 1; i <= a.length; i++) {
array[i] = math.pow(a[i], rhs).toDouble();
}
return array;
}
extension MatrixArray<T> on lapack.Matrix<T> {
lapack.Array<T> row(int i) {
final array = lapack.Array<T>(dimensions.$1);
for (var j = 1; j <= dimensions.$2; j++) {
array[j] = this[i][j];
}
return array;
}
lapack.Array<T> col(int j) {
final array = lapack.Array<T>(dimensions.$2);
for (var i = 1; i <= dimensions.$2; i++) {
array[i] = this[i][j];
}
return array;
}
}
extension MatrixOp on lapack.Matrix<double> {
lapack.Matrix<double> operator -() {
final m = copy();
for (var i = 1; i <= dimensions.$2; i++) {
for (var j = 1; j <= dimensions.$2; j++) {
m[i][j] = -this[i][j];
}
}
return m;
}
}
extension VectorOperations on lapack.Array<double> {
/// 向量积
lapack.Array<double> operator *(lapack.Array<double> rhs) {
assert(length == rhs.length);
final array = lapack.Array<double>(length);
for (var i = 1; i <= length; i++) {
array[i] = this[i] * rhs[i];
}
return array;
}
/// 向量减法
lapack.Array<double> operator -(lapack.Array<double> rhs) {
assert(length == rhs.length);
final array = copy();
for (var i = 1; i <= length; i++) {
array[i] -= rhs[i];
}
return array;
}
}
extension ScalarMultiply on num {
lapack.Array<double> operator *(lapack.Array<double> rhs) {
final array = lapack.Array<double>(rhs.length);
for (var i = 1; i <= rhs.length; i++) {
array[i] = this * rhs[i];
}
return array;
}
}
/// 返回拟合椭圆的局部参数定义
///
/// [center] 是一个元组 (x0, y0)
/// [width] 水平轴的总长度(直径)。
/// [height] 垂直轴的总长度(直径)。
/// [phi] 从 x 轴逆时针旋转到半长轴的角度(弧度)
((double, double) center, double width, double height, double phi) getParams(
List<double> coefficients) {
assert(coefficients.length == 6);
// 特征向量是椭圆的一般形式的系数
// 方程两边除以 2 是为了修正(*)和(**)之间的小差异
// a*x^2 + b*x*y + c*y^2 + d*x + e*y + f = 0 (*) Eqn 1
// a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*f*y + g = 0 (**) Eqn 15
// 我们将使用 (**) 来遵循他们的文档
final a = coefficients[0],
b = coefficients[1] / 2.0,
c = coefficients[2],
d = coefficients[3] / 2.0,
f = coefficients[4] / 2.0,
g = coefficients[5];
// 找到椭圆中心 [eqn.19 and 20] from (**)
final x0 = (c * d - b * f) / (math.pow(b, 2) - a * c),
y0 = (a * f - b * d) / (math.pow(b, 2) - a * c);
final center = (x0, y0);
// 找到半轴长度 [eqn. 21 and 22] from (**)
final numerator = 2 *
(a * math.pow(f, 2) +
c * math.pow(d, 2) +
g * math.pow(b, 2) -
2 * b * d * f -
a * c * g),
denominator1 = (math.pow(b, 2) - a * c) *
(math.sqrt(math.pow((a - c), 2) + 4 * math.pow(b, 2)) - (c + a)),
denominator2 = (math.pow(b, 2) - a * c) *
(-math.sqrt(math.pow((a - c), 2) + 4 * math.pow(b, 2)) - (c + a));
final height = math.sqrt(numerator / denominator1),
width = math.sqrt(numerator / denominator2);
// 半长轴与 x 轴之间的逆时针旋转角度
// [eqn. 23] from (**)
// 使用三角恒等式 eqn 9 form (***)
final phi = switch (b) {
0 when a > c => 0.0,
0 when a < c => math.pi / 2,
_ when a > c => 0.5 * math.atan(2 * b / (a - c)),
_ when a < c => 0.5 * (math.pi + math.atan(2 * b / (a - c))),
// 椭圆是完美的圆形,答案退化
_ => 0.0,
};
return (center, width, height, phi);
}
void main() {
final samples = [
(19.59, 5.00),
(17.71, 5.73),
(16.16, 5.66),
(14.15, 5.34),
(12.14, 5.06),
(10.04, 6.46),
(8.19, 6.86),
(5.55, 7.63),
(4.07, 8.64),
(4.19, 10.20),
(4.66, 11.76),
(5.55, 13.03),
(7.63, 13.53),
(9.41, 12.92),
(10.56, 11.69),
(11.23, 11.39),
(13.26, 10.77),
(15.93, 9.88),
(18.30, 9.57),
(20.67, 9.18),
(22.67, 8.46),
(23.93, 7.44),
(23.55, 5.84),
(21.86, 5.00),
];
final coefficients = fit(samples);
final (
center,
width,
height,
phi,
) = getParams(coefficients);
final svg = File('output.svg');
svg.writeAsStringSync(
'''<svg height="500" width="500" viewBox="0 0 30 15" xmlns="http://www.w3.org/2000/svg">
<g fill="black">
${samples.map((sample) => '<circle cx="${sample.$1}" cy="${sample.$2}" r="0.1" />').join("\n")}
</g>
<g stroke="blue" stroke-width="0.1" fill="transparent">
<ellipse cx="${center.$1}" cy="${center.$2}" rx="$width" ry="$height" transform="rotate(${phi * 180 / math.pi} ${center.$1} ${center.$2})"/>
</g>
</svg>
''');
}
运行结果
上述代码会生成一个 SVG 文件 output.svg
,其中包含以下内容:
- 输入样本点以黑色圆圈表示;
- 拟合的椭圆以蓝色线条表示。
生成的 SVG 文件内容类似如下:
<svg height="500" width="500" viewBox="0 0 30 15" xmlns="http://www.w3.org/2000/svg">
<g fill="black">
<circle cx="19.59" cy="5.00" r="0.1" />
<circle cx="17.71" cy="5.73" r="0.1" />
...
</g>
<g stroke="blue" stroke-width="0.1" fill="transparent">
<ellipse cx="11.00" cy="9.00" rx="5.00" ry="3.00" transform="rotate(45.0 11.00 9.00)" />
</g>
</svg>
更多关于Flutter线性代数计算插件dart_lapack的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
1 回复