[机器学习]基于tensorflow.js的k-means聚类分析

2020-12-02 11:18:28 浏览数 (1)

项目说明:

A Simple JavaScript Library to make it easy for people to use KMeans algorithms with Tensorflow JS.

The library was born out of another project in which except KMeans, our code completely depended on TF.JS

As such, moving to TF.JS helped standardise our code base substantially and reduce dependency on other libraries

需要安装的依赖库:“@tensorflow/tfjs-core”

代码语言:javascript复制
npm install “@tensorflow/tfjs-core” -g

调用程序Demo:

代码语言:javascript复制
//const KMeans = require("tf-kmeans");
const KMeans = require("../lib/index.js");
const tf = require("@tensorflow/tfjs");
function SyncTest() {
  tf.tidy(() => {
    const kmeans = new KMeans.default({
      k: 2,
      maxIter: 30,
      distanceFunction: KMeans.default.EuclideanDistance
    });
    const dataset = tf.tensor([[2, 2, 2], [5, 5, 5], [3, 3, 3], [4, 4, 4], [7, 8, 7]]);
    const predictions = kmeans.Train(
      dataset
    );

    console.log("Assigned To ", predictions.arraySync());
    console.log("Centroids Used are ", kmeans.Centroids().arraySync());
    console.log("Prediction for Given Value is");
    kmeans.Predict(tf.tensor([2, 3, 2])).print();
    console.log("Amount of Memory Used is ", tf.memory());
    // Use this In case kmeans not executed in Tidy Function
    kmeans.Dispose();
    predictions.dispose();
    dataset.dispose();
  });
}

async function AsyncTest() {
  const kmeans = new KMeans.default({
    k: 3,
    maxIter: 30,
    distanceFunction: KMeans.default.EuclideanDistance
  });
  const dataset = tf.tensor([[2, 2, 2], [5, 5, 5], [3, 3, 3], [4, 4, 4], [7, 8, 7]]);

  console.log("nnAsync Test");
  const predictions = await kmeans.TrainAsync(
    dataset,
    // Called At End of Every Iteration
    async(iter, centroid, preds)=>{
      console.log("===");
      console.log("Iteration Count", iter);
      console.log("Centroid ", await centroid.array());
      console.log("Prediction ", await preds.array());
      console.log("===");
      // You could instead use TFVIS for Plotting Here
    }
  );
  console.log("Assigned To ", await predictions.array());
  console.log("Centroids Used are ", await kmeans.Centroids().array());
  console.log("Prediction for Given Value is");
  kmeans.Predict(tf.tensor([2, 3, 2])).print();
  console.log("Amount of Memory Used is ", tf.memory());

  kmeans.Dispose();
  predictions.dispose();
  dataset.dispose();
}

SyncTest();

AsyncTest().then(() => console.log("Hi"));

输出的结果:

代码语言:javascript复制
[Running] node "d:tf-kmeans-mastersamplesindex.js"

============================
Hi there ?. Looks like you are running TensorFlow.js in Node.js. To speed things up dramatically, install our node backend, which binds to TensorFlow C  , by running npm i @tensorflow/tfjs-node, or npm i @tensorflow/tfjs-node-gpu if you have CUDA. Then call require('@tensorflow/tfjs-node'); (-gpu suffix for CUDA) at the start of your program. Visit https://github.com/tensorflow/tfjs-node for more details.
============================
Assigned To  [ 1, 0, 1, 0, 0 ]
Centroids Used are  [ [ 5.333333492279053, 5.666666507720947, 5.333333492279053 ],
  [ 2.5, 2.5, 2.5 ] ]
Prediction for Given Value is
Tensor
    [1]
Amount of Memory Used is  { unreliable: true,
  reasons:
   [ 'The reported memory is an upper bound. Due to automatic garbage collection, the true allocated memory may be less.' ],
  numTensors: 7,
  numDataBuffers: 7,
  numBytes: 160 }


Async Test
===
Iteration Count 0
Centroid  [ [ 4.5, 4.5, 4.5 ], [ 7, 8, 7 ], [ 2.5, 2.5, 2.5 ] ]
Prediction  [ 2, 0, 2, 0, 1 ]
===
Assigned To  [ 2, 0, 2, 0, 1 ]
Centroids Used are  [ [ 4.5, 4.5, 4.5 ], [ 7, 8, 7 ], [ 2.5, 2.5, 2.5 ] ]
Prediction for Given Value is
Tensor
    [2]
Amount of Memory Used is  { unreliable: true,
  reasons:
   [ 'The reported memory is an upper bound. Due to automatic garbage collection, the true allocated memory may be less.' ],
  numTensors: 6,
  numDataBuffers: 6,
  numBytes: 152 }
Hi

[Done] exited with code=0 in 0.304 seconds

对库函数的说明:

Functions

  1. Constructor Takes 3 Optional parameters
    1. k:- Number of Clusters
    2. maxIter:- Max Iterations
    3. distanceFunction:- The Distance function Used Currently only Eucledian Distance Provided
  2. Train Takes Dataset as Parameter Performs Training on This Dataset Sync callback function is optional
  3. TrainAsync Takes Dataset as Parameter Performs Training on This Dataset Also takes async callback function called at the end of every iteration
  4. Centroids Returns the Centroids found for the dataset on which KMeans was Trained
  5. Predict Performs Predictions on the data Provided as Input

通过typescript编译后的k-means库:

代码语言:javascript复制
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label  ; return { value: op[1], done: false };
                case 5: _.label  ; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
exports.__esModule = true;
var tf = require("@tensorflow/tfjs-core");
var KMeans = /** @class */ (function () {
    function KMeans(_a) {
        var _b = _a === void 0 ? {} : _a, _c = _b.k, k = _c === void 0 ? 2 : _c, _d = _b.maxIter, maxIter = _d === void 0 ? 10 : _d, _e = _b.distanceFunction, distanceFunction = _e === void 0 ? KMeans.EuclideanDistance : _e;
        this.k = 2;
        this.maxIter = 200;
        this.distanceFunction = KMeans.EuclideanDistance;
        this.k = k;
        this.maxIter = maxIter;
        this.distanceFunction = distanceFunction;
    }
    KMeans.EuclideanDistance = function (values, centroids) {
        return tf.tidy(function () { return values.squaredDifference(centroids).sum(1).sqrt(); });
    };
    KMeans.prototype.GenerateIndices = function (rows) {
        var indices = [];
        indices.length = rows;
        for (var i = 0; i < indices.length;   i)
            indices[i] = i;
        return indices;
    };
    KMeans.prototype.NewCentroidSingle = function (values, assignments, cluster, rows) {
        return tf.tidy(function () {
            // Make All Values Of Array to be of Same Size as Our Cluster
            var selectedIndices = [];
            selectedIndices.length = rows;
            selectedIndices = selectedIndices.fill(cluster);
            var selectedIndicesT = tf.tensor(selectedIndices);
            var where = tf.equal(assignments, selectedIndicesT).asType("int32");
            where = where.reshape([where.shape[0], 1]);
            var count = where.sum();
            var newCentroid = values.mul(where).sum(0).div(count);
            return newCentroid;
        });
    };
    KMeans.prototype.NewCentroids = function (values, assignments) {
        var _this = this;
        return tf.tidy(function () {
            var rows = values.shape[0];
            var centroids = [];
            for (var cluster = 0; cluster < _this.k;   cluster) {
                centroids.push(_this.NewCentroidSingle(values, assignments, cluster, rows));
            }
            return tf.stack(centroids);
        });
    };
    KMeans.prototype.AssignCluster = function (value, centroids) {
        var _this = this;
        return tf.tidy(function () { return _this.distanceFunction(value, centroids).argMin(0); });
    };
    KMeans.prototype.AssignClusters = function (values, centroids) {
        var _this = this;
        return tf.tidy(function () {
            var rows = values.shape[0];
            var minIndexes = [];
            for (var _i = 0, _a = _this.GenerateIndices(rows); _i < _a.length; _i  ) {
                var index = _a[_i];
                var value = values.gather(index);
                minIndexes.push(_this.AssignCluster(value, centroids));
                value.dispose();
            }
            return tf.stack(minIndexes);
        });
    };
    KMeans.prototype.RandomSample = function (vals) {
        var _this = this;
        return tf.tidy(function () {
            var rows = vals.shape[0];
            if (rows < _this.k)
                throw new Error("Rows are Less than K");
            var indicesRaw = tf.util.createShuffledIndices(rows).slice(0, _this.k);
            var indices = [];
            indicesRaw.forEach(function (index) { return indices.push(index); });
            // Extract Random Indices
            return tf.gatherND(vals, tf.tensor(indices, [_this.k, 1], "int32"));
        });
    };
    KMeans.prototype.CheckCentroidSimmilarity = function (newCentroids, centroids, vals) {
        var _this = this;
        return tf.tidy(function () { return newCentroids
            .equal(centroids)
            .asType("int32")
            .sum(1)
            .div(vals.shape[1])
            .sum()
            .equal(_this.k)
            .dataSync()[0]; });
    };
    KMeans.prototype.TrainSingleStep = function (values) {
        var _this = this;
        return tf.tidy(function () {
            var predictions = _this.Predict(values);
            var newCentroids = _this.NewCentroids(values, predictions);
            return [newCentroids, predictions];
        });
    };
    KMeans.prototype.Train = function (values, callback) {
        if (callback === void 0) { callback = function (_centroid, _predictions) { }; }
        this.centroids = this.RandomSample(values);
        var iter = 0;
        while (true) {
            var _a = this.TrainSingleStep(values), newCentroids = _a[0], predictions = _a[1];
            var same = this.CheckCentroidSimmilarity(newCentroids, this.centroids, values);
            if (same || iter >= this.maxIter) {
                newCentroids.dispose();
                return predictions;
            }
            this.centroids.dispose();
            this.centroids = newCentroids;
              iter;
            callback(this.centroids, predictions);
        }
    };
    KMeans.prototype.TrainAsync = function (values, callback) {
        var _this = this;
        if (callback === void 0) { callback = function (_iter, _centroid, _predictions) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
            return [2 /*return*/];
        }); }); }; }
        return __awaiter(this, void 0, void 0, function () {
            var iter, _a, newCentroids, predictions, same;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        this.centroids = this.RandomSample(values);
                        iter = 0;
                        _b.label = 1;
                    case 1:
                        if (!true) return [3 /*break*/, 3];
                        _a = this.TrainSingleStep(values), newCentroids = _a[0], predictions = _a[1];
                        same = this.CheckCentroidSimmilarity(newCentroids, this.centroids, values);
                        if (same || iter >= this.maxIter) {
                            newCentroids.dispose();
                            return [2 /*return*/, predictions];
                        }
                        this.centroids.dispose();
                        this.centroids = newCentroids;
                        return [4 /*yield*/, callback(iter, this.centroids, predictions)];
                    case 2:
                        _b.sent();
                          iter;
                        return [3 /*break*/, 1];
                    case 3: return [2 /*return*/];
                }
            });
        });
    };
    KMeans.prototype.Predict = function (y) {
        var _this = this;
        return tf.tidy(function () {
            if (y.shape[1] == null)
                y = y.reshape([1, y.shape[0]]);
            return _this.AssignClusters(y, _this.centroids);
        });
    };
    KMeans.prototype.Centroids = function () {
        return this.centroids;
    };
    KMeans.prototype.Dispose = function () {
        this.centroids.dispose();
    };
    return KMeans;
}());
exports["default"] = KMeans;

编译前:

代码语言:javascript复制
import * as tf from "@tensorflow/tfjs-core";

export default class KMeans {
    public k: number = 2;
    public maxIter: number = 200;
    public distanceFunction = KMeans.EuclideanDistance;
    public centroids!: tf.Tensor;

    public constructor({ k = 2, maxIter = 10, distanceFunction = KMeans.EuclideanDistance } = {}) {
        this.k = k;
        this.maxIter = maxIter;
        this.distanceFunction = distanceFunction;
    }

    public static EuclideanDistance(values: tf.Tensor, centroids: tf.Tensor) {
        return tf.tidy(() => values.squaredDifference(centroids).sum(1).sqrt());
    }
    private GenerateIndices(rows: number) {
        const indices: number[] = [];
        indices.length = rows;
        for (let i = 0; i < indices.length;   i)
            indices[i] = i;
        return indices;
    }
    private NewCentroidSingle(values: tf.Tensor, assignments: tf.Tensor, cluster: number, rows: number) {
        return tf.tidy(() => {
            // Make All Values Of Array to be of Same Size as Our Cluster
            let selectedIndices: number[] = [];
            selectedIndices.length = rows;
            selectedIndices = selectedIndices.fill(cluster);
            const selectedIndicesT = tf.tensor(selectedIndices);

            let where = tf.equal(assignments, selectedIndicesT).asType("int32");
            where = where.reshape([where.shape[0], 1]);
            const count = where.sum();

            const newCentroid = values.mul(where).sum(0).div(count)
            return newCentroid;
        })
    }
    private NewCentroids(values: tf.Tensor, assignments: tf.Tensor) {
        return tf.tidy(() => {
            const rows = values.shape[0];
            const centroids: tf.Tensor[] = [];
            for (let cluster = 0; cluster < this.k;   cluster) {
                centroids.push(this.NewCentroidSingle(values, assignments, cluster, rows));
            }
            return tf.stack(centroids);
        });
    }
    private AssignCluster(value: tf.Tensor, centroids: tf.Tensor) {
        return tf.tidy(() => this.distanceFunction(value, centroids).argMin(0));
    }
    private AssignClusters(values: tf.Tensor, centroids: tf.Tensor) {
        return tf.tidy(() => {
            const rows = values.shape[0];
            const minIndexes: tf.Tensor[] = [];
            for (const index of this.GenerateIndices(rows)) {
                const value = values.gather(index);
                minIndexes.push(this.AssignCluster(value, centroids));
                value.dispose();
            }
            return tf.stack(minIndexes);
        });
    }
    private RandomSample(vals: tf.Tensor) {
        return tf.tidy(() => {
            const rows = vals.shape[0];
            if (rows < this.k)
                throw new Error("Rows are Less than K");

            const indicesRaw = tf.util.createShuffledIndices(rows).slice(0, this.k);
            const indices: number[] = [];
            indicesRaw.forEach((index: number) => indices.push(index))
            // Extract Random Indices
            return tf.gatherND(vals, tf.tensor(indices, [this.k, 1], "int32"))
        })
    }
    private CheckCentroidSimmilarity(newCentroids: tf.Tensor, centroids: tf.Tensor, vals: tf.Tensor) {
        return tf.tidy(() => newCentroids
            .equal(centroids)
            .asType("int32")
            .sum(1)
            .div(vals.shape[1]!)
            .sum()
            .equal(this.k)
            .dataSync()[0]
        );
    }
    private TrainSingleStep(values: tf.Tensor) {
        return tf.tidy(() => {
            const predictions = this.Predict(values);
            const newCentroids = this.NewCentroids(values, predictions);
            return [newCentroids, predictions];
        });
    }
    public Train(values: tf.Tensor, callback = (_centroid: tf.Tensor, _predictions: tf.Tensor) => { }) {
        this.centroids = this.RandomSample(values);
        let iter = 0;
        while (true) {
            let [newCentroids, predictions] = this.TrainSingleStep(values);
            const same = this.CheckCentroidSimmilarity(newCentroids, this.centroids, values);
            if (same || iter >= this.maxIter) {
                newCentroids.dispose();
                return predictions;
            }
            this.centroids.dispose();
            this.centroids = newCentroids;
              iter;
            callback(this.centroids, predictions);
        }
    }
    public async TrainAsync(values: tf.Tensor, callback = async (_iter: number, _centroid: tf.Tensor, _predictions: tf.Tensor) => { }) {
        this.centroids = this.RandomSample(values);
        let iter = 0;
        while (true) {
            let [newCentroids, predictions] = this.TrainSingleStep(values);
            const same = this.CheckCentroidSimmilarity(newCentroids, this.centroids, values);
            if (same || iter >= this.maxIter) {
                newCentroids.dispose();
                return predictions;
            }
            this.centroids.dispose();
            this.centroids = newCentroids;
            await callback(iter, this.centroids, predictions);
              iter;
        }
    }
    public Predict(y: tf.Tensor) {
        return tf.tidy(() => {
            if (y.shape[1] == null)
                y = y.reshape([1, y.shape[0]]);
            return this.AssignClusters(y, this.centroids);
        });
    }
    public Centroids() {
        return this.centroids;
    }
    public Dispose() {
        this.centroids.dispose();
    }
}

先不说了,广告时间又到了,现在植入广告:几个《传热学》相关的小程序总结如下,可在微信中点击体验:

  1. 有限元三角单元网格自动剖分
  2. Delaunay三角化初体验 (理论戳这)
  3. Contour等值线绘制 (理论戳这)
  4. 2D非稳态温度场有限元分析
  5. 1D稳态导热温度场求解 (源码戳这)
  6. 1D非稳态导热温度场求解程序 (源码戳这)
  7. 2D稳态导热温度场求解 (源码戳这)
  8. 普朗克黑体单色辐射力

《传热学》相关小程序演示动画如下(其中下图1D非稳态导热计算发散,调小时间步长后重新计算,结果收敛!):

黑体单色辐射力如下图,可见温度越高,同频率辐射力越大:

《(计算)流体力学》中的几个小程序,可在微信中点击体验:

  1. Blasius偏微分方程求解速度边界层 (理论这里)
  2. 理想流体在管道中的有势流动 (源码戳这)
  3. 涡量-流函数法求解顶驱方腔流动 (源码戳这)
  4. SIMPLE算法求解顶驱方腔流动 (源码戳这)
  5. Lattice Boltzmann Method计算绕流演示(参考源码)
  6. 流体力学实验在线演示进行演示

关于《(计算)流体力学》相关的几个小程序演示动画如下:

LBM(=Lattice Boltzmann Method)计算得到的圆柱绕流“卡门涡街”演示(由于网格较少,分辨率低,圆柱近乎正方形):

顺便,《(热工过程)自动控制》中关于PID控制器的仿真可点击此处体验:PID控制演示小程序,(PID控制相关视频见:基础/整定/重要补充)。动画如下:

(正文完!)

现将往期内容制成目录,内容如下:

1 前言(已完成)

2 HTML5 基础(已完成)

2.1 开发平台搭建(已完成)

2.2 HTML5基础入门(已完成)

2.2.1 js基础(已完成)

2.2.2 HTML标签简介(已完成)

2.2.3 文档对象模型DOM及表单(已完成)

2.2.4 HTML5 Canvas绘图基础(已完成)

2.2.5 HTML5程序调试(已完成)

2.2.6 第三方js类库(已完成)

2.2.7 webAssemble简介/工具链配置/应用DemoCode

2.3 简单网页编写Primer(已完成)

2.3.1 基于easyUI(已完成)

2.3.2 基于bootstrap(已完成)

2.3.3 Wrap it up!(已完成)

2.4 电脑/手机客户端开发简介(已完成)

2.5 node.js回首望(已完成)

3 基于HTML5的数据可视化(已完成)

3.1 Contour绘制(已完成)

3.1.1 借助显卡GPU绘制Contour(已完成)

3.1.2 使用绘图API绘制Contour的思路(已完成)

3.1.3 绘制三维Contour图的思路(已完成)

3.2 矢量图的绘制(已完成)

3.3 绘制曲线(已完成)

3.4 js生成报表(已完成)

4 高等数学中若干简单数值计算算例(已完成)

4.1 数值积分、高等函数绘制(已完成)

4.2 非线性方程求解(已完成)

4.3 差分与简单常微分方程初值问题(已完成)

5 使用HTML5编程实现热传导温度场求解(已完成)

5.1 一维导热算例(已完成)

5.1.1一维无内热源温度场数值模拟(基于基于HTML5编程)(已完成)

5.1.2 一维非稳态无内热源导热程序(已完成)

5.2 二维导热算例-综述(已完成)

5.2.1 二维导热算例-热导的概念(已完成)

5.2.2 二维导热算例-迭代计算(已完成)

5.2.3 二维导热算例-整体架构(已完成)

5.2.4 二维无内热源稳态导热程序(已完成)

5.2.5.1 webGL显式迭代计算温度场的shader[显卡风扇不能停]

5.2.5.2 webGL隐式迭代计算温度场的shader[显卡风扇不能停]

5.3 几个传热学视频

5.3.1 [视频]导热控制偏微分方程

5.3.2 [视频]一维肋的稳态导热温度场求解

5.3.3 [视频]集中参数法求解集总体的非稳态温度场

5.3.4 [视频]热传导问题的数值解法

5.3.5 [视频]二维常物性不可压流体对流换热问题的数学描述

5.3.6 [视频]两个封闭系统辐射换热计算

5.4 Wrap it up!(已完成)

6 工程流体力学(已完成)

6.1 理想流体的简单势流计算(已完成)

6.2 粘性流体涡量-流函数算法(已完成)

6.3 SIMPLE算法(已完成)

6.4 投影算法(已完成)

6.5 边界层-Blasius方程的求解(已完成)

6.6 开源软件与商业软件(已完成)

7 小型制冷设计(已完成)

7.1 使用js多快好省绘制简单CAD图纸(已完成)

7.1.1 二维图纸绘制(已完成)

7.1.2 三维图纸绘制(已完成)

7.2 冷凝器算例(已完成)

7.2.1 需求分析及前端界面(已完成)

7.2.2 计算程序(已完成)

7.2.3 图纸输出(已完成)

7.3 蒸发器算例(已完成)

8 热工过程自动控制(已完成)

8.1 时域分析与频域分析(已完成)

8.2 汽包锅炉水位自动控制(已完成)

8.2a 数字PID控制示例,以液位控制为例

8.3 串口读写(已完成)

8.4 PID控制器三部分:基础/整定/重要补充(已完成)

9 物联网(已完成)

10 机器学习(已完成)

11 虚拟现实(已完成)

11.1 webVR/AR:webGL的副业

Where to go from here?(已完成)

[python从入门到放弃系列]

Python基本命令、函数、数据结构

8个常用Python库从安装到应用

python API操作tecplot做数据处理(已完成)

用pyautogui批量输入表单(已完成)

推公式sympy(已完成)

基于百度OCR的文字识别(已完成)

pyautogui acrobat去PDF水印一例(已完成)

[瞎侃系列]

平行宇宙引-双缝干涉实验-量子纠缠态

Gmsh使用教程

不服跑个分!-解Laplace偏微分方

《传热学/流体力学》中几个简单演示程序

LBM计算卡门涡街绕流

0 人点赞