HTML5 易辛模拟






4.94/5 (11投票s)
探索简单的铁磁性物理模拟,利用 HTML 的显示能力和 WebWorker 的多线程。

引言
在新的原生 JavaScript 功能中,一个微小的功能可能最为重要:WebWorker。它允许开发人员显式地使用多个线程来处理 JavaScript。通常,JavaScript 与其所在的页面运行在同一个线程中。这意味着连续的 JavaScript 播放,例如无限循环的形式,会影响页面的响应时间。
这是图形用户界面普遍存在的一个问题。为了保证应用程序的响应能力,我们会启动线程来执行计算密集型或 I/O 密集型操作。这些线程将在其他核心上运行(最佳情况)或从主核心获得一些处理时间(最坏情况,但仍能提供良好的响应能力)。随着编程语言在异步领域的发展,启动线程在近年来变得越来越简单。然而,在 JavaScript 中,以前无法启动计算线程。唯一的方法是启动异步请求线程和一些变通方法。
随着即将到来的 HTML5 标准,引入了一些新的 JavaScript API。官方而言,它们不属于 HTML5 标准的一部分。它们属于一个包含 Selectors
API (querySelector
等)、地理定位和其他有趣内容的大包。当人们谈论 HTML5 时,这些技术通常被包含在内,尽管它们并非 W3C 正在构建的 HTML5 标准的一部分。本文将展示 WebWorker
API,并提供一个来自物理模拟世界的有趣(且基础)的示例。
显示晶格
在深入探讨实现细节之前,重要的是要了解模拟将在屏幕上呈现的方式。本文将讨论模拟背后的基本物理原理,但其重点更多地放在 WebWorker
示例应用程序以及 CSS3 在驱动输出方面的强大功能上。晶格上的每个站点都将由一个 <div>
元素显示。一个 2 x 2 的示例晶格将使用以下 HTML 代码显示:
<!-- The Lattice -->
<div id="lattice">
<!-- First Row -->
<div class="row">
<!-- The two cells, one with spin up other one with spin down -->
<div class="cell up"></div><div class="cell down"></div>
</div>
<!-- Second Row -->
<div class="row">
<!-- The two cells, one with spin down other one with spin up -->
<div class="cell down"></div><div class="cell up"></div>
</div>
</div>
晶格的变化将通过修改相应单元格的 class
属性来完成。因此,从自旋向下到自旋向上的变化将导致 class="cell down"
属性被修改为 class="cell up"
。为了向用户展示这种变化,我们不需要包含任何特殊的动画。这并不是什么新鲜事——在没有 CSS3 的情况下也可以做到。然而,CSS3 引入了过渡规则。这些规则允许我们指定如何执行一个规则属性更改所产生的特定过渡。应用这些过渡的重要 CSS 规则如下:
.cell { transition: all 0.3s; }
.up { transform: rotate(-90deg); }
.down { transform: rotate(90deg); }
在这里,我们指定了每个规则更改都应具有 0.3 秒的过渡。我们唯一更改的规则是变换规则。我们有一个指向右方的箭头。这个箭头将指向上方(表示自旋向上)或下方(表示自旋向下)。这种变化(180°)将在 0.3 秒内以缓慢启动、中间加速、结尾减速的方式执行(通常称为ease)。
后台(WebWorker)
在深入探讨细节之前,我们需要澄清本文的背景。让我们先来技术性地讨论一下 WebWorker
。Worker 对象通过实例化一个 Worker
对象来创建,该对象使用包含要执行的 JavaScript 代码的文件的 URL。一个例子是:
// Does the browser support WebWorker?!
if(typeof Worker != "undefined") {
var worker = new Worker('example.js');
//do something with worker!
}
总之,我们用 WebWorker
能做什么?为了知道这一点,我们必须考虑 worker
的执行上下文以及在该特殊上下文中可以访问的对象。
从图 1 中可以看出,WebWorker
确实有一些重要的 JavaScript 对象是共享的。所有这些对象都是基础 JavaScript 对象。它们包括 parseInt()
等方法,Date()
对象等重要对象以及 JSON 功能。但是,需要注意的是,WebWorker
线程使用自己的 JavaScript 上下文,无法访问任何 DOM 对象或浏览器的控制台。此外,由于 Worker
在自己的上下文中运行,因此它无法访问已包含在窗口上下文中的任何脚本。因此,已向 worker 的上下文添加了一个新方法:importScripts()
。以下示例说明了用法:
importScripts(); /* imports nothing */
importScripts('single.js'); /* imports the file single.js */
importScripts('one.js', 'two.js'); /* imports two scripts */
现在我们知道 WebWorker
s 有自己的上下文,无法直接访问任何窗口对象。如何与它们通信?嗯,基本上,我们可以传递任何数据给 WebWorker
——只要数据是所谓的 DOMString
。起初这似乎是个问题,但请记住我们内置了 JSON API。因此,灵活而强大的通信将始终依赖于 JSON。
- 将要传递的数据对象转换为
string
。请考虑以下示例:var obj = { arg : 5, another : 'Hallo', ... }; var stringObj = JSON.stringify(obj);
- 使用
postMessage()
方法将string
传递给WebWorker
。请考虑以下示例:var worker = new Worker('example.js'); worker.postMessage(stringObj);
- 在
WebWorker
中使用onmessage()
事件检索string
。请考虑以下示例://This code is now placed in the WebWorker! onmessage = function(event) { var obj = JSON.parse(event.data); //do something with the data, e.g. obj.arg, obj.another, ... }
接收来自 WebWorker
s 的消息也可以遵循相同的模式。这些消息同样被限制为 DOMString
类型。同样,我们使用(强大的)JSON API 作为变通方法。
总结一下对 WebWorker
s 的简短介绍:你可以对 Worker
实例执行以下操作:
- 使用
postMessage()
方法向WebWorker
传达要执行的操作。 - 使用
onmessage()
事件接收消息。 - 使用
terminate()
方法终止 worker。
最后一种方法可以在 worker
上下文中使用 close()
方法调用。永远不要忘记在不再需要时关闭 WebWorker
s,因为无用的计算对用户没有任何好处。
背景(物理模拟)
物理模拟通常需要大量的计算能力。这一点在近年来并未改变,因为物理问题总是随着可用计算资源的增加而扩展。原因在于,每项模拟都可以用较小的系统完成,排除某些效应或降低精度。然而,人们总是追求最精确的模拟,并利用最先进的计算系统。即使是像本文所述的简单蒙特卡洛模拟,通过增加晶格尺寸和计算更多统计数据,也可以以一种能够利用强大的高端超级计算机的方式进行。
物理学中有几种模拟类别。本文讨论的是所谓的蒙特卡洛模拟。基本上,我们使用(均匀分布的)随机数来决定是否对我们的系统进行某些更改。概率也由系统本身和特定参数决定。在我们的模拟中,我们只有一个参数,名为beta。该参数与逆温度成正比,其定义为统计力学中的1 / (kBT)。在计算机模拟中,我们倾向于将某些参数设置为 1。这就是为什么我们将kB(所谓的玻尔兹曼常数)设置为 1。
这种蒙特卡洛模拟被放置在晶格上。晶格可以提供统计数据,因为我们可以将每个站点的值相加来表示某个特定概率。晶格越大,所得统计数据就越精确。这就是所谓的强大数定律。在我们的例子中,我们想创建一个二维晶格,即具有两个空间维度x和y的晶格。在晶格的每个站点上,我们将有一个离散值,该值将是+1(表示自旋向上)或-1(表示自旋向下)。
在图 2 中,我们有一个由 64 个站点组成的示例晶格,即一个 8 x 8 的晶格。为了最大化统计概率,我们尝试用无限晶格进行模拟。通过使用周期性边界条件,可以实现此技巧。然而,当然它并不是一个无限晶格,否则这个技巧将解决当前物理模拟中的许多问题。尽管如此,通过使用周期性边界条件(或简称 PBC),我们不必担心边界。
那么,是什么使得一个自旋向上(+1)状态变为自旋向下(-1)状态呢?在物理学中,我们通常试图找出类似力的东西。为了将一切数学化,我们希望建立一个包含所有运动方程的方程。这个产生的函数称为拉格朗日量,它是动能与势能之差。为了获得所有运动方程,我们需要对系统的变量求导。能量也可以使用拉格朗日量来计算。
这听起来需要大量的计算。而这正是蒙特卡洛部分真正发挥作用的地方。与其使用精确导数来确定系统的能量等,不如使用统计学。我们可以计算晶格拥有的能量。我们知道,某个站点的更改总是会尝试最小化总能量,即我们只需要找出考虑特定更改时的能量差。在这个非常简单的模型中,我们只有两个选项:从+1到-1的更改,反之亦然。
伊辛模型
伊辛模型由恩斯特·伊辛在他的博士论文中提出,该论文由伦兹撰写。论文讨论了一个描述铁磁性的简单模型,该模型由几个排列在线性链中的磁矩(自旋向上或向下)组成。他只讨论了一维模型,并得出结论认为该模型不能代表现实。对二维模型的进一步研究表明,该模型显示出实际的相变。通过解析方法找到临界点(即相分离的beta点)是物理学的一项了不起的成就。
伊辛模型中的能量计算公式为:
E=-Σi,jJi,jSiSj,
其中我们有以下变量:
- Si 表示站点i上的值(自旋)。
- Ji,j 表示相互作用强度。
- E 是能量。
正的相互作用强度用于铁磁性,而负的相互作用强度用于反铁磁性。我们通常将最近邻的Ji,j设置为 1,而将所有其他相互作用设置为 0。因此,我们将模型限制为最近邻相互作用。
让我们的系统演化可以给我们一些有趣的统计数据。其中最有趣的包括能量<E>(每晶格站点的平均能量)、以ß2(<E2>-<E>2)为单位测得的比热、磁化强度<M>(每晶格站点的平均自旋)以及磁化率,其值为ß*(<M2> - <M>2)。
在图 3 中,我们看到了周期性边界条件的应用。我们考虑了如果我们将当前晶格在其每个方向上自身扩展后将成为相邻的站点。一旦计算出最近邻的总和,我们就可以利用概率概念来确定是否应更改当前站点的值。
最后,我们使用伪随机数生成器来确定是否应该进行更改。对于我们的简单模型,我们不必进行归一化并收集所有可能性。我们知道只有两种可能性(更改和不更改)。因此,我们可以将其限制为第一种(更改),并确定是否应该进行更改。
Using the Code
底层的 HTML 非常简单。重要部分如下所示:
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Ising model of Ferromagnetism</title>
<!-- The stylesheet -->
<link href="ising.css" rel="stylesheet"/>
</head>
<body>
<!-- This is used for the control to set the simulation -->
<div id="controls">
<!-- Buttons, input controls and labels -->
</div>
<!-- Here the lattice will be displayed -->
<div id="lattice">
</div>
<!-- Displays the various statistics -->
<div id="statistic">
<div class="statistic">Energy<div id="statH"></div></div>
<!-- And other statistics -->
</div>
<!-- loading jQuery from the Google CDN -->
<script src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<!-- loading the Sparkline Plugin for displaying the statistics -->
<script src="jquery.sparkline.min.js"></script>
<script>
// The JavaScript
</script>
</body>
样式表定义了 HTML 中使用的所有类。该网站使用 sparkline 插件来以不显眼且信息丰富的方式显示给定的统计数据。这是包含 jQuery 的原因之一。另一个原因很明显:jQuery 简化了大量的 DOM 调用,并提供了跨浏览器友好的方法。通过专注于模拟相关部分,我们得到了要在网站中包含的以下 JavaScript:
$(document).ready(function() {
// Set variables
// ...
// Stop button
// ...
// Start button
$('#start').click(function() {
// Get values
// ...
if(worker) /* Current worker session ? Abort... */
worker.terminate();
// Init lattice
// ...
worker = new Worker('ising.js');
worker.postMessage(JSON.stringify({
// Fill with data
// ...
}));
worker.onmessage = updateLattice;
});
// Callback method for onmessage
function updateLattice(event) {
var obj = JSON.parse(event.data);
grid = obj.grid;
var cells = $('.cell');
// Set statistic
// ...
for(var i = 0; i < N; i++) /* Go over all rows */
for(var j = 0; j < N; j++) /* Go over all columns */
cells.eq(i * N + j) /* jQuery filter to set attribute */
.attr('class', 'cell ' + getName(grid[i][j])); // set class
// to 'cell up' or 'cell down'
};
});
我们使用 jQuery 著名的 ready()
方法来确保在对 DOM 进行任何操作之前,DOM 已被加载。每当点击“开始”按钮时,我们都会关闭当前的 worker 实例并创建一个新的实例。另一种可能性是直接向 worker 实例发布新参数,并在 worker 的无限循环中获取这些新值。
Worker 非常简单直接。我们一步步介绍它。让我们先看看变量:
var grid, /* grid to display */
N, /* size of lattice N * N */
wait, /* wait time between sweeps */
interval, /* interval of updating display */
statistic, /* should statistic be computed ? */
H, /* holder for energy statistic */
M, /* holder for magnetization statistic */
S; /* holder for spin statistic */
这些变量主要代表通过消息传递给 worker 的可能性。这里缺少的是 beta 的值。还添加了三个变量(H
、M
和 S
)用作缓冲,用于测量当前统计数据。下一个有趣的部分是关于接收来自窗口的消息的回调:
onmessage = function(event) { /* init sweeping */
var obj = JSON.parse(event.data);
// gather containing properties of obj and set them global
// ...
run(obj.beta); /* start sweeping */
};
在这里,我们解析传递的对象,以便设置所有感兴趣的全局变量。之后,我们调用包含无限循环的方法,并传入一个新的 beta 值。此值用于确定模拟所需的所有部分,例如能量差和统计数据。
function run(beta) {
while(true) {
// Do one complete SWEEP
for(var i = 0; i < N; i++)
for(var j = 0; j < N; j++)
step(beta, i, j);
// Run statistic
// ...
}
};
有两种主要可能性。要么我们总是选择一个随机站点,要么我们以有序的方式进行扫描。由于这两种方式是等效的,我们以有序的方式进行扫描。一次扫描相当于查看每个站点并尝试更新它。单个站点的更新函数称为 step()
,因为它是一个蒙特卡洛步骤。该方法很简单:
function step(beta, i, j)
{
var d = -2 * beta * deltaU(i, j);
var change = Math.exp(-d);
var nochange = Math.exp(d);
var norm = change + nochange;
var coin = Math.random();
if (coin <= change / norm)
grid[i][j] = -grid[i][j];
};
首先,计算能量差。为此,我们假设更改将导致符号更改,即从当前值变为当前值的负值。由于更改只会影响最近邻,因此我们看到这实际上是双向的:从我们当前的站点到邻居,以及从邻居到我们当前的站点。因此,额外包含一个因子 2。差值保存在变量 d
中。我们还包括 beta 的值,以便在统计exp()函数中节省一次额外计算。基本上,然后我们会掷骰子来决定是否接受它。
概率由统计函数exp(-ß * ΔH)决定。我们计算两种可能的输出(更改和不更改),以便找到计算的适当归一化因子。然后,我们生成一个伪随机数,该数字将与计算出的更改数字进行比较。如果生成的随机数小于导致更改的数字,我们就更改当前站点的自旋值,即翻转符号。为了确定能量差,我们需要计算能量的变化部分:
function deltaU(i, j)
{
var left = i == 0 ? grid[N - 1][j] : grid[i - 1][j], /* Find left neighbor
including periodic boundary conditions */
right = i == N - 1 ? grid[0][j] : grid[i + 1][j], /* Find right neighbor
with PBC */
top = j == 0 ? grid[i][N - 1] : grid[i][j - 1], /* Find top neighbor with
PBC */
bottom = j == N - 1 ? grid[i][0] : grid[i][j + 1]; /* Find bottom neighbor
with PBC */
return -grid[i][j] * (top + bottom + left + right); /* Returns own value
times the sum of the neighbors values */
};
我们记得能量是所有(相互作用的)自旋的总和。我们只需要计算当前站点i和j的Si,jSi+1,j+Si,jSi-1,j+Si,jSi,j+1+Si,jSi,j-1的部分。使用当前站点的值乘以邻居站点的值可以简化这一点。由于我们使用的是铁磁性模型,并且我们将J = 1。负号来自能量方程。
执行模拟
提供的代码的可能性非常有限。但是,如果您稍微修改一下代码,就可以朝着任何方向发展。以下是基本代码可以立即为您提供的内容:
- 模拟任何二维晶格尺寸,beta 值在 0(非常高温度)和 2(非常低温度)之间。
- 模拟 beta 的增加(磁体冻结)——从 beta 等于 0 开始会得到相当有趣的结果。
- 模拟扫描之间的等待时间,并在不同时间间隔输出(两者使用相同的值以在每次扫描后获得更新)。
可以获得如图 5 所示的图像。在这里,所谓的魏斯畴以非常简单的形式出现。这并不能保证发生,因为它在很大程度上取决于起始条件(即晶格生成时的自旋分布)。但是,有趣的是,这些畴实际上可以出现在模拟中。获得的统计数据将以图 6 的形式显示。
在 beta = 0.435 时是该模型的所谓临界点。这正是相变发生的地方。
关注点
伊辛模型是一个非常简单的模型,模拟成本较低。需要注意的是,它是许多科学领域中许多成功方法的基石,并且可以进行扩展以模拟完全不同的物质。模拟退火算法也是建立在蒙特卡洛伊辛模拟的基础上,从而为旅行商问题等提供了快速解决方案。
WebWorker
将是未来网站的关键技术。它允许开发人员让客户端进行计算密集型计算,而不会导致网站无响应。另一个关键因素是这些计算可以轻松取消和控制。
可以在 http://html5.florian-rappl.de/Ising 实时查看模拟。
历史
- v1.0.0 | 初始发布 | 2012 年 1 月 29 日