<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {background:[[ColorPalette::PrimaryMid]];}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected{color:[[ColorPalette::PrimaryDark]];
	background:[[ColorPalette::TertiaryPale]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
	border:1px solid [[ColorPalette::PrimaryMid]];}
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}

.wizard .notChanged {background:transparent;}
.wizard .changedLocally {background:#80ff80;}
.wizard .changedServer {background:#8080ff;}
.wizard .changedBoth {background:#ff8080;}
.wizard .notFound {background:#ffff80;}
.wizard .putToServer {background:#ff80ff;}
.wizard .gotFromServer {background:#80ffff;}

#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
.sparktick {background:[[ColorPalette::PrimaryDark]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}

.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}
.readOnly {background:[[ColorPalette::TertiaryPale]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity=60)';}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0px; top:0px;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0px; padding-bottom:0px;}

.fieldsetFix {border:0; padding:0; margin:1px 0px;}

.sparkline {line-height:1em;}
.sparktick {outline:0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
#displayArea {margin: 1em 1em 0em;}
noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
}
/*}}}*/
<!--{{{-->
<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
<div class='headerShadow'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</div>
<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser excludeLists'></span></div>
<!--}}}-->
To get started with this blank [[TiddlyWiki]], you'll need to modify the following tiddlers:
* [[SiteTitle]] & [[SiteSubtitle]]: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
* [[MainMenu]]: The menu (usually on the left)
* [[DefaultTiddlers]]: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
You'll also need to enter your username for signing your edits: <<option txtUserName>>
These [[InterfaceOptions]] for customising [[TiddlyWiki]] are saved in your browser

Your username for signing your edits. Write it as a [[WikiWord]] (eg [[JoeBloggs]])

<<option txtUserName>>
<<option chkSaveBackups>> [[SaveBackups]]
<<option chkAutoSave>> [[AutoSave]]
<<option chkRegExpSearch>> [[RegExpSearch]]
<<option chkCaseSensitiveSearch>> [[CaseSensitiveSearch]]
<<option chkAnimate>> [[EnableAnimations]]

----
Also see [[AdvancedOptions]]
<<importTiddlers>>
//{{{
[
	{
		"pluginName": "$tw.VPM",
		"pluginScripts": [
			{
				"main": "nodejs/$tw.util.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.util.min",
				"description": "Plugin for general utilities.",
				"required": false,
				"toLoad": false,
				"developing": true,
				"deActivated": false
			},
			{
				"main": "$tw.ve.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.min",
				"description": "Plugin for view mode editing features in TiddlyWiki.",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.LaTex.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.LaTex.min",
				"description": "Plugin for LaTex formula handling.<br>MUST have a real LaTex rendering engine, such as MathJax or KaTex, to work properly.",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.3D.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.3D.min",
				"description": "Plugin for 3D graphing in TiddlyWiki. (Default: THREE.js.)",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.data.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.data.min",
				"description": "Plugin for data processing in TiddlyWiki. (Default: D3.js.)",
				"required": false,
				"developing": true,
				"toLoad": true
			},
			{
				"main": "$tw.numeric.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.min",
				"description": "Plugin for numeric calculatons.",
				"required": false,
				"toLoad": true,
				"developing": true,
				"deActivated": false
			},
			{
				"main": "$tw.physics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.min",
				"description": "Plugin for physics simulation.",
				"required": false,
				"toLoad": true,
				"developing": true,
				"deActivated": false
			},
			{
				"name": "Parallel.js",
				"main": "js/Parallel-transferable.js",
				"fallback": "https://unpkg.com/paralleljs@1.0/lib/parallel.js",
				"website": "https://parallel.js.org/",
				"description": "The Parallel.js main source.",
				"developing": true,
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.ve",
		"pluginScripts": [
			{
				"main": "$tw.ve.core.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.core.min",
				"description": "The core of $tw.ve.",
				"developing": true,
				"required": true,
				"toLoad": true
			},
			{
				"main": "$tw.ve.extra.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.extra.min",
				"description": "The extended features of $tw.ve.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"main": "$tw.ve.table.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.table.min",
				"description": "View mode editing for tables in $tw.ve.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"main": "$tw.ve.tcalc.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.min",
				"description": "Spreadsheet features in $tw.ve.",
				"developing": true,
				"required": false,
				"toLoad": true
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.LaTex",
		"pluginScripts": [
			{
				"name": "MathJax",
				"main": "$tw.LaTex.MathJax.js",
				"fallback": "https://twve.tiddlyspot.com/#$tw.LaTex.MathJax.min",
				"description": "Use MathJax as the rendering engine.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"name": "KaTex",
				"main": "$tw.LaTex.KaTex.js",
				"fallback": "https://twve.tiddlyspot.com/#$tw.LaTex.KaTex.min",
				"description": "Use KaTex as the rendering engine.",
				"developing": true,
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.3D",
		"pluginScripts": [
			{
				"name": "THREE.js",
				"main": "<script type='module'>import * as THREE from 'three';$tw.threeD.THREE=THREE;</script>",
				"description": "The THREE.js main source.",
				"required": true,
				"toLoad": true
			},
			{
				"main": "<script type='module'>import {CSS3DRenderer, CSS3DObject} from 'three/addons/renderers/CSS3DRenderer.js';$tw.threeD.CSS3DRenderer=CSS3DRenderer;$tw.threeD.CSS3DObject=CSS3DObject;</script>",
				"description": "The CSS 3D renderer.",
				"required": false,
				"toLoad": true
			},
			{
				"main": "<script type='module'>import {TrackballControls} from 'three/addons/controls/TrackballControls.js';$tw.threeD.TrackballControls = TrackballControls;</script>",
				"description": "The track ball control handler.",
				"required": false,
				"toLoad": true
			},
			{
				"main": "js/controls/FlyControls.min.js",
				"description": "The fly control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/MouseControls.min.js",
				"description": "The mouse control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/DragControls.min.js",
				"description": "The drag-and-drop handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/VRControls.min.js",
				"description": "The VR control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "<script type='module'>import {CSS3DRenderer} from 'https://unpkg.com/three@0.161.0/examples/jsm/renderers/CSS3DRenderer.js';$tw.threeD.CSS3DRenderer=CSS3DRenderer;</script>",
				"description": "The CSS 3D renderer.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "<script type='module'>import {TrackballControls} from 'https://unpkg.com/three@0.161.0/examples/jsm/controls/TrackballControls.js';$tw.threeD.TrackballControls = TrackballControls;</script>",
				"description": "The track ball control handler.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "<script type='module'>import {CSS3DRenderer} from 'https://unpkg.com/three@0.161.0/examples/jsm/renderers/CSS3DRenderer.js';$tw.threeD.CSS3DRenderer = CSS3DRenderer;</script>",
				"description": "The CSS 3D renderer.",
				"required": false,
				"toLoad": false
			},
			{
				"main": "js/controls/TrackballControls.min.js",
				"description": "The track ball control handler.",
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.data",
		"pluginScripts": [
			{
				"name": "plotly.js",
				"main": "https://cdn.plot.ly/plotly-2.31.1.min.js",
				"fallback": "plotly.min.js",
				"description": "The plotly.js library.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "Apache ECharts",
				"main": "https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js",
				"fallback": "echarts.min.js",
				"description": "The Apache ECharts library.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "D3.js",
				"main": "https://d3js.org/d3.v7.min.js",
				"fallback": "d3.min.js",
				"description": "The D3 main source.",
				"required": true,
				"toLoad": true
			},
			{
				"main": "$tw.data.TypedArray.js",
				"fallback": "https://twve.tiddlyspot.com/#$tw.data.TypedArray.min",
				"description": "The typed array module of $tw.data.",
				"developing": true,
				"required": false,
				"toLoad": true
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.numeric",
		"pluginScripts": [
			{
				"main": "$tw.numeric.ODE.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.ODE.min",
				"description": "The ODE solver of $tw.numeric.",
				"developing": true,
				"required": false,
				"toLoad": false,
				"deActivated": true
			},
			{
				"main": "$tw.numeric.FFT.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.numeric.FFT.min",
				"description": "The Fast Fourier Transform (FFT) module of $tw.numeric.",
				"developing": true,
				"required": false,
				"toLoad": false,
				"deActivated": true
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.physics",
		"pluginScripts": [
			{
				"main": "$tw.physics.electrodynamics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.electrodynamics.min",
				"description": "The physics simulation codes for electrodynamics.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"main": "$tw.physics.quantum.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.quantum.min",
				"description": "The physics simulation codes for quantum.",
				"developing": true,
				"required": false,
				"deActivated": true,
				"toLoad": false
			},
			{
				"main": "$tw.physics.statisticaldynamics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.statisticaldynamics.min",
				"description": "The physics simulation codes for statistical dynamics.",
				"developing": true,
				"required": false,
				"deActivated": true,
				"toLoad": false
			},
			{
				"main": "$tw.physics.thermodynamics.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.physics.thermodynamics.min",
				"description": "The physics simulation codes for thermodynamics.",
				"developing": true,
				"required": false,
				"deActivated": true,
				"toLoad": false
			}
		],
		"debugging": false
	}
]
//}}}
//{{{
[
	{
		"pluginName": "$tw.ve.table",
		"pluginScripts": [
			{
				"main": "$tw.ve.tablelarge.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tablelarge.min",
				"description": "Large table support in $tw.ve.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "TableSortingPlugin",
				"main": "TableSortingPlugin.min.js",
				"fallback": "",
				"description": "Add table sorting feature in $tw.ve.",
				"required": false,
				"toLoad": false
			},
			{
				"name": "SortableGridPlugin",
				"main": "SortableGridPlugin.min.js",
				"fallback": "",
				"description": "Add table sorting feature in $tw.ve.",
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.ve.tcalc",
		"pluginScripts": [
			{
				"name": "Date/Time Extension",
				"main": "$tw.ve.tcalc.datetime.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.datetime.min",
				"description": "Date/Time functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"name": "Statistics Extension",
				"main": "$tw.ve.tcalc.stats.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.stats.min",
				"description": "Statistics functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": true
			},
			{
				"name": "Bookkeeping Extension",
				"main": "$tw.ve.tcalc.bkkp.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.bkkp.min",
				"description": "Daily life bookkeeping functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": false
			},
			{
				"name": "Financial Extension",
				"main": "$tw.ve.tcalc.fin.js",
				"fallback": "http://twve.tiddlyspot.com/#$tw.ve.tcalc.fin.min",
				"description": "Financial functions extension of $tw.ve.tcalc.",
				"developing": true,
				"required": false,
				"toLoad": false
			}
		],
		"debugging": false
	},
	{
		"pluginName": "$tw.LaTex.MathJax",
		"pluginScripts": [
			{
				"name": "MathJax-3-CHTML",
				"id": "MathJax-script",
				"main": "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js",
				"description": "The MathJax rendering engine.",
				"default": true,
				"required": false,
				"toLoad": false
			},
			{
				"name": "MathJax-2",
				"main": "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML",
				"description": "The MathJax rendering engine.",
				"required": false,
				"toLoad": true
			}
		],
		"debugging": false
	}
]
//}}}
{{Title{
IYPT 2016 第八題:磁鐵小火車(撰寫中)
}}}
{{Author{
葉旺奇^^[1]^^,......,林中一^^[2]^^,曾賢德^^[1]^^
}}}{{Affiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
# 國立中興大學物理學系
}}}{{Address{
Contact: wcy2@mail.ndhu.edu.tw
}}}
{{Abstract{
磁鐵小火車是一個實作上相對容易,而原理說明較難定量的問題。此篇文章利用一般瀏覽器都可執行的 Javascript 語言對此問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{Section{
前言
}}}
{{Paragraph{
磁鐵小火車是在 [[IYPT 2016 的競賽題目|http://iypt.org/images/e/ef/problems2016.pdf]]中的第八題,其敘述為:
>Button magnets are attached to both ends of a small cylindrical battery. When placed in a copper coil such that the magnets contact the coil, this "train" starts to move. Explain the phenomenon and investigate how relevant parameters affect the train's speed and power.
>將鈕扣型磁鐵吸附在圓柱型小電池兩端,放進一個銅線圈並讓磁鐵接觸線圈,此時這個【火車】便開始移動。解釋這個現象並探討火車的速度與功率會受到哪些因素影響。
網路上可以找到許多影片展示這個現象,例如[[世界一簡単な構造の電車|https://www.youtube.com/watch?v=J9b0J29OzAU]]、[[其二|https://www.youtube.com/watch?v=Y1MDOerruDU]]、[[How to Build the Simplest Electric Train|https://www.youtube.com/watch?v=BWW4kPjd4yc]]等等。要自己做出這樣的小火車並不困難,練習幾次就可以把銅線捲得又均勻又夠密,讓小火車可以順暢移動。
}}}
{{Paragraph{
這題的實作相對容易,台灣區比賽時也有不少隊伍展現豐富的實驗結果,然而對於原理的解釋卻大多僅能停留在定性的說明,無法定量地進行理論與實驗的比較。
>決賽時有一組以模擬計算討論磁力矩造成的影響,並試圖與實驗做比較,是少數(或是唯一?)能夠把細節講得較為仔細的情況。該組有三位人員入選 2016 年國手,並於國際賽中榮獲金牌!
這篇文章對磁鐵小火車的運動進行定量模擬,計算磁鐵(以及電池)在線圈裡面的受力,並據此計算其運動狀態,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Paragraph{
磁鐵小火車在銅線圈裡之所以會移動,定性上可以這樣說明:
# 有電流從電池正極經過磁鐵流進銅線圈,並經由銅線圈流到負極的磁鐵而回到電池形成迴路(圖一A),
# 電流在銅線圈裡流動時會受到磁鐵磁場的磁力,而磁鐵也同時受到其反作用力而移動(圖一B)。
** 這裡也可以說成是【電流產生磁場,與磁鐵的磁場交互作用而互相受力】。
{{Figure{
|noborder|k
|[img(260px,135px)[image/research/Fig-IYPT-2016-08-01A.JPG]]|[img(260px,135px)[image/research/Fig-IYPT-2016-08-01B.JPG]]|
|>|width:350px;圖一 A(左)電流從電池正極(圖中左方)經吸附磁鐵流出至銅線圈,沿線圈流至吸附在負極的磁鐵(圖中右方),再流回負極完成迴路。B(右)各小段電線受到磁鐵的磁力,紫紅色箭頭表示正極側磁鐵產生的磁力 \(d\vec F_\text{anode}\),青色箭頭代表負極側磁鐵產生的磁力 \(d\vec F_\text{cathod}\),線圈總受力為全部小段受力之向量和,也就是 \(\vec F_\text{B,coil} = \int (d\vec F_\text{anode} + d\vec F_\text{cathod})\)(未畫出)。磁鐵與電池系統所受的力則為其反作用力 \(\vec F_\text{B,train} = -\vec F_\text{B,coil}\),以黃色箭頭畫在電池的中心位置(圖中電池中心處有兩個黃色箭頭,較大的向下,是為重力,較小的向右,便是磁力)。|
}}}
}}}
{{Paragraph{
圖一 A 顯示一個磁鐵小火車在運動過程中電流的方向,圖中電池的正極在左方,負極在右方,電流的方向,若從電池正極位置往負極方向看過去,是在順時針方向,也就是如果以等效磁矩的概念來看此電流所產生的磁場的話,等效磁矩的 N 極是在靠近電池負極,而等效 S 極則在正極附近的地方。圖一 B 顯示的則為計算而得,磁鐵對線圈中各小段電流所產生的磁力,整段線圈所受的磁力 \(\vec F_\text{B,coil}\) 即為各小段線圈受力的向量和,而磁鐵與電池組成的小火車之受力就是線圈受力的反作用力,\(\vec F_\text{B,train} = -\vec F_\text{B,coil}\)。當此力大過小火車與周圍環境的摩擦力時,小火車便會前進。
}}}
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
模擬計算的想法是螺線圈通電流的效果等同於一個磁矩,與磁鐵的磁矩交互作用而對磁鐵產生磁力,這個磁力可以由 \[\vec F_{B} = -\vec\nabla (\vec\mu \cdot \vec B)\] 計算出來^^[3]^^。不過由於做這個微分需要計算磁鐵位置以及其鄰近位置的磁位能,也就是對單一磁鐵至少得做兩個位置的磁位能計算,計算量較大,因此這裡我們採取的策略是:反過來計算磁鐵對電線中電流產生的磁力^^[3]^^ \[\vec F_{B} = \int \vec dF_{B} = I \int d\vec l \times \vec B(\vec r), \tag{1}\] 其反作用力便是電流作用於磁鐵的力,這樣一來僅需要對磁鐵本身的位置做計算即可。這裡磁場以及各小段電線位置都是可以確實計算出來的,如果暫時不考慮線圈可能會有的縱向震盪波,而電流的估計則是使用歐姆定律 \(I = V/R\),其中 \(V\) 是電池電壓,假設在行進過程中此電壓變化不大,而 \(R = R_\text{path} + r\) 是電路的總電阻,包含電池內電阻 \(r\) 以及外部路徑電阻 \(R_\text{path}\),此外部電阻也是使用簡單的 \(R = \rho L/A\) 來估算,其中 \(\rho\) 是電路材料的電阻率,包含銣鐵硼磁鐵與銅線,\(L\) 與 \(A\) 則分別為路徑的長度與截面積。
}}}
{{Paragraph{
實際的計算中為簡單起見,我們使用單一磁偶極矩 \(\vec\mu\) 的磁場^^[3]^^ \[\vec B(\vec r) = {\mu_0 \over 4\pi r^3}[3(\vec \mu \cdot \hat r)\hat r - \vec\mu] \tag{2}\] 來近似表示一個磁鐵的磁場(如圖二A所示),並讓磁鐵表面的磁場等於實際測量值,同時將螺線圈的每一圈分成 20 個等弧長的小段來計算,以期能在足夠的精確度下兼顧動畫的順暢度。螺線圈密度為 300 圈/公尺,與實驗條件相近,銅線、磁鐵、電池等之尺寸與質量皆由實驗值代入,銅線與磁鐵之電阻率分別為 \(1.68 \times 10^{-8} \Omega \cdot \text{m}\) 及 \(140 \times 10^{-6} \Omega \cdot \text{m}\),由維基百科獲得^^[4,5]^^,電阻的估計則是假設電流為【均勻通過整個截面】,電池內電阻則使用一般電池室溫內電阻 1.5 &Omega;^^[6]^^,且暫時假設電池溫度沒有明顯變化。
}}}
{{Figure{
|noborder|k
|[img(260px,135px)[image/research/Fig-IYPT-2016-08-02A.JPG]]|[img(260px,135px)[image/research/Fig-IYPT-2016-08-02B.JPG]]|
|>|width:350px;圖二 A(左)使用單一磁偶極之磁場來近似表示磁鐵的磁場。圖中磁鐵左右各一,左方為電池正極側,右方為負極側。磁鐵中心箭頭代表其磁偶極矩,正極側磁矩指向電池正極,而負極側磁矩指向電池負極。正極側磁鐵周圍紫紅色箭頭表示該磁鐵產生的磁場,箭頭長度正比於箭頭中心處磁場強度(負極側磁鐵的磁場未畫出)。B(右)磁鐵在電線各段處產生的磁場,紫紅色箭頭表示正極側磁鐵產生的磁場 \(\vec B_\text{anode}\),青色箭頭代表負極側磁鐵產生的磁場 \(\vec B_\text{cathod}\)。各小段電線所受的磁力便是經過的電流 \(I\) 與該位置兩磁場的外積 \(d\vec F_{B} = Id\vec l \times (\vec B_\text{anode} + \vec B_\text{cathod}\))。|
}}}
{{Section{
結果與討論
}}}
{{Subsection{
''移動與抖動''
}}}
{{Paragraph{
實驗顯示,當磁鐵順向排列(磁矩同向)時,不論磁矩方向為何,磁鐵小火車都不會移動,僅在原地抖動,而當磁鐵逆向排列時(磁矩反向),磁鐵則會移動,移動的方向則與磁鐵方向及電流方向有關。以圖一的狀況為例我們可以很容易定性地了解為何磁鐵逆向排列時小火車可以移動。線圈中電流產生的磁場等效於 N 極在電池負極側的磁矩所產生的,則當負極側磁鐵的 N 極朝向負極,也就是磁鐵 N 極對著等效磁矩的 N 極時,兩個 N 極會產生互斥作用;此時如果正極側磁鐵的 N 極是朝向正極,也就是對著等效磁矩的 S 極,則此兩個磁矩會產生相吸作用。負極側相斥以及正極側相吸,對小火車而言受力方向是相同的(圖一的右方),因此小火車以負極在前正極在後的方式前進。如果把兩個磁鐵都反向,則小火車受力方向也跟著反向(圖一的左方),就會變成正極在前負極在後的方式前進。反過來說,如果磁鐵是順向排列時,我們也能夠很快地理解兩端都是相吸或者都是相斥,受力互相抵消而使得小火車無法前進。
}}}
{{Subsection{
''移動速度''
}}}
{{Paragraph{
實驗結果顯示小火車的移動速度
}}}
{{Subsection{
''終端速度''
}}}
{{Paragraph{
* 終端速度的現象
* 到達終端速度的時間
* 終端速度的大小
台灣區比賽時大部分的實驗結果都呈現有終端速度的現象,
物理上要有終端速度,通常需要一個【與速度呈正相關】,也就是速度越快就越大的阻力存在,最簡單的情況便是阻力與速度成正比,
}}}
{{Section{
結論
}}}
{{Paragraph{
此篇文章採取計算磁鐵對電流產生的磁力來獲得其反作用力,也就是電流對磁鐵的磁力,比起位能微分的計算方式,相對較為節省計算資源。計算結果可以定量說明靠近磁鐵的電流對磁鐵受力有主要的影響,亦可說明磁鐵排列方式與移動與否的關聯。藉由將運動軌跡擬合至與實驗相符,可推算出線圈與小火車之間摩擦力的形式為...,此摩擦力的可能來源出了移動摩擦,還有線圈的縱向震盪,以及磁力產生的力矩。此處計算忽略電池的升溫可能造成電流的改變。
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# 電磁學課本,維基百科
# 維基 電阻率,銅
# 維基 電阻率,銣鐵硼
}}}
{{Title{
IYPT 2017 第 八題:看見密度(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,沈子耕 ^^2^^......,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
Schlieren Imaging
}}}
{{Section{
前言
}}}
{{Paragraph{
看見密度是在 [[IYPT 2017 的競賽題目|http://iypt.org/Problems]] 中的第八題,其敘述為:
>Schlieren Photography is often used to visualise density variations in a gas. Build a Schlieren setup and investigate how well it can resolve density differences.
>紋影攝影經常用來顯示器體中密度的變化。建造一個這樣的設備並檢驗它能解析密度差異到如何的程度。
網路上可以找到許多文章說明這個現象,...。
}}}
{{Paragraph{
這題在實作上...,而計算模擬的部分也......,使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Paragraph{
紋影攝影的現象...,基本原理...
}}}
{{Subsection{
光線的偏折
}}}
{{Paragraph{
光線在空氣中行進會發生偏折的話,主要是因為空氣的局部折射率不均勻所造成的,而局部折射率不均勻的主要原因則是局部密度的不均勻。局部密度不均勻可能是因為有其他粒子,如煙霧、水氣等,進入這個區域,或者是因為該區域的溫度不均勻而導致。光線偏折的角度和局部折射率的變化有關,當光線行進(假設主要沿著 //z//-軸)於一個微小區域時,如果這個區域的折射率和周遭(//x//- 和 //y//-方向)的區域有所不同,則行進方向會受到偏折,其偏折角度公式如下:
}}}
{{Subparagraph{
[>img(30%,)[image/teaching/YPT-Fig-Refraction.JPG]]\[\Delta \epsilon = {c/n_2-c/n_1 \over \Delta y}\Delta t\] Combining \(\Delta t = \Delta z{n \over c},\) \[\begin{eqnarray*}\Delta \epsilon &=& {c/n_2-c/n_1 \over \Delta y}\Delta z{n \over c} \\ &=& {\Delta z \over \Delta y}n{n_1 - n_2 \over n_1n_2} = {n \over n_1 n_2}{n_1 - n_2 \over \Delta y}\Delta z \\ {\Delta \epsilon \over \Delta z} &=& {n \over n_1n_2}{\Delta n \over \Delta y}\end{eqnarray*}\] Going to infinitesimal this becomes \[{d\epsilon \over dz} = {1 \over n}{dn \over dy} \quad \to \quad \boxed{\epsilon = {1 \over n}\int \nabla n\ d s}\] Small angle approximation \(d\epsilon = dy/dz,\) \[{\partial^2 y \over \partial z^2} = {1\over n}{\partial n \over \partial y}\]
}}}
{{Subparagraph{
如果這個區域跟沿著行進方向的下一個區域之折射率也有所不同,則經過兩區域的介面時也會發生偏折,偏折角度依照 Snell 定律計算:\[\vec v_\text{reflect} = r\vec I + \left(rc - \sqrt{1-r^2(1-c^2)}\vec n \right),\] 其中 \(\vec I\) 為入射光方向上的單位向量,\(\vec n\) 為介面的法向量(指向入射端),\(r = n_i / n_r\),為入射端與折射端之折射率比值,而 \(c = -\vec n \cdot \vec I\) 為入射角度的餘弦值。
}}}
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
將空間切割成許多細小的方塊,每個方塊足夠小,以致於可以合理假設個別方塊中的折射率是固定的,而相鄰小方塊的折射率可以有微小的變化。同時我們讓小方塊足夠大,使得光線在小方塊中行進時也會受到鄰近方塊折射率差異的影響而產生偏折。
}}}
{{Section{
結果與討論
}}}
{{Subsection{
...
}}}
{{Section{
結論
}}}
{{Paragraph{
...
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# Wikipedia [[Snell's Law|https://en.wikipedia.org/wiki/Snell%27s_law]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
}}}
{{Title{
IYPT 2017 第十六題:節拍器同步(撰寫中)
}}}
{{Author{
葉旺奇^^[1]^^,徐嘉宏^^[2]^^,......,曾賢德^^[1]^^
}}}{{Affiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
# 台北市立建國高級中學
}}}{{Address{
Contact: wcy2@mail.ndhu.edu.tw
}}}
{{Abstract{
【同步】是一個常見的自然現象,將兩個左右擺動的節拍器放在一個可以左右移動的平台上便可觀察此現象,而一般而言節拍器與擺可視為極為接近的力學系統,差別可說僅在於有沒有調節裝置節擺器(escapement)而以。此篇文章利用一般瀏覽器都可執行的 Javascript 語言,以【擺懸掛在可移動天花板上】代替【節拍器放在可移動平台上】,採用 4 階 ~Runge-Kutta 演算法,對於擺動限制在 //x//-//y// 平面上以及天花板移動限制在 //x// 方向上的情況進行定量模擬計算,其結果顯示沒有節擺器的擺動裝置可能沒有同步現象,表示同步現象可能是節擺器之間的同步,而擺動則是耦合的機制。
}}}
{{Section{
前言
}}}
{{Paragraph{
節拍器同步是在 [[IYPT 2017 的競賽題目|http://iypt.org/Problems]] 中的第十六題,其敘述為:
>A number of mechanical metronomes standing next to each other and set at random initial phases under certain conditions reach synchronous behaviour in a matter of minutes. Investigate the phenomenon.
>將幾個力學節拍器放在一起,隨機給定初始角度後開始震盪,條件合適的話可以在幾分鐘後達成同步,探討這個現象。
網路上可以找到許多影片展示這個現象,簡單的如 [[兩個節拍器|https://www.youtube.com/watch?v=yysnkY4WHyM]],複雜的如 [[32 個節拍器|https://www.youtube.com/watch?v=5v5eBf2KwF8]] 甚至 [[64 個節拍器|https://www.youtube.com/watch?v=4L7BnVScTUQ]] 等等。
}}}
{{Paragraph{
這題在實作上只要將多個節拍器放置在一個可以水平移動的平台上即可進行,困難度不算高,而計算模擬的部分也有足夠的文獻可以參考^^[1-2]^^。事實上在 2017/01/24 的提升高中生國際移動力成果發表會上,便有建國中學徐嘉宏同學使用 Python 程式語言將此模型寫出,且獲得符合實驗的結果。筆者在當時跟徐同學拿到其程式碼,並將其中節擺器的貢獻去除,看到的結果是兩個擺之間僅有耦合但沒有同步,推測節擺器在同步現象中起著主要的作用,因而決定要進一步確認.....這篇文章便是以擺來代替節拍器,模擬多個擺之間有耦合情況時的擺動結果,使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Paragraph{
多個擺(或節拍器)在一個水平可移動的平台上之所以能夠同步,定性上可以這樣說明:
# 擺或節拍器的擺動造成平台的移動,而此平台的移動會反過來影響擺或節拍器的擺動。
# 
}}}
{{Paragraph{
段落
}}}
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
如前所述,此篇文章''以【多個擺懸掛在同一個可水平移動的天花板下】來取代【多個節拍器放置在同一個可水平移動的平台上】'',而不論物理狀況為何,要做模擬計算首先得寫下運動方程,而要得到運動方程,最直接的辦法便是運用[[拉格朗日方法]]。我們可以按照 Ref[1] 的做法,從系統的拉格朗日量(Lagrangian)開始,\[\begin{equation}L = T - U = \sum_{i=1}^N \left({1 \over 2} m_i \dot{\vec r}_i^2 - m_i g y_i\right) + {1 \over 2}m_c\dot{\vec r}_c^2 - m_cgy_c,\end{equation}\] 其中下標 \(i\) 代表第 \(i\) 個擺,\(N\) 為擺的總數,而下標 \(c\) 代表掛著這些擺的天花板(ceiling)。每一個擺的位置 \(\vec r_i\) 除了受到自己擺動的影響,也受到天花板移動的影響:\[\begin{eqnarray}& \vec r_i &=& \vec r_{i,h} + \vec r_{i,CM} \nonumber\\ \to \quad & \dot{\vec r}_i &=& \dot{\vec r}_c + \dot{\vec r}_{i,CM},\end{eqnarray}\] 其中 \(\vec r_{i,h}\) 為第 \(i\) 個擺的懸掛位置,且 \(\dot{\vec r}_{i,h} = \dot{\vec r}_c\),\(\vec r_{i,CM}\) 則為其質心位置(從懸掛位置量起)。個別擺的位能項可寫成 \[\begin{equation}m_i gy_i = m_ig(r_{i,CM}-\vec r_{i,CM} \cdot (-\hat y)) = m_igr_{i,CM}(1-\cos\theta_i),\end{equation}\] 其中 \(\theta_i\) 是第 \(i\) 個擺與 \(-\hat y\) 的夾角,位能零點定義在所有擺的擺錘都在其懸掛點正下方時的位能。利用 (2) 與 (3) 我們把拉格朗日量 (1) 改寫如下:\[\begin{equation}L = \sum_{i=1}^N \left({1 \over 2} m_i (\dot{\vec r}_c + \dot{\vec r}_{i,CM})^2 - m_i g r_{i,CM}(1-\cos\theta_i)\right) + {1 \over 2}m_c\dot{\vec r}_c^2-m_cgy_c.\end{equation}\]
}}}
{{Paragraph{
簡單情況下我們讓''天花板的運動只有 \(x\) 方向,也就是 \(\vec r_c = x_c \hat x\),且擺的運動只在 //x//-//z// 平面上,只有角度的變化,沒有長度的變化(\(r_{i,CM} = l_i\) 為常數)'',則我們進一步把 (4) 式簡化如下:\[\begin{eqnarray*} & \vec r_{i,CM} &=& l_i(\sin\theta_i \hat x - \cos\theta_i \hat y) \\ \to \quad & \dot{\vec r}_{i,CM} &=& l_i(\cos\theta_i \dot\theta_i\hat x + \sin\theta_i \dot\theta_i\hat y).\end{eqnarray*}\] 同時可以省去天花板的位能(因其為常數),由此我們進一步將拉格朗日量簡化成 \[\begin{eqnarray}L &=& \sum_{i=1}^N \left({1 \over 2} m_i (\dot{\vec r}_c + l_i(\cos\theta_i\hat x+\sin\theta_i\hat y)\dot{\theta_i}))^2 - m_i g l_i(1-\cos\theta_i)\right) + {1 \over 2}m_c\dot{\vec r}_c^2 \nonumber \\ &=& \sum_i^N\left({1 \over 2}m_i(\dot x_c^2 + l_i^2\dot{\theta_i}^2+2l_i\dot x_c\cos\theta_i \dot{\theta_i})-m_igl_i(1-\cos\theta_i)\right) + {1 \over 2}m_c \dot x_c^2.\end{eqnarray}\]
}}}
{{Paragraph{
有了 (5) 式的拉格朗日量,則個別擺的角度運動方程便可以由拉格朗日方程(Lagrange's Equation)獲得(暫時忽略阻力):\[\begin{eqnarray}&{d \over dt}{\partial L \over \partial \dot{\theta}_i} &-& {\partial L \over \partial \theta_i} &=& 0 \nonumber \\ \to \quad & {d \over dt}\left(m_i(l_i^2\dot{\theta}_i+l_i\dot x_c\cos\theta_i)\right) &-& \left(m_il_i\dot x_c(-\sin\theta_i)\dot\theta_i-m_igl_i\sin\theta_i\right) &=& 0 \nonumber \\ \to \quad & m_i(l_i^2\ddot{\theta}_i+l_i(\ddot{r}_c\cos\theta_i+\dot x_c(-\sin\theta_i)\dot{\theta}_i) &-& m_il_i\sin\theta_i(-g-\dot x_c\dot\theta_i) &=& 0 \nonumber \\ \to \quad & m_il_i^2\ddot\theta_i + m_il_i(\ddot x_c \cos\theta_i - \dot x_c \sin\theta_i\dot\theta_i &+& g\sin\theta_i + \dot x_c\sin\theta_i\dot\theta_i) &=& 0 \nonumber \\ \to \quad & \boxed{m_il_i^2\ddot{\theta_i} + m_il_i(g\sin\theta_i + \ddot x_c\cos\theta_i)=0},\end{eqnarray}\] 而天花板的運動方程則為 \[\begin{eqnarray}&{d \over dt}{\partial L \over \partial \dot x_c} - {\partial L \over \partial x_c} &=& 0 \nonumber \\ \to \quad & {d \over dt}\left(\sum_{i=1}^N m_i(\dot x_c+l_i\cos\theta_i\dot \theta_i)+m_c\dot x_c\right) &=& 0 \nonumber \\ \to \quad & \sum_{i=1}^N m_i(\ddot x_c+l_i(-\sin\theta_i\dot\theta_i^2+\cos\theta_i\ddot\theta_i)) + m_c\ddot x_c &=& 0 \nonumber \\ \to \quad & \boxed{\left(\sum_{i=1}^N m_i + m_c\right)\ddot x_c + \sum_{i=1}^N m_il_i(\ddot\theta_i \cos\theta_i - \dot\theta_i^2 \sin\theta_i)=0}.\end{eqnarray}\]
}}}
{{Paragraph{
個別擺的角度運動方程,也就是 (6) 式,的前兩項可以很容易看出意義,第一項就是轉動慣量 \(I_i\) 乘以擺的角加速度 \(\alpha_i\),\[(m_il_i^2)\ddot\theta_i^2 = I_i \alpha_i,\] 而第二項的意義則是重力 \(m_i\vec g\) 產生的力矩 \(\vec \tau_{i,g}\),\[m_il_ig\sin\theta_i = l_i (m_ig)\sin\theta_i = |\vec r_{i,CM} \times (m_i\vec g)| = \tau_{i,g}.\] 至於第三項的意義則是天花板運動對個別擺所產生的等效力矩 \(\vec \tau_{i,c}\),\[m_il_i \ddot x_c^2\cos\theta_i = l_i(m_i\ddot x_c^2)\sin(\pi/2-\theta_i) = |\vec r_{i,CM} \times m_i\vec a_c| = \tau_{i,c},\] 其中 \(\pi/2 - \theta_i\) 為擺繩與天花板之間的夾角,而 \(m_i \vec a_c\),根據 Ref[2] 裡的 Ref[22] 的說法,是由於天花板在移動所產生的//慣性力//,是個假想力。這一條方程其實就是牛頓第二運動定律 \(\tau = I\alpha\)。
}}}
{{Paragraph{
天花板的運動方程,第 (7) 式,其實就是系統的''水平''總動量守恆,我們可以從獲得 (7) 式的過程之第二行看出這件事:\[{d \over dt}\left(\sum_{i=1}^N m_i(\dot x_c+l_i\cos\theta_i\dot \theta_i)+m_c\dot x_c\right) = 0 \\ \to {d \over dt}\left(m_c\dot x_c + \sum_{i=1}^N (m_i\dot x_c+m_il_i\cos\theta_i\dot\theta_i)\right) = 0 \\ \to m_c\dot x_c + \sum_{i=1}^N (m_i\dot x_c+m_il_i\cos\theta_i\dot\theta_i) = \text{const.}\] 其中 \(l_i \dot\theta_i = l_i \omega_i = v_i\) 是第 //i// 個擺的切線速率,乘上 \(\cos\theta_i\) 便是平行於天花板的速度分量大小。
}}}
{{Paragraph{
實際計算上我們使用[[針對二階微分方程的 4 階 Runge-Kutta 方法|四階 Runge-Kutta 方法]],需要把二階微分寫成函數來做計算:\[\begin{eqnarray} \ddot\theta_i &=& -{1 \over l_i} (g\sin\theta_i+\ddot x_c\cos\theta_i) \\ \ddot x_c &=& -\sum_{i=1}^N{ m_i \over M}l_i(\ddot\theta_i \cos\theta_i - \dot\theta_i^2 \sin\theta_i),\end{eqnarray}\] 其中 \(M \equiv \left(\sum_{i=1}^N m_i\right) + m_c\) 為系統的總質量。
}}}
----
{{Paragraph{
如果和 Ref[2] 做個比較,上面的 (8)、(9) 會對應到 Ref[2] 的 (1)、(5) 兩條式子:\[\begin{eqnarray}{d^2\theta \over dt^2} &=& -{mr_{c.m.}g \over I}\sin\theta - \epsilon\left[\left(\theta \over \theta_0\right)^2{d\theta \over dt}\right] - \left(r_{c.m.} m\cos\theta \over I\right){d^2 x \over dt^2} \quad & \text{Ref[2] 的 (1) 式} \\ x &=& -{m \over M+2m} r_{c.m.}(\sin\theta_1 + \sin\theta_2) & \text{Ref[2] 的 (5) 式}. \end{eqnarray}\] 如果把 (8) 式和 (10) 式拿來比較,並且注意 (10) 式的 \(r_{c.m.}\) 其實就是 (8) 式裡的 \(l_i\),則可以發現 (8) 式少了 \[\epsilon\left[\left(\theta \over \theta_0\right)^2-1\right]{d\theta \over dt}.\] 這一項,根據 Ref[2] 的說明,是用來描述節拍器的【節擺器(escapement)】以及【阻力】的,所謂節擺器是節拍器裡的一種設計,通常是在每次擺動到某個特定點(可能是端點之一)的時候,靠著內部的彈簧給擺身加上一個推力,用以補償因為阻力造成的損失,以維持節拍器的恆定。我們這裡只計算擺本身的運動,而且暫時沒有考慮阻力,所以沒有那一項。

把 (9) 式和 (11) 式做比較的話,只要將 (11) 式拿來做兩次微分,不難發現就和 (9) 式一模一樣。
}}}
----
{{Paragraph{
@@有了 (8) 及 (9) 式,我們就可以計算各個擺在任何時間的角度 \(\theta_i\)、角速度 \(\omega_i\)、以及角加速度 \(\alpha_i\)@@,並據以畫圖。如果需要計算擺繩的張力,可以利用 \[T - m_ig\cos\theta_i = m_ia_{cen} = m_il_i\omega_i^2 \quad \to \quad T = m_i(g\sin\theta_i + l_i\omega_i^2).\]
}}}
{{Section{
結果與討論
}}}
{{Subsection{
一個擺的運動:確認程式正確
}}}
{{Paragraph{
我們先檢驗單一個擺的運動來確認程式碼的正確性。當天花板為固定的時候,其結果應該和單擺或實體擺一樣,週期的理論值為^^[5]^^ \[\begin{eqnarray*}T &=&& 2\pi \sqrt{L \over g}\left(1 + {1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4 + \cdots\right) & \quad \text{單擺 Simple pendulum} \\ &\text{or}&& 2\pi \sqrt{I \over mL g}\left(1 + {1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4 + \cdots\right) & \quad \text{實體擺(物理擺)Physical pendulum.}\end{eqnarray*}\]
最大角度在計算過程中誤差預期,顯示程式碼是正確的。
}}}
{{Subsection{
兩個擺的同步:從微小差異開始
}}}
{{Paragraph{
從微小差異開始
}}}
{{Subsection{
兩個擺的同步:從較大差異開始
}}}
{{Paragraph{
如果起始差異較大,
}}}
{{Subsection{
多個擺的同步
}}}
{{Paragraph{
如果有兩個以上的擺,
}}}
{{Section{
結論
}}}
{{Paragraph{
此篇文章討論限制在 //x//-//y// 平面上的多擺同步問題,
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# [[Ward T. Oud, Master's Thesis|http://alexandria.tue.nl/repository/books/626694.pdf]]
# J. Pantaleone. Synchronization of metronomes. Am. J. Phys. 70, 10, 992-1000 (2002), [[link1|http://salt.uaa.alaska.edu/physics_public/metro.pdf]], [[link2|http://www.math.pitt.edu/~bard/classes/mth3380/syncpapers/metronome.pdf]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
#  [[Wikipedia Pendulum|https://en.wikipedia.org/wiki/Pendulum_(mathematics)]].
}}}
{{Title{
IYPT 2017 第十六題之二:二維擺的同步(撰寫中)
}}}
{{Author{
葉旺奇^^[1]^^
}}}
{{Affiliation{
# 國立東華大學物理學系
}}}
{{Address{
Contact: wcy2@mail.ndhu.edu.tw
}}}
{{Abstract{
前一篇 [[2017-16 節拍器同步]] 裡討論一維擺動的同步問題,這一篇將其結果延伸到二維擺動的情況,也就是擺錘的動作【不】再現制於 //x//-//y// 平面上,而是可以有二個自由度的擺動(\(\theta, \phi\)),同時天花板板的移動也有兩個自由度(//x//, //z//)。此篇同樣是利用一般瀏覽器都可執行的 Javascript 語言,採用 4 階 ~Runge-Kutta 演算法對此情況進行定量模擬計算,目前並無實驗結果可供比較。
}}}
{{Section{
前言
}}}
{{Paragraph{
擺動限制在 //x//-//y// 平面上以及天花板的移動限制在 //x// 方向上的情況已經在[[2017-16 節拍器同步]]裡面討論過,這篇討論進一步放寬限制,擺動可以有兩個自由度 \(\theta, \phi\),其中 \(\theta\) 仍舊是擺繩與 \(-\hat y\) 的夾角,而 \(\phi\) 則是擺繩在 //x//-//z// 平面上的投影與 \(+\hat z\) 軸的夾角,角度皆按習慣以逆時針方向為正。同時我們讓天花板移動也不限制在 \(x\) 方向,而是可以有 \(x, z\) 兩個方向。使用程式語言為 Javascript,3D 繪圖引擎為 three.js^^[1]^^,數據作圖則使用 D3.js^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Paragraph{
參考 [[2017-16 節拍器同步]]
}}}
{{Paragraph{
段落
}}}
{{Section{
實驗
}}}
{{Paragraph{
目前無實驗規劃
}}}
{{Section{
計算模型
}}}
{{Paragraph{
在[[前一篇文章|2017-16 節拍器同步]]的第 (4) 式我們寫下懸掛在同一天花板下多個擺的拉格朗日量 \[\begin{equation}L = \sum_{i=1}^N \left({1 \over 2} m_i (\dot{\vec r}_c + \dot{\vec r}_{i,CM})^2 - m_i g r_{i,CM}(1-\cos\theta_i)\right) + {1 \over 2}m_c\dot{\vec r}_c^2-m_cgy_c.\end{equation},\] 這個式子並沒有限制運動的自由度,因此我們可以在此繼續使用。當我們讓擺動有兩個自由度的時候(按照前言裡的定義),個別擺的質心位至應寫成 \[\vec r_{i,CM} = l_i (\sin\theta_i \sin\phi_i \hat x - \cos\theta_i \hat y + \sin\theta_i\cos\phi_i \hat z), \qquad 0 < \theta < \pi, 0 < \phi < 2\pi\] 而其質心速度則為 \[\begin{eqnarray}\dot{\vec r}_{i,CM} &=&& l_i( & (\cos\theta_i\dot\theta_i\sin\phi_i+\sin\theta_i\cos\phi_i\dot\phi_i) & \hat x \nonumber \\ &&+&&\sin\theta_i\dot\theta_i & \hat y \nonumber \\ &&+&& (\cos\theta_i\dot\theta_i\cos\phi_i - \sin\theta_i\sin\phi_i\dot\phi_i) & \hat z)\end{eqnarray}\] 並且我們讓天花板可以有 //x// 與 //z// 方向的移動,也就是 \[\begin{equation}\vec r_c = x_c \hat x + z_c \hat z.\end{equation}\]
}}}
{{Paragraph2{
將 (2)、(3) 兩式代回 (1) 式,我們便將系統的拉格朗日量展開成 \[\begin{eqnarray}L &=&& \sum_{i=1}^N ( \quad(1/2)m_i(\quad\dot x_c^2 + \dot z_c^2 + l_i^2\dot\theta_i^2 + l_i^2\sin^2\theta_i\dot\phi_i^2 + 2l_i\dot x_c (\cos\theta_i\dot\theta_i\sin\phi_i + \sin\theta_i\cos\phi_i\dot\phi_i) + 2l_i\dot z_c(\cos\theta_i\dot\theta_i\cos\phi_i - \sin\theta_i\sin\phi_i\dot\phi_i)\quad ) \nonumber \\ &&&\qquad- m_igl_i(1-\cos\theta_i)\quad) + {1 \over 2}m_c(\dot x_c^2 + \dot z_c^2).\end{eqnarray}\] 從 (4) 式我們便可以得出個別擺與天花板的運動方程。由於是二為運動,個別擺的運動方程會有兩條,\[{d \over dt}{\partial L \over \partial \dot\theta_i} - {\partial L \over \partial \theta_i} = 0 \\ {d \over dt}{\partial L \over \partial \dot\phi_i} - {\partial L \over \partial \phi_i} = 0,\]
}}}
{{Paragraph2{
我們先看第一條的第一項:\[\begin{eqnarray} {d \over dt}{\partial L \over \partial \dot\theta}_i &=&& {d \over dt}\left(m_i(l_i^2\dot{\theta}_i+l_i\dot x_c\cos\theta_i\sin\phi_i+l_i\dot z_c\cos\theta_i\cos\phi_i)\right) \nonumber \\ &=&& m_il_i^2\ddot\theta_i \nonumber \\ &&+& m_il_i(\ddot x_c\cos\theta_i\sin\phi_i-\dot x_c\sin\theta_i\dot\theta_i\sin\phi_i+\dot x_c\cos\theta_i\cos\phi_i\dot\phi_i) \nonumber \\ &&+& m_il_i(\ddot z_c\cos\theta_i\cos\phi_i-\dot z_c\sin\theta_i\dot\theta_i\cos\phi_i-\dot z_c\cos\theta_i\sin\phi_i\dot\phi_i).\end{eqnarray}\]
}}}
{{Paragraph2{
接著看第一條的第二項:\[\begin{eqnarray} {\partial L \over \partial \theta}_i &=&& m_il_i^2\sin\theta_i\cos\theta_i\dot\phi_i^2 \nonumber \\ &&+& m_il_i\dot x_c(-\sin\theta_i\dot\theta_i\sin\phi_i + \cos\theta_i\cos\phi_i\dot\phi_i) \nonumber \\ &&+& m_il_i\dot z_c(-\sin\theta_i\dot\theta_i\cos\phi_i - \cos\theta_i\sin\phi_i\dot\phi_i) \nonumber \\ &&-& m_igl_i\sin\theta_i.\end{eqnarray}\]
}}}
{{Paragraph2{
根據拉格朗日方程,(5) 式減去 (6) 式必須為 0(無外力且忽略阻力),所以我們得到一條運動方程:\[\begin{equation} m_il_i^2(\ddot\theta_i+\sin\theta_i\cos\theta_i\dot\phi_i^2) + m_il_i(g\sin\theta_i + \ddot x_c\cos\theta_i\sin\phi_i + \ddot z_c\cos\theta_i\cos\phi_i) = 0.\end{equation}\]
再做一次相同的步驟我們就得到個別擺的另外一條運動方程:\[\begin{equation}m_il_i^2(\sin^2\theta_i\ddot\phi_i+2\sin\theta_i\cos\theta_i\dot\theta_i\dot\phi_i)+m_il_i(\ddot x_c\sin\theta_i\cos\phi_i-\ddot z_c\sin\theta_i\sin\phi_i) = 0.\end{equation}\]
}}}
{{Paragraph{
天花板的兩條運動方程則為 \[{d \over dt}{\partial L \over \partial \dot x_c} - {\partial L \over \partial x_c} = 0, \\ {d \over dt}{\partial L \over \partial \dot z_c} - {\partial L \over \partial z_c} = 0.\] 其中關於 \(x_c\) 的方程為 \[{d \over dt}\left(\sum_{i=1}^N (m_i\dot x_c + m_il_i(\cos\theta_i\dot\theta_i\sin\phi_i + \sin\theta_i\cos\phi_i\dot\phi_i)) + m_c\dot x_c\right) = 0 \\ \to \left(\sum_{i=1}^N m_i + m_c\right)\ddot x_c \\ + \sum_{i=1}^N m_il_i(-\sin\theta_i\dot\theta_i^2\sin\phi_i+\cos\theta_i\ddot\theta_i\sin\phi_i+\cos\theta_i\dot\theta_i\cos\phi_i\dot\phi_i) \\ + \sum_{i=1}^N m_il_i(\cos\theta_i\dot\theta_i\cos\phi_i\dot\phi_i - \sin\theta_i\sin\phi_i\dot\phi_i^2 + \sin\theta_i\cos\phi_i\ddot\phi_i) = 0.\]
}}}
{{Paragraph2{
把這個方程再整理一下可得 \[\left(\sum_{i=1}^N m_i + m_c\right)\ddot x_c + \sum_{i=1}^N m_il_i\left(-\sin\theta_i\sin\phi_i(\dot\theta_i^2+\dot\phi_i^2)+2\cos\theta_i\cos\phi_i\dot\theta_i\dot\phi_i+\cos\theta_i\sin\phi_i\ddot\theta_i+\sin\theta_i\cos\phi_i\ddot\phi_i\right) = 0.\] 同樣的過程我們可以得到 \(z_c\) 的方程:\[\left(\sum_{i=1}^N m_i + m_c\right)\ddot z_c + \sum_{i=1}^N m_il_i\left(-\sin\theta_i\cos\phi_i(\dot\theta_i^2+\dot\phi_i^2)-2\cos\theta_i\sin\phi_i\dot\theta_i\dot\phi_i+\cos\theta_i\cos\phi_i\ddot\theta_i-\sin\theta_i\sin\phi_i\ddot\phi_i\right) = 0.\]
}}}
{{Section{
結果與討論
}}}
{{Subsection{
一個擺的運動:確認程式正確
}}}
{{Paragraph{
單一個擺的運動週期、
}}}
{{Subsection{
兩個擺的同步:從微小差異開始
}}}
{{Paragraph{
從微小差異開始
}}}
{{Subsection{
兩個擺的同步:從較大差異開始
}}}
{{Paragraph{
如果起始差異較大,
}}}
{{Subsection{
多個擺的同步
}}}
{{Paragraph{
如果有兩個以上的擺,
}}}
{{Section{
結論
}}}
{{Paragraph{
此篇文章討論限制在 //x//-//y// 平面上的多擺同步問題,
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# three.js, https://threejs.org/
# D3.js, https://d3js.org/
# J. Pantaleone. Synchronization of metronomes. Am. J. Phys. 70, 10, 992-1000 (2002), http://salt.uaa.alaska.edu/physics_public/metro.pdf, http://www.math.pitt.edu/~bard/classes/mth3380/syncpapers/metronome.pdf
# Ward T. Oud, Master's Thesis, http://alexandria.tue.nl/repository/books/626694.pdf
}}}
** @@color:red;''tilt angle the same in both directions? related to omega of shaft?''@@
** @@color:red;''Viscosity has a play here?''@@
** possible to change dir without hitting the wall? (rep: yes, seen it) ''[bumping?]''
{{Title{
IYPT 2018 第 七 題:錐形堆(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
It is...
}}}
{{Section{
前言
}}}
{{Paragraph{
[>img(30%,)[https://upload.wikimedia.org/wikipedia/commons/b/ba/Angleofrepose.png]] 錐形堆是在 [[IYPT 2018 的競賽題目|http://iypt.org/Problems]] 中的第七題,其敘述為:
>Non-adhesive materials can be poured such that they form a cone-like pile. Investigate the parameters that affect the formation of the cone and the angle itmakes with the ground.
>不相黏的顆粒物質可以倒成一個錐形的堆積,研究造成此結果的相關參數,以及錐形與地面的夾角。
(圖片來源:[[維基百科 Angle of Repose|https://en.wikipedia.org/wiki/Angle_of_repose]])
}}}
{{Paragraph{
......。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Paragraph{
根據文獻[4], sliding and rolling frictions are the primary reasons for the formation of a sandpile, particle size and container thickness significantly influence the angle of repose

The angle of repose is one of the most important macro-scopic parameters in characterizing the behavior of granular materials. It is related to many important phenomena, including avalanching, stratification, and segregation
}}}
{{Subsection{
最簡單的情況 球狀的彈性體
}}}
{{Paragraph{
沙子堆積的過程中,可以想見會有許多顆粒發生碰撞,最後堆積的結果應該會和這些碰撞有直接的關聯,這篇報告將試圖從微觀的顆粒碰撞出發來討論其對巨觀形貌的影響,我們先從最簡單的情況開始。

如果我們將沙子看做是一顆一顆圓球狀的彈性體,以彈性碰撞
}}}
{{Subparagraph{

}}}
{{Subparagraph{
...
}}}
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
...
}}}
{{Section{
結果與討論
}}}
{{Subsection{
...
}}}
{{Section{
結論
}}}
{{Paragraph{
...
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# Wikipedia [[Angle of Repose|https://en.wikipedia.org/wiki/Angle_of_repose]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# Y. C. Zhou, B. H. Xu, A. B. Yu, and P. Zulli. Numerical investigation of the angle of repose of monosized spheres. Phys. Rev. E 64, 021301 (2001). [[PDF|http://ro.uow.edu.au/cgi/viewcontent.cgi?article=7733&context=eispapers]]
}}}
{{Title{
IYPT 2018 第 十一 題:前後左右擺(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
Fix one end of a ...
}}}
{{Section{
前言
}}}
{{Paragraph{
[>img(18%,)[image/teaching/YPT-Fig-RA-Pendulum-01.JPG]] 前後左右擺是在 [[IYPT 2018 的競賽題目|http://iypt.org/Problems]] 中的第十一題,其敘述為:
>Fix one end of a horizontal elastic rod to a rigid stand. Support the other end of the rod with a taut string to avoid vertical deflection and suspend a bob from it on another string (see figure). In the resulting pendulum the radial oscillations (parallel to the rod) can spontaneously convert into azimuthal oscillations (perpendicular to the rod) and vice versa. Investigate the phenomenon.
>將一根彈性棒的一端固定在支架上,另一端用緊繩綁住使得它只能夠在水平方向擺動,並在此端懸掛一個單擺(如圖)。這個單擺的逕向擺動(平行於彈性棒,前後)以及方位擺動(垂直於彈性棒,左右)可以自發性地相互轉換。探究這個現象。
}}}
{{Paragraph{
關於前後左右擺的運動狀況,可以參考[[這個實驗影片|https://youtu.be/UJJpUlFzZhs]] 或是 [[這個模擬動畫|https://youtu.be/1BhCXEd1zM4]]。這題在實作上可以使用學校實驗室現有的裝置(固定座、固定夾、砝碼等)來進行,可以很快開始觀察現象......。在計算模擬的部分除了要模擬單擺運動(此部分很容易),主要得處理【當支點在擺動時單擺的受力為何】這個問題,只要受力可以知道,模擬就可以進行。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Subsection{
當支點固定的時候
}}}
{{Paragraph{
[>img(25%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Simple_gravity_pendulum.svg/450px-Simple_gravity_pendulum.svg.png]] 擺動運動在高中通常都有學到簡單的情況,至少對於現象並不會陌生。__簡單的情況是支點固定不動,擺繩長度不變,且擺動維持在同一平面__,此時對於擺錘而言,其受力僅有自身重力與擺繩的張力,空氣阻力在這裡通常可以忽略不計(參考右圖,來源:[[Wikipedia Pendulum|https://en.wikipedia.org/wiki/Pendulum]]):\[\vec F_\text{bob} = m\vec g + \vec T,\] 其中 \[\begin{equation}\label{eq-T-planar}\vec T = \left(mg\cos\theta + m{v^2 \over r}\right)(-\hat r)\end{equation}\] 是擺繩提供的張力,\(\vec r\) 為從支點測量的擺錘位置向量。可以很容易看出第二項是半徑為 \(r\) 速率為 \(v\) 的圓周運動所需之向心力。
}}}
{{SubParagraph{
[>img(20%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Conical_pendulum.svg/375px-Conical_pendulum.svg.png]] 如果擺動__不限制在平面__上,但擺繩剛好畫出一個圓錐體(圓錐擺)的話(參考右圖,來源:[[Wikipedia Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]]),則張力的計算更為簡單,由於擺錘所畫出的圓周必定在水平面上,且擺繩和鉛直線的角度會維持不變,因此圓錐體的張力只需從【垂直分力抵消重力】或者是【水平分力提供圓周運動的向心力】兩個條件擇一計算即可,\[\begin{eqnarray}\label{eq-T-conical-v}T\cos\theta = mg \quad &\to& \quad T = {mg \over \cos\theta} \quad &\text{垂直分力抵銷重力,或者} \\ \label{eq-T-conical-h}T\sin\theta = m{v^2 \over r} \quad &\to& \quad T = {mv^2 \over r\sin\theta} \quad &\text{水平分力提供向心力。}\end{eqnarray}\] 這裡要注意的是 (\ref{eq-T-conical-h}) 式和 (\ref{eq-T-planar}) 式的 \(r\) 是不一樣的,(\ref{eq-T-planar}) 式的 \(r\) 就是擺繩的長度,但 (\ref{eq-T-conical-h}) 式的 \(r\) 是擺繩在水平面上的投影長度。
> [>img(20%,)[image/teaching/YPT-Fig-AR-Pendulum-Complex.jpg]] 順道一提,由於 (\ref{eq-T-conical-v}) 式和 (\ref{eq-T-conical-h}) 式必須同時成立,圓錐擺的速率是受到限制的,將 (\ref{eq-T-conical-h}) 式除以 (\ref{eq-T-conical-v}) 式就可以得到 \[\tan\theta = {v^2 \over gr} \quad \to \quad v = \sqrt{gr\tan\theta} = \sqrt{gL\sin\theta\tan\theta}.\] 如果速率不等於此,那麼擺錘的軌跡就可能相當複雜,如右圖一般。
}}}
{{Subsection{
當支點會動的時候
}}}
{{Paragraph{
如果單擺的支點是可動的,那麼擺錘的運動就會牽引支點使其產生運動,而支點的運動會反過來影響擺錘的運動。這種擺錘和支點都有運動的情況,其運動方程會分別有擺錘以及支點所屬的,且兩方的方程會有耦合,這種相互耦合的方程通常很難直觀地寫下來,一般而言我們是借助 Lagrange 方法來得到這些運動方程。要經由此路徑得到運動方程,我們就得先寫下這個系統的 Lagragian \(L = K E - U E\)。為了避免過程產生混淆,我們先
# 讓鉛直線和 \(z\) 軸重合,\(+z\) 為向上,
# 重新將擺繩與鉛直線的夾角 \(\theta\) 定為【從 \(+z\) 軸量過來的角度】,也就是和球座標裡的 \(\theta\) 定義是一致的。
這樣一來我們可以如下寫出系統的 Lagrangian,我們從寫下擺錘(bob,下標 b)與支點(pivot,下標 p)的位置開始,並限制支點只能在水平面上擺動:
}}}
{{Subparagraph{
\begin{eqnarray*}\vec r_p &=&&&& l_p(\cos\phi_p,&\sin\phi_p,&0) \\ \\ \vec r_b &=&& \vec r_p &+& l_b(\sin\theta\cos\phi_b,&\sin\theta\sin\phi_b,&\cos\theta) \\ &=&&&& l_p(\cos\phi_p,&\sin\phi_p,&0) \\ &&&&+& l_b(\sin\theta\cos\phi_b,&\sin\theta\sin\phi_b,&\cos\theta \\ &=&&&& (l_p\cos\phi_p+l_b\sin\theta\cos\phi_b,&l_p\sin\phi_p+l_b\sin\theta\sin\phi_b,&l_b\cos\theta)\end{eqnarray*} 要寫下動能項,我們要先寫出速度,
\[\dot{\vec r_p} = l_p(-\sin\phi_p \dot\phi_p, \quad \cos\phi_p \dot\phi_p, \quad 0),\] \begin{eqnarray*}\dot{\vec r_b} &=&& {d \over dt}&(&l_p\cos\phi_p+l_b\sin\theta\cos\phi_b, \quad l_p\sin\phi_p+l_b\sin\theta\sin\phi_b, \quad l_b\cos\theta) \\ &=&&&(&-l_p\sin\phi_p\dot\phi_p+l_b(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b) \quad, \\ &&&&& l_p\cos\phi_p\dot\phi_p+l_b(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b) \quad , \\ &&&&& -l_b\sin\theta\dot\theta \quad ).\end{eqnarray*} 擺錘的動能 \[K E_\text{bob} = {1 \over 2}m_b\dot r_b^2,\] 這是沒有疑慮的,不過實驗上支點的運動是一根棒子的彈性擺動,其動能較為複雜,為簡單起見,我們用剛體的轉動來計算,然後將結果乘上一個係數 \(\alpha\),其值可以經由簡化模型算出,或者與實驗比對而得到。如此一來我們將擺錘與支點的動能寫成如下:\begin{eqnarray*}KE &=&& {1\over 2}m_b \dot{\vec r_b^2} &+& {1 \over 2}\alpha m_p \dot{\vec r_p^2} \\ &=&& {1 \over 2}m_b( && (-l_p\sin\phi_p\dot\phi_p+l_b(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b))^2 \\&&&&+& (l_p\cos\phi_p\dot\phi_p+l_b(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b))^2 \\ &&&&+& (l_b\sin\theta\dot\theta)^2 \qquad ) \\ &&+&{1 \over 2}\alpha m_p(&& l_p^2(\sin^2\phi_p\dot\phi_p^2 + \cos^2\phi_p\dot\phi_p^2) \quad  ) \\ &=&& {1 \over 2}m_b( && (-l_p\sin\phi_p\dot\phi_p)^2+l_b^2(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b)^2 - 2l_pl_b\sin\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b) \\&&&&+& (l_p\cos\phi_p\dot\phi_p)^2+l_b^2(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b)^2 + 2l_pl_b\cos\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b) \\ &&&&+& (l_b\sin\theta\dot\theta)^2 \qquad ) \\ &&+& \alpha ( && {1 \over 2} m_pl_p^2\dot\phi_p^2 \qquad ) \\ &=&& {1 \over 2}m_b( && (l_p^2\sin^2\phi_p\dot\phi_p^2 + l_p^2\cos^2\phi_p\dot\phi_p^2) \\ &&&&+& l_b^2(\cos^2\theta\dot\theta^2\cos^2\phi_b+\sin^2\theta\sin^2\phi_b\dot\phi_b^2-2\cos\theta\sin\theta\cos\phi_b\sin\phi_b\dot\theta\dot\phi_b) \\ &&&&-& 2l_pl_b\sin\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b) \\&&&&+& l_b^2(\cos^2\theta\dot\theta^2\sin^2\phi_b+\sin^2\theta\cos^2\phi_b\dot\phi_b^2+2\cos\theta\sin\theta\cos\phi_b\sin\phi_b\dot\theta\dot\phi_b) \\ &&&&+& 2l_pl_b\cos\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b) \\ &&&&+& l_b^2\sin^2\theta\dot\theta^2 \qquad ) \\ &&+& \alpha ( && {1 \over 2} m_pl_p^2\dot\phi_p^2 \qquad ) \\ &=&& {1 \over 2}m_b( && l_b^2(\cos^2\theta\dot\theta^2+\sin^2\theta\dot\phi_b^2) + l_b^2\sin^2\theta\dot\theta^2 \\ &&&&-& 2l_pl_b\sin\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b) \\ &&&&+& 2l_pl_b\cos\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b) \qquad ) \\ &&&+&& \alpha{1 \over 2} m_pl_p^2\dot\phi_p^2 + {1 \over 2}m_bl_p^2\dot\phi_p^2 \\ &=&& m_bl_bl_p(&& \sin\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b) \\ &&&&+& \cos\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b) \qquad ) \\ &&&+&& {1 \over 2}m_bl_b^2(\dot\theta^2+\sin^2\theta\dot\phi_b^2) + \alpha{1 \over 2} m_pl_p^2\dot\phi_p^2 + {1 \over 2}m_bl_p^2\dot\phi_p^2 \end{eqnarray*}
}}}
{{SubParagraph{
位能的話就相對簡單許多,只有擺錘的重力位能以及支點的彈性位能。簡單起見我們讓原點的重力位能為 0,並且用 \({1 \over 2}\beta k_p(l_p\phi_p)^2\) 來近似支點的彈性位能(__關於材料的彈性位能,可參考下方文獻[5]__),其中係數 \(\beta\) 如同支點動能裡的 \(\alpha\),可從簡化模型計算或是和實驗比對來獲得:\[U E = m_bgl_b\cos\theta + {1 \over 2}\beta k_pl_p^2\phi_p^2\]
}}}
{{SubParagraph{
動能位能都寫下來,Lagragian 自然就得到了:\begin{eqnarray} L &=&& m_bl_bl_p(\sin\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b)+\cos\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b)) \nonumber\\ &&+& {1 \over 2}m_bl_b^2(\dot\theta^2+\sin^2\theta\dot\phi_b^2)+\alpha{1 \over 2} m_pl_p^2\dot\phi_p^2 + {1 \over 2}m_bl_p^2\dot\phi_p^2 \nonumber\\ \label{eq-Lagrangian} &&-& m_bgl_b\cos\theta - {1 \over 2}\beta k_pl_p^2\phi_p^2 \end{eqnarray} 接下來就是套用 Lagrange's equations \[{d \over dt}{\partial L \over \partial \dot q} - {\partial L \over \partial q} = 0 \quad \to \quad {d \over dt}{\partial L \over \partial \dot q} = {\partial L \over \partial q},\] 把 \(q\) 代入各個變數來得到其對應的運動方程。
}}}
{{SubParagraph{
從 (\ref{eq-Lagrangian}) 式來看,此系統的變數共有三個:\(\theta\)、\(\phi_b\),以及 \(\phi_p\),各有一條對應的運動方程,我們先從支點的變數 \(\phi_p\) 做起,等號左邊的 \[{\partial L \over \partial \dot\phi_p} = m_bl_bl_p(\sin\phi_p(\cos\theta\dot\theta\cos\phi_b - \sin\theta\sin\phi_b\dot\phi_b) + \cos\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b)) + (\alpha m_p+m_b)l_p^2\dot\phi_p.\] 將它對時間做全微分 \begin{eqnarray*}{d \over dt}{\partial L \over \partial \dot\phi_p} &=&& m_bl_bl_p \cos\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b - \sin\theta\sin\phi_b\dot\phi_b) \\ &&+& m_bl_bl_p \sin\phi_p(-\sin\theta\dot\theta^2\cos\phi_b+\cos\theta\ddot\theta\cos\phi_b-\cos\theta\dot\theta\sin\phi_b\dot\phi_b) \\ &&-& m_bl_bl_p\sin\phi_p(\cos\theta\dot\theta\sin\phi_b\dot\phi_b+\sin\theta\cos\phi_b\dot\phi_b^2+\sin\theta\sin\phi_b\ddot\phi_b) \\ &&-& m_bl_bl_p \sin\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b) \\ &&+& m_bl_bl_p \cos\phi_p(-\sin\theta\dot\theta^2\sin\phi_b+\cos\theta\ddot\theta\sin\phi_b+\cos\theta\dot\theta\cos\phi_b\dot\phi_b) \\ &&+& m_bl_bl_p \cos\phi_p(\cos\theta\dot\theta\cos\phi_b\dot\phi_b-\sin\theta\sin\phi_b\dot\phi_b^2+\sin\theta\cos\phi_b\ddot\phi_b) \\ &&+& (\alpha m_p+m_b)l_p^2\ddot\phi_p\end{eqnarray*}
}}}
{{SubParagraph{
等號右邊則為 \[{\partial L \over \partial \phi_p} = m_bl_bl_p(\cos\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b)-\sin\phi_p\dot\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b))-\beta k_pl_p^2\phi_p.\] 我們先把每一項都除以 \(m_bl_bl_p\),然後按照變數 \(\phi_p\) 的二次微分、一次微分、無微分項的順序整理起來,再把二次微分項放在等號左邊,其餘放到右邊,便得到運動方程。其中等號左邊的二次微分項為 \[\left({\alpha m_p \over m_b}+1\right){l_p \over l_b}\ddot\phi_p = \left((\alpha m_p + m_b)l_p \over m_b l_b\right)\ddot\phi_p,\] 等號右邊的一次微分項為 \begin{eqnarray*} &&-& \cos\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b)\dot\phi_p \\ &&+& \sin\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b)\dot\phi_p \\ &&+& \cos\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b)\dot\phi_p \\ &&-& \sin\phi_p(\cos\theta\dot\theta\sin\phi_b+\sin\theta\cos\phi_b\dot\phi_b)\dot\phi_p \\ &=&& 0.\end{eqnarray*} 而無微分項加起來則是 \begin{eqnarray*}&&-& \sin\phi_p(-\sin\theta\dot\theta^2\cos\phi_b+\cos\theta\ddot\theta\cos\phi_b-\cos\theta\dot\theta\sin\phi_b\dot\phi_b) \\ &&+& \sin\phi_p(\cos\theta\dot\theta\sin\phi_b\dot\phi_b+\sin\theta\cos\phi_b\dot\phi_b^2+\sin\theta\sin\phi_b\ddot\phi_b) \\ &&-& \cos\phi_p(-\sin\theta\dot\theta^2\sin\phi_b+\cos\theta\ddot\theta\sin\phi_b+\cos\theta\dot\theta\cos\phi_b\dot\phi_b) \\ &&-& \cos\phi_p(\cos\theta\dot\theta\cos\phi_b\dot\phi_b-\sin\theta\sin\phi_b\dot\phi_b^2+\sin\theta\cos\phi_b\ddot\phi_b)-k_pl_p^2\phi_p \\ &=&& \sin\phi_p(-\cos\theta\cos\phi_b\ddot\theta + \sin\theta\sin\phi_b\ddot\phi_b + \sin\theta\cos\phi_b(\dot\theta^2+\dot\phi_b^2) + 2\cos\theta\sin\phi_p\dot\theta\dot\phi_b) \\ &&+& \cos\phi_p(-\cos\theta\sin\phi_b\ddot\theta - \sin\theta\cos\phi_b\ddot\phi_b +\sin\theta\sin\phi_b(\dot\theta^2+\dot\phi_b^2) - 2\cos\theta\cos\phi_b\dot\theta\dot\phi_b)-\beta k_pl_p^2\phi_p \end{eqnarray*}
}}}
{{SubParagraph{
整合起來我們就得到關於支點角度 \(\phi_p\) 的運動方程:\begin{eqnarray}\left((\alpha m_p + m_b)l_p \over m_b l_b\right)\ddot\phi_p &=&& \sin\phi_p(-\cos\theta\cos\phi_b\ddot\theta + \sin\theta\sin\phi_b\ddot\phi_b + \sin\theta\cos\phi_b(\dot\theta^2+\dot\phi_b^2) + 2\cos\theta\sin\phi_p\dot\theta\dot\phi_b) \nonumber\\ \label{eq-phi_p} &&+& \cos\phi_p(-\cos\theta\sin\phi_b\ddot\theta - \sin\theta\cos\phi_b\ddot\phi_b +\sin\theta\sin\phi_b(\dot\theta^2+\dot\phi_b^2) - 2\cos\theta\cos\phi_b\dot\theta\dot\phi_b) - \beta k_pl_p^2\phi_p.\end{eqnarray} 這個方程看起來頗複雜,大概很難直觀地寫下來。
}}}
{{Paragraph{
接下來是擺錘的運動方程,有兩個相關變數 \(\theta\) 及 \(\phi_b\),我們先從 \(\phi_b\) 開始,按照同樣的流程先看等號左邊,
}}}
\[{\partial L \over \partial \dot\phi_b} = m_bl_bl_p(-\sin\phi_p\dot\phi_p\sin\theta\sin\phi_b+\cos\phi_p\dot\phi_p\sin\theta\cos\phi_b)+m_bl_b^2\sin^2\theta\dot\phi_b\] 對時間做全微分 \begin{eqnarray*}{d \over dt}{\partial L \over \partial \dot\phi_b} &=&& m_bl_bl_p(-\cos\phi_p\dot\phi_p^2\sin\theta\sin\phi_b-\sin\phi_p\ddot\phi_p\sin\theta\sin\phi_b-\sin\phi_p\dot\phi_p\cos\theta\dot\theta\sin\phi_b-\sin\phi_p\dot\phi_p\sin\theta\cos\phi_b\dot\phi_b) \\ &&+& m_bl_bl_p(-\sin\phi_p\dot\phi_p^2\sin\theta\cos\phi_b+\cos\phi_p\ddot\phi_p\sin\theta\cos\phi_b+\cos\phi_p\dot\phi_p\cos\theta\dot\theta\cos\phi_b-\cos\phi_p\dot\phi_p\sin\theta\sin\phi_b\dot\phi_b) \\ &&+& m_bl_b^2(2\sin\theta\cos\theta\dot\theta\dot\phi_b+\sin^2\theta\ddot\phi_b)\end{eqnarray*} 然後看等號右邊, \begin{eqnarray*}{\partial L \over \partial \phi_b} &=&& m_bl_bl_p(\sin\phi_p\dot\phi_p(-\cos\theta\dot\theta\sin\phi_b-\sin\theta\cos\phi_b\dot\phi_b)+\cos\phi_p\dot\phi_p(\cos\theta\dot\theta\cos\phi_b-\sin\theta\sin\phi_b\dot\phi_b))\end{eqnarray*} 同樣把每一項都除以 \(m_bl_bl_p\),然後按照變數 \(\phi_b\) 的微分冪次高低順序整理起來,再把二次微分項放在等號左邊,其餘放到右邊。其中等號左邊的二次微分項僅有一項 \[{l_b \over l_p}\sin^2\theta\ddot\phi_b,\] 等號右邊的一次微分項為 \begin{eqnarray*} &&+& \sin\phi_p\dot\phi_p\sin\theta\cos\phi_b\dot\phi_b + \cos\phi_p\dot\phi_p\sin\theta\sin\phi_b\dot\phi_b - {l_b \over l_p}2\sin\theta\cos\theta\dot\theta\dot\phi_b \\ &&-& \sin\phi_p\dot\phi_p\sin\theta\cos\phi_b\dot\phi_b - \cos\phi_p\dot\phi_p\sin\theta\sin\phi_b\dot\phi_b \\ &=&& - {l_b \over l_p}2\sin\theta\cos\theta\dot\theta\dot\phi_b.\end{eqnarray*} 無微分項則有 \begin{eqnarray*} &&& \cos\phi_p\dot\phi_p^2\sin\theta\sin\phi_b + \sin\phi_p\ddot\phi_p\sin\theta\sin\phi_b + \sin\phi_p\dot\phi_p\cos\theta\dot\theta\sin\phi_b \\ &&+& \sin\phi_p\dot\phi_p^2\sin\theta\cos\phi_b - \cos\phi_p\ddot\phi_p\sin\theta\cos\phi_b - \cos\phi_p\dot\phi_p\cos\theta\dot\theta\cos\phi_b \\ &&-& \sin\phi_p\dot\phi_p\cos\theta\dot\theta\sin\phi_b + \cos\phi_p\dot\phi_p\cos\theta\dot\theta\cos\phi_b \\ &=&& \sin\phi_b(\cos\phi_p\dot\phi_p^2\sin\theta+\sin\phi_p\ddot\phi_p\sin\theta+\sin\phi_p\dot\phi_p\cos\theta\dot\theta-\sin\phi_p\dot\phi_p\cos\theta\dot\theta) \\ &&+& \cos\phi_b(\sin\phi_p\dot\phi_p^2\sin\theta - \cos\phi_p\ddot\phi_p\sin\theta - \cos\phi_p\dot\phi_p\cos\theta\dot\theta + \cos\phi_p\dot\phi_p\cos\theta\dot\theta)\end{eqnarray*} 整合起來我們就得到關於擺錘角度 \(\phi_b\) 的運動方程:\begin{eqnarray}{l_b \over l_p}\sin^2\theta\ddot\phi_b &=&-& {l_b \over l_p}2\sin\theta\cos\theta\dot\theta\dot\phi_b \nonumber\\ &&+& \sin\phi_b(\cos\phi_p\dot\phi_p^2\sin\theta+\sin\phi_p\ddot\phi_p\sin\theta+\sin\phi_p\dot\phi_p\cos\theta\dot\theta-\sin\phi_p\dot\phi_p\cos\theta\dot\theta) \nonumber\\ &&+& \label{eq-phi_b} \cos\phi_b(\sin\phi_p\dot\phi_p^2\sin\theta - \cos\phi_p\ddot\phi_p\sin\theta - \cos\phi_p\dot\phi_p\cos\theta\dot\theta + \cos\phi_p\dot\phi_p\cos\theta\dot\theta)\end{eqnarray} 這裡可以容易看出,假如支點固定不動,也就是 \(\dot\phi_p = \ddot\phi_p = 0\),(\ref{eq-phi_b}) 式便簡化成 \begin{equation}\label{eq-phi_b-fixed}\ddot\phi_b = -2{\cos\theta \over \sin\theta}\dot\theta\dot\phi_b.\end{equation} (@@這應該不難檢查正確與否。@@)

最後把擺錘與 \(z\)-軸角度 \(\theta\) 相關的運動方程做出來(注意我們的 \(\theta\) 定義是 \(+z\) 為 0,和一般單擺定義 \(-z\) 軸為 0 是不同的), \begin{eqnarray*} & {\partial L \over \partial \dot\theta} &=&& m_bl_bl_p(\sin\phi_p\dot\phi_p\cos\theta\cos\phi_b+\cos\phi_p\dot\phi_p\cos\theta\sin\phi_b)+m_bl_b^2\dot\theta \\ \to \quad & {d \over dt}{\partial L \over \partial \dot\theta} &=&& m_bl_bl_p(\cos\phi_p\dot\phi_p^2\cos\theta\cos\phi_b+\sin\phi_p\ddot\phi_p\cos\theta\cos\phi_b-\sin\phi_p\dot\phi_p\sin\theta\dot\theta\cos\phi_b-\sin\phi_p\dot\phi_pcos\theta\sin\phi_b\dot\phi_b) \\ &&&+& m_bl_bl_p(-\sin\phi_p\dot\phi_p^2\cos\theta\sin\phi_b+\cos\phi_p\ddot\phi_p\cos\theta\sin\phi_b-\cos\phi_p\dot\phi_p\sin\theta\dot\theta\sin\phi_b+\cos\phi_p\dot\phi_p\cos\theta\cos\phi_b\dot\phi_b) \\ &&&+& m_bl_b^2\ddot\theta\end{eqnarray*} \begin{eqnarray*}{\partial L \over \partial \theta} &=&& m_bl_bl_p(\sin\phi_p\dot\phi_p(-\sin\theta\dot\theta\cos\phi_b-\cos\theta\sin\phi_b\dot\phi_b)+\cos\phi_p\dot\phi_p(-\sin\theta\dot\theta\sin\phi_b+\cos\theta\cos\phi_b\dot\phi_b)) \\ &&+& m_bl_b^2\sin\theta\cos\theta\dot\phi_b^2+m_bgl_b\sin\theta\end{eqnarray*} 每一項都除以 \(m_bl_bl_p\),按照 \(\theta\) 的微分冪次高低順序整理,再把二次微分項放在等號左邊,其餘放到右邊。等號左邊的二次微分項為 \[{l_b \over l_p}\ddot\theta.\] 等號右邊一次微分項有 \begin{eqnarray*} &&& \sin\phi_p\dot\phi_p\sin\theta\dot\theta\cos\phi + \cos\phi_p\dot\phi_p\sin\theta\dot\theta\sin\phi_b \\ &&-& \sin\phi_p\dot\phi_p\sin\theta\dot\theta\cos\phi_b - \cos\phi_p\dot\phi_p\sin\theta\dot\theta\sin\phi_b \\ &=&& 0.\end{eqnarray*} 沒有微分的項則有 \begin{eqnarray*}&&-& (\cos\phi_p\dot\phi_p^2\cos\theta\cos\phi_b+\sin\phi_p\ddot\phi_p\cos\theta\cos\phi_b-\sin\phi_p\dot\phi_p\cos\theta\sin\phi_b\dot\phi_b) \\ &&-& (-\sin\phi_p\dot\phi_p^2\cos\theta\sin\phi_b+\cos\phi_p\ddot\phi_p\cos\theta\sin\phi_b+\cos\phi_p\dot\phi_p\cos\theta\cos\phi_b\dot\phi_b) \\ &&+& \sin\phi_p\dot\phi_p(-\cos\theta\sin\phi_b\dot\phi_b)+\cos\phi_p\dot\phi_p(\cos\theta\cos\phi_b\dot\phi_b) \\ &&+& {l_b \over l_p}\sin\theta\cos\theta\dot\phi_b^2+{g \over l_p}\sin\theta\end{eqnarray*} 這裡仔細看可以發現除了最後兩項和 \(\sin\theta\) 有關,其它都是只和 \(\cos\theta\) 有關的項,把它們整理起來,\begin{eqnarray*}& \cos\theta(& - \cos\phi_p\dot\phi_p^2\cos\phi_b - \sin\phi_p\ddot\phi_p\cos\phi_b + \sin\phi_p\dot\phi_p\sin\phi_b\dot\phi_b \\ && + \sin\phi_p\dot\phi_p^2\sin\phi_b - \cos\phi_p\ddot\phi_p\sin\phi_b - \cos\phi_p\dot\phi_p\cos\phi_b\dot\phi_b) \\ && - \sin\phi_p\dot\phi_p\sin\phi_b\dot\phi_b+\cos\phi_p\dot\phi_p\cos\phi_b\dot\phi_b \quad ) \\ = \quad & \cos\theta (&(\sin\phi_p\sin\phi_b-\cos\phi_p\cos\phi_b)\dot\phi_p^2 - (\sin\phi_p\cos\phi_b+\cos\phi_p\sin\phi_b)\ddot\phi_p \quad ).\end{eqnarray*}  整合起來我們就得到關於擺錘角度 \(\theta\) 的運動方程:\begin{eqnarray}{l_b \over l_p}\ddot\theta &=&& {l_b \over l_p}\sin\theta\cos\theta\dot\phi_b^2+{g \over l_p}\sin\theta \nonumber\\ &&+& \label{eq-theta} \cos\theta((\sin\phi_p\sin\phi_b-\cos\phi_p\cos\phi_b)\dot\phi_p^2 - (\sin\phi_p\cos\phi_b+\cos\phi_p\sin\phi_b)\ddot\phi_p).\end{eqnarray} 如果支點固定不動,則 (\ref{eq-theta}) 式將簡化成 \begin{equation}\label{eq-theta-fixed}\ddot\theta = \sin\theta\cos\theta\dot\phi_b^2+{g \over l_b}\sin\theta,\end{equation} 這應該和 (\ref{eq-phi_b-fixed}) 式一樣不難驗證才對。若更進一步簡化成只有平面擺動(\(\dot\phi_b = 0\)),則很明顯只剩下 \begin{equation}\ddot\theta = {g \over l_b}\sin\theta,\end{equation} 這和常見的公式差了一個負號,那是因為我們的 \(\theta\) 角是從 \(+z\) 軸開始量起,而一般討論單擺時候的 \(\theta'\) 是從 \(-z\) 軸,兩個角度之間的關係為 \[\theta' = \pi - \theta \quad \to \quad \sin\theta' = \sin(\pi-\theta)=\sin\theta, \quad \ddot\theta' = -\ddot\theta,\] 替換過去之後就會得到 \[\ddot\theta' = -{g \over l_b}\sin\theta',\] 和常見的公式是一致的。

以上 (\ref{eq-phi_p})、(\ref{eq-phi_b})、(\ref{eq-theta}) 等三式即為本系統的運動方程,看來都頗為複雜,要獲得解析解恐怕不容易,用數值方法求解應該會比較實際些。
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
計算模型中以彈簧來模擬擺繩,以及控制支點的運動
}}}
{{Section{
結果與討論
}}}
{{Subsection{
...
}}}
{{Section{
結論
}}}
{{Paragraph{
...
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# Wikipedia [[Pendulum|https://en.wikipedia.org/wiki/Pendulum]], [[Double Pendulum|https://en.wikipedia.org/wiki/Double_pendulum]] / [[Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# [[Flexure Formula|https://www.mathalino.com/reviewer/mechanics-and-strength-of-materials/flexure-formula]]
# [[Wiki beam theory|https://en.wikipedia.org/wiki/Euler–Bernoulli_beam_theory]] / [[材料力學第 6 章彎曲|https://www.google.com.tw/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&cad=rja&uact=8&ved=0ahUKEwiMh6iH_oXaAhWCUrwKHQy1Ct0QFgg3MAE&url=https%3A%2F%2Flms.ctl.cyut.edu.tw%2Fsys%2Fread_attach.php%3Fid%3D2312797&usg=AOvVaw1ReRUkCVTNb21vDxgp0bdX]] / [[材料力學地 6 章彎曲|https://www.google.com.tw/url?sa=t&rct=j&q=&esrc=s&source=web&cd=3&cad=rja&uact=8&ved=0ahUKEwiMh6iH_oXaAhWCUrwKHQy1Ct0QFghHMAI&url=https%3A%2F%2Flms.ctl.cyut.edu]]
# [[Wiki Young's modulus|https://en.wikipedia.org/wiki/Young%27s_modulus|.tw%2Fsys%2Fread_attach.php%3Fid%3D2312802&usg=AOvVaw0efLldM7iOvaxxWr0nXJPi]]
}}}
{{Title{
IYPT 2018 第 十三 題:秤時間(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
It is...
}}}
{{Section{
前言
}}}
{{Paragraph{
[>img(20%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Wooden_hourglass_3.jpg/330px-Wooden_hourglass_3.jpg]] 秤時間是在 [[IYPT 2018 的競賽題目|http://iypt.org/Problems]] 中的第十三題,其敘述為:
>It is commonly known that an hourglass changes its weight (as measured by a scale) while flowing. Investigate this phenomenon.
>沙漏在漏沙的過程中重量會改變(用秤來量的話),這是一件眾所週知的事情。探討這個現象。
(圖片來源:[[維基百科沙漏|https://en.wikipedia.org/wiki/Hourglass]])
}}}
{{Paragraph{
......。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
原理
}}}
{{Paragraph{
沙漏...
}}}
{{Subsection{
漏沙的時候
}}}
{{Paragraph{
在漏沙的過程中,一部分的沙子在空中下落,這部分的沙子重量是【不會】被底下的秤量到的。簡單起見假設漏沙率是個常數,\[{dm \over dt} = \text{const.},\] 則在剛開始落沙且還沒有沙子到達底部的這段(非常短)時間內,由於有一小部分沙子在空中,秤盤的讀數應該會稍微下降,當沙子抵達底部時,它的重量又重新被秤盤量到,加上撞擊底部時候產生的衝擊力,會使得秤盤讀數重新上升,一會之後達到穩定。穩定期間在空中的沙量總共是 \[\Delta m = {dm \over dt} \sqrt{2h \over g},\] 其中 \(\sqrt{2h/g}\) 是每一小部分沙子 \(dm\) 在空中停留的時間。這些沙子的重量是不會傳到秤盤的,因此秤盤所受的力會減少 \begin{equation}\label{eq-fly}\Delta f_\text{fly} = \Delta mg = {dm \over dt} \sqrt{2gh}.\end{equation} 這也是一開始秤盤讀數可以減少的最大值。

當一小部分沙子 \(dm\) 落到底部的時候,它因自由落下而具有動量 \[p_f = dm v_f = dm\sqrt{2gh}.\] 這個動量將因為沙子落到底部而有所改變。在一開始底部沒有沙子的時候,落下的沙子直接撞擊沙漏的底部,一般而言我們會預期它往回彈,但是當底部已經有沙子之後,再掉下來的沙子就不一定會回彈,有可能撞擊沙堆側邊減低掉下的速率,但持續往下;也有可能一瞬間停在沙堆上之後再往下滾落等等。我們假設最簡化的情況 :__沙子落到底部後沒有反彈,且在 \(dt\) 時間內完全停止__,則秤盤因為沙子落到底部所受到的衝擊力為 \begin{equation}\label{eq-hit}\Delta f_\text{hit} = {dp_f \over dt} = {dm \over dt} \sqrt{2gh}.\end{equation}

比較上面的 (\ref{eq-fly}) 以及 (\ref{eq-hit}) 兩式可以發現,沙子產生的衝擊力正好補回在空中下落的沙子重量,也就是說,在穩定漏沙的期間沙漏的重量不會改變!
}}}
{{Subparagraph{
上面的推論看起來很棒,減少的重量都補回來了!但是這個推論在沙子對稱盤產生衝擊力的部分,特別是''【沙子在 \(dt\) 時間內停止】這個假設,其實是沒有實際根據的!''首先我們完全不知道沙子在撞擊秤盤的時候改變多少動量,我們只是做一個合理的猜測,假如沙子的動量改變大於上面的假設,那麼秤盤的重量將會增加而不是維持不變;反之如果動量改變比較小,那麼秤得的重量也將變小。

按照題意來看,重量是會改變的(眾所週知,真的嗎?),也就是說,題目已經告訴我們這個假設是不合實際的,但是到底是變重或是變輕呢?根據 2008 IYPT Reference kit 裡面的參考文獻 ...,穩定時期的重量會稍微變重,從這個結果來看,沙子撞擊底部造成的衝擊力會稍大於前面的假設。

從上面 (\ref{eq-hit}) 式來看,''重量的變化會與漏沙率 \(dm/dt\) 以及漏沙高度 \(h\) 有關'',雖然具體的關係尚未可知,但可以合理預期漏沙率越大,高度越大,變化量也會跟著越大。
}}}
{{Subparagraph{
...
}}}
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
...
}}}
{{Section{
結果與討論
}}}
{{Subsection{
...
}}}
{{Section{
結論
}}}
{{Paragraph{
...
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# Wikipedia [[Hourglass|https://en.wikipedia.org/wiki/Hourglass]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# [[Weight of Hourglass|http://demoweb.physics.ucla.edu/content/110-weight-hourglass]]
# Youtube videos: [[Floating Hourglass|https://www.youtube.com/watch?v=kctdo6rQZbY&feature=youtu.be]], [[Weight of Hourglass|https://www.youtube.com/watch?v=JIVrQQ5jRlc&feature=youtu.be]]
}}}
{{Title{
IYPT 2019 第 三 題:降頻音叉(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
It is...
}}}
{{Section{
! 問題敘述
}}}
{{Paragraph{
降頻音叉是在 [[IYPT 2019 的競賽題目|http://iypt.org/Problems]] 中的第三題,其敘述為:
>Allow a tuning fork or another simple oscillator to vibrate against a sheet of paper with a weak contact between them. The frequency of the resulting sound can have a lower frequency than the tuning fork’s fundamental frequency. Investigate this phenomenon.
>讓一個音叉或者簡單的震盪物體,稍微靠著一張紙的話,震動的頻率可以略低於它的基頻,探討這個現象。
}}}
{{Paragraph{
......。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
! 現象描述
}}}
{{Paragraph{
XX
}}}
{{Section{
! 原理與模型
}}}
{{Subsection{
!! 定性說明
}}}
{{Subparagraph{
XX
}}}
{{Subsection{
!! 定量說明
}}}
{{Subparagraph{
XX
}}}
{{Subsection{
XX
}}}
<<<
{{Subparagraph{
XX
}}}
<<<
{{Subsection{
XX
}}}
<<<
{{Subparagraph{
XX
}}}
<<<
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
...
}}}
{{Section{
結果與討論
}}}
{{Subsection{
...
}}}
{{Section{
結論
}}}
{{Paragraph{
...
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
}}}
{{Subparagraph{
常見的求解過程為
# 將加速度表示成位置和速度的函數 \(\vec a = A(\vec r, \vec v)\) ,
# 使用[[四階榮格-庫塔演算法(Runge-Kutta algorithm)|四階 Runge-Kutta 方法 -- 二階微分方程]]解二階微分方程。
** 在手動計算的年代,人們基本上''無法''求解向量方程。
** 在電腦發達的現代我們可以對向量方程做數值解。
這裡''第二個步驟是通用於任何力學系統的,因此針對個別系統只需要做第一步驟即可'',底下我們便要討論如何將此系統的加速度寫成位置與速度的函數。
}}}
{{Subparagraph{
在剛剛釋放不久,重物還在下滑期間,兩物的運動方程為 \begin{eqnarray}\label{eq-Fh}\vec F &=& M\vec g + T (-\hat r_{h,P}) &=& M \vec a_h = Ma_h \hat r_{h,P} & \quad \text{重物}, \\ \label{eq-Fl} \vec f &=& m\vec g + T (-\hat r_{l,P}) &=& m\vec a_l = m(\vec a_{l,C}+\vec a_{l,T}) & \quad \text{輕物},\end{eqnarray} 其中 \(\hat r_{h,P}\) 及 \(\hat r_{l,P}\) 分別是沿著各段細繩指向重物與輕物的單位向量,而 \(\vec a_{l,C}\) 及 \(\vec a_{l,T}\) 則分別為輕物加速度的向心與切線分量,且 \(\vec T\) 的大小會隨著兩物體的位置與速度而變。
}}}
{{Subparagraph{
在這樣的情況下,重物往下掉落一段距離 \(\Delta r_{h,P}\) 的同時也會讓輕物的擺動半徑減少 \(\Delta r_{h,P}\) (@@color:red;這個敘述有錯!@@),也就是說輕物的向心加速度除了維持當下瞬間圓周運動趨勢所需的部分之外,還要包含重物下落的加速度:\begin{equation}\label{eq-alC}\vec a_{l,C} = \left({v_l^2 \over r_{l,P}}+a_h\right)(-\hat r_{l,P}),\end{equation} 而其切線加速度則僅由重力的切線分量提供,\begin{equation}\label{eq-alT}\vec a_{l,T} = {\vec f_{l,T} \over m} = {m\vec g - (m\vec g \cdot \hat r_{l,P})\hat r_{l,P} \over m} = \vec g - (\vec g \cdot \hat r_{l,P})\hat r_{l,P}.\end{equation}
}}}
{{Subparagraph{
將 (\ref{eq-alC}) 與 (\ref{eq-alT}) 式代入 (\ref{eq-Fl}) 式並除以輕物質量 \(m\) 則得到 \begin{equation}\label{eq-T}\vec g + {T \over m}(-\hat r_{l,P}) = \vec g + \left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}} + a_h\right)(-\hat r_{l,P}) \quad \to \quad T = m\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}} + a_h\right).\end{equation} 再將(\ref{eq-T}) 式代入 (\ref{eq-Fh}) 式中則有 \begin{eqnarray}Mg \hat r_{h,P} + m\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}} + a_h\right)(-\hat r_{h,P}) &=& Ma_h \hat r_{h,P} \nonumber \\ Mg - m\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}} + a_h\right) &=& Ma_h \nonumber \\ Mg - m\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}}\right) &=& a_h\left(M+m\right) \nonumber \\ \label{eq-ah-solved} {M \over M+m}g - {m \over M+m}\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}}\right) &=& a_h.\end{eqnarray}
}}}
{{Subparagraph{
上面的 (\ref{eq-alC})、(\ref{eq-alT}) 以及 (\ref{eq-ah-solved}) 三式即為兩物體的加速度表示成位置與速度的函數,寫成向量的話就是 \begin{equation}\label{eq-ah}\boxed{ \vec a_h = \left[{M \over M+m}g - {m \over M+m}\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}}\right)\right]\hat r_{h,P},} \end{equation}\begin{equation}\label{eq-al}\boxed{\vec a_l =} \vec g + \left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}} + a_h\right)(-\hat r_{l,P}) = \boxed{\vec g + \left[{M \over M+m}g + {M \over M+m}\left(\vec g \cdot \hat r_{l,P} + {v_l^2 \over r_{l,P}}\right)\right](-\hat r_{l,P}).}\end{equation} 有了 (\ref{eq-ah})、(\ref{eq-al}) 這些式子就可以數值方法(如[[四階 Runge-Kutta 方法 -- 二階微分方程]])解出兩物體的運動狀態。
}}}
{{Title{
IYPT 2019 第 十四 題:迴旋擺(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
It is...
}}}
{{Section{
! 問題敘述
}}}
{{Paragraph{
[>img(30%,)[https://i.ytimg.com/vi/SXQ9VaYm3yQ/maxresdefault.jpg]] 迴旋擺是在 [[IYPT 2019 的競賽題目|http://iypt.org/Problems]] 中的第十四題,其敘述為:
> Connect two loads, one heavy and one light, with a string over a horizontal rod and lift up the heavy load by pulling down the light one. Release the light load and it will sweep around the rod, keeping the heavy load from falling to the ground. Investigate this phenomenon.
> 用一條細繩將兩個物體綁在一起,一重一輕,細繩跨過一根水平橫桿,將輕物體往外拉使得重物體往上升。當手放開之後,輕物體會繞著橫桿轉圈使得重物體不至於掉落地面。探討這個現象。
(圖片來源:[[YouTube Toy Physics - Looping Pendulum|https://www.youtube.com/watch?v=SXQ9VaYm3yQ]])
}}}
{{Paragraph{
......。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]] [7],數據作圖則使用 [[D3.js|https://d3js.org/]] [8],可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
! 現象描述
}}}
{{Paragraph{
如同上圖所示,當輕物體被放開的時候,會像單擺一樣往下擺盪,到橫桿下方附近會往上升,之後開始纏繞橫桿,越纏繞越靠近,整個路徑呈現螺旋狀;而重物則是在一開始的時候往下掉,到某個地方就停止下落,之後就維持在那裏(有微小擺動)。__重物停止下落的時間點,從 [[影片|https://www.youtube.com/watch?v=SXQ9VaYm3yQ]] 中可以看出大約是在輕物開始纏繞橫桿的時間點附近__。重物停止下落後,輕物仍持續纏繞橫桿,直到繩子用完為止。
}}}
{{Section{
! 原理與模型
}}}
{{Subsection{
!! 定性說明
}}}
{{Subparagraph{
當輕物剛被放開的時候有類似單擺的軌跡,以及重物會往下落,這都是可以預期的,然而重物下落到某處便停止,表示拉著重物的繩子張力有變大,而繩子張力會變大,可能有兩種原因:
# 繩子與橫桿之間的磨擦力因為接觸長度增加而變大(影片中看起來此時繩子還沒有重疊);
** 一個 [[簡單的模型|http://www.jrre.org/att_frict.pdf]] 可以定量描述接觸段的摩擦力與繩子兩端張力的關係
# 輕物端傳來的拉力變大。
** 這個狀況就跟【單擺的擺錘越靠近支點正下方,擺繩的拉力就越大】這個現象是同樣道理。
這兩個因素誰的影響比較大?應該可以做個簡單的實驗測試或者模型推論而知道。

重物停止下落之後,輕物仍然持續纏繞橫桿,但重物並沒有繼續上升而是停在原處,應該是由於繩子與橫桿之間有足夠的摩擦力,或者是繩子纏繞時會相互重疊,繩子與繩子之間有足夠摩擦力,以至於張力被抵銷使得重物停在原處。
> 假如橫桿與繩子間的摩擦力很小,且繩子在纏繞過程中沒有互相重疊,是否會讓重物上升呢?
}}}
{{Subsection{
!! 定量說明
}}}
{{Subparagraph{
這裡我們只討論簡單的情況:__重物只在垂直方向(//z//-方向)運動而沒有擺動,而輕物的運動只限於平面(//y-z// 平面,+//y//-軸朝右)__。首先我們定義描述重物與輕物的參數:重物質量為 \(M\),位置為 \(\vec r_{h,O}\),輕物質量為 \(m\),位置為 \(\vec r_{l,O}\),其中位置向量的下標 \(O\) 代表此向量是從原點(Origin)量起的。
}}}
{{Subparagraph{
接著我們定義描述繩子的參數。如果仔細觀察此系統的運動,很容易發現繩子【繫著重物(重物段)】及【繫著輕物(輕物段)】這兩段的運動方式並不一樣,將它們分開來討論應該比較好處理。另外,如果將繩子跨過橫桿的地方放大來看,如圖一 B,可以看見因為橫桿的半徑不是 0 的緣故,繩子跨過橫桿的長度也不會是 0(橫跨段,圖中的白色弧線),這一小段從圖一 B 中位於左方的紅色圓點(【重物支點】,重物段繩子剛接觸到橫桿的地方)開始,到右方的黃色圓點(【輕物支點】,輕物段繩子剛接觸到橫桿的地方)為止。若我們使用 \(\vec p_{h,O}\) 及 \(\vec p_{l,O}\) 來分別代表重物支點與輕物支點的位置,則重物段與輕物段繩子的長度分別可以寫成 \begin{eqnarray} 重物段 \quad L_h &=& |\vec r_{h,P}| &\equiv& |\vec r_{h,O} - \vec p_{h,O}|, \nonumber\\ \label{eq-string-length} 輕物段 \quad L_l &=& |\vec r_{l,P}| &\equiv& |\vec r_{l,O} - \vec p_{l,O}|,\end{eqnarray} 其中位置向量的下標 \(P\) 代表該向量是從支點(Pivot)開始量起的。
}}}
{{Subparagraph{
至於橫跨段的繩長 \(\Delta L\),我們可以利用圓弧長度的公式計算出來 \(\Delta L = R\Delta\theta\),其中 \(R\) 為橫桿的半徑,\(\Delta \theta\) 為這段弧線所對應的張角。此張角的大小,從圖一 B 可以看出,就是輕物支點的天頂角 \(\theta_{p,l}\)(polar angle,從 +//z//-軸量起的角度,圖中黃色張角)與重物支點天頂角 \(\theta_{p,h}\)(圖中紅色張角)之間的角度,我們把它寫成 \begin{equation}\label{eq-delta-theta}\Delta \theta = \theta_{p,l} + \theta_{p,h} = \theta_p + {\pi \over 2},\end{equation} 其中 \(\theta_{p,h} = \pi/2\) 是因為我們假設重物只在垂直方向運動,也因此我們把 \(\theta_{p,l}\) 的下標 \(l\) 給省去,只寫 \(\theta_p\)。這個角度其實和輕物段繩子與水平線的夾角完全一樣(參考圖一 B,灰色張角黃色張角大小一樣),因此我們直接將輕物段與水平線的夾角寫成 \(\theta_p\)(圖一 A)。
}}}
{{Figure{
|noborder|k
| [img(300px,)[image/YPT/Fig-IYPT-2019-14-01A.JPG]] | [img(300px,457px)[image/YPT/Fig-IYPT-2019-14-01B.JPG]] |
|>|圖一 A(左)運動過程中的某個瞬間,重物端繩子長度為 \(r_{h,P}\),垂直向下;輕物端繩子長度為 \(r_{l,P}\),與水平夾角為 \(\theta_p\)。B(右)這個瞬間繩子跨過橫桿(白色弧線)的長度為 \(\Delta L = R\Delta\theta\),其中 \(R\) 為橫桿半徑,\(\Delta\theta\) 為白色弧線的張角。這個張角 \(\Delta\theta\) 和輕物端繩子的水平夾角 \(\theta_p\) 之間有個簡單的關係:\(\Delta\theta = \theta_p+\pi/2\)(參考前段內文說明)。|
}}}
{{Subparagraph{
要得到定量的結果,我們可以用數值方法直接求解向量方程,也可以依照個別變數的純量方程去求解,以下會分別就兩種方式做說明並比較其結果。
}}}
{{Subsection{
!!! 摩擦力可忽略的簡單情況
}}}
{{Subparagraph{
首先我們討論沒有磨擦力的簡單情況,此時重物與輕物所受的繩子張力應是幾乎一樣的,也就是 \(T_h \sim T_l = T\)(參考圖二)。
}}}
{{Subsection{
!!!! 向量方程數值求解
}}}
{{Subparagraph{
牛頓第二運動定律 \[\vec F = m\vec a = m{d^2 \vec r \over dt^2} \quad \text{(質量不變)}\] 本身就是個向量方程,如果使用能夠處理向量的數值工具便可以直接求解這條方程,求解的過程原則上只要''知道如何計算加速度向量''即可,至於如何計算加速度,一般的做法就是
# 對個別物體畫出受力圖,寫下它的運動方程,
# 計算合力(向量),並據以計算加速度。
這兩個步驟對所有力學系統都適用,不同系統的差異只在於「所受的力不同」罷了。圖二顯示出迴旋擺的物體受力圖,據此我們可以很容易寫下重物與輕物的運動方程:\begin{eqnarray}\vec T_h + M\vec g &=& M\vec a_h, \nonumber \\ \label{eq-motion} \vec T_l + m\vec g &=& m\vec a_l.\end{eqnarray}
}}}
{{Figure{
|noborder|k
|width:45%; [img(,350px)[image/YPT/Fig-IYPT-2019-14-02A.JPG]] |width:45%;[img(,250px)[image/YPT/Fig-IYPT-2019-14-02B.JPG]] |
|>|圖二、迴旋擺系統的物體受力圖,在任何一個瞬間,兩個物體分別受到重力以及繩子的張力影響而進行運動(忽略空氣阻力)。A(左)重物受力,B(右)輕物受力。|
}}}
{{Subparagraph{
計算物體所受的力,概念上很簡單,很多時候也都有現成的公式可以套用,例如重力、電力、空氣阻力等,但也有一些力在【一般情況】下是沒有現成公式可以套用的,例如繩子張力、正向力等便是。遇到這種一般情況無現成公式可套用的力,一種簡單的方式便是【以彈簧來模擬】,例如__底下我們將以彈簧來模擬系統中的繩子__,利用虎克定律來計算彈簧回復力,並把它當成繩子的張力代入牛頓運動方程進行計算。
}}}
{{Subparagraph{
以彈簧來模擬繩子,__好處是處理起來相對簡單,''缺點是模擬的時間間隔必須夠小,否則很容易出現過大的力量使運動出現不合理的現象''__。

這個系統我們把整條繩子當成一個彈簧,當繩子被拉長的時候會產生線性回復力,沒有被拉長的時候就不會,因此整個過程中只要能算出繩子長度,就可以算出當下繩子的回復力,這個回復力就是繫在繩子上物體所受的張力。
}}}
{{Subparagraph{
繩子的長度該如何計算?只要參考圖一以及 (1) 式將三段的長度加總起來便可,也就是 \[L = L_l + L_h + \Delta L = r_{l,P} + r_{h,P} + \Delta L.\] 當這個總長超過繩子的原長 \(L_0\) 時,繩子便會產生一個回復力(張力),其大小為 \begin{equation}T = k(L -L_0),\end{equation} 其中 \(k\) 是繩子的彈性係數;回復力方向是朝著【拉回原繩長】的方向。至於彈性係數 \(k\) 值應設定為多少,就看我們預設繩子是否容易拉長來決定。簡單起見我們假設繩子很難拉長,也就是在運動過程中繩長幾乎不變,那麼給它一個很大的 \(k\) 值就可以模擬出這樣的結果。至於要多大才合理,可以參考實際物質的楊氏模數(Young's modulus)來計算出一個合理的 \(k\) 值,例如以木頭或鋼鐵的楊氏模數計算出來的 \(k\) 值,應該足以代表很難拉長的繩子。另外,也可以把它當成一個參數和實驗值擬合出一個合理的 \(k\) 值。

有了這個張力之後,把它代回 (3) 式便可據此計算物體的加速度,算出加速度就可以解出運動方程的結果。
}}}
{{Subsection{
!!!! 單變數純量方程求解
}}}
{{Subparagraph{
傳統上處理力學問題並不是直接求解向量方程,而是將運動方程改寫成幾個單變數純量方程再去求解(在手動計算的年代這可能是唯一可行的辦法?),一般而言其做法為:
# 找出決定系統動能 \(T\) 與位能 \(U\) 的變數,用這些變數寫下系統的 Lagrangian \(L = T - U\) ,
# 使用 Lagrage 方程 \begin{equation}\label{eq-LagrangesEq}{d \over dt}{\partial L \over \partial \dot q} - {\partial L \over \partial q} = 0\end{equation} 來得到對應各個變數 \(q\) 的單變數純量方程(\(\dot q \equiv dq / dt\)),
# 對這些方程求解(多個變數就會有多條方程聯立)。
** 在手動計算的年代大多只能做【簡單情況】或者【漸進行為】的解析解,
** 在現代可以用電腦做出一般清況的數值解。
這裡''第三個步驟是具有通用性的,針對個別系統只需做前兩個步驟即可'',接下來我們便討論此系統的變數與其對應的單變數純量方程。
}}}
{{Subparagraph{
__要討論影響系統的變數,原則上找出【決定位置的變數】以及【限制條件】就足夠__。我們沿用前面一段的變數(見圖一),並將原點的位能定為 0,在這簡化情況下重物只在 \(z\)-軸方向運動(正 \(z\) 垂直向上),其位置只需要 \begin{equation}\label{eq-rhp-0}r_{h,P} = -z_{h,P}\end{equation} ''一個變數''便可決定,至於輕物的運動則是在 \(y-z\) 平面上,屬於二維運動,一般而言需要''兩個變數''來決定其位置。若我們讓正 \(y\)-軸水平向右,並使用球座標的標準定義,則在 \(y-z\) 平面(\(\phi = \pi / 2\))上的__輕物支點__位置可以寫成,: \[\vec p_{l,O} = (0, \quad R\sin\theta_p, \quad R\cos\theta_p),\] 其中 \(R\) 是橫桿的半徑,\(\theta_p\) 是輕物支點的天頂角(polar angle)。由於 \(\theta_p\) 和輕物段繩子與水平線之間的夾角是相同的(參考圖一 B 及 (2) 式的說明),輕物本身的位置可以寫成 \begin{eqnarray}\vec r_{l,O} &=& \vec p_{l,O} + (0, \quad r_{l,P}\cos\theta_p, \quad -r_{l,P}\sin\theta_p) \nonumber \\ \label{eq-rlo} &=& (0, \quad R\sin\theta_p+r_{l,P}\cos\theta_p, \quad R\cos\theta_p-r_{l,P}\sin\theta_p).\end{eqnarray} 這裡的兩個變數明顯是 \(r_{l,p}\) 和 \(\theta_p\)。
}}}
{{Subparagraph{
從上面的討論我們知道要決定重物及輕物的位置需要三個變數,但是有沒有什麼限制條件呢?如果我們假設繩子總長 \(L_0\) 不變,這就是一個限制條件,它會限制這三個變數必須滿足 \begin{equation}\label{eq-constraint} -z_{h,P}+r_{l,P}+R\left(\theta_p+{\pi \over 2}\right) = L_0.\end{equation} 三個變數受到一個限制條件的約束,表示只有其中兩個變數是可以互相獨立的,這裡我們選擇和輕物直接相關的變數 \(r_{l,p}\) 及 \(\theta_p\)。經過一些代數過程(參考下方 【附錄:單變數純量方程的代數過程】一節)我們便得到這兩個變數的運動方程:\begin{equation}\label{eq-rlp-ddot}\boxed{\ddot r_{l,P} = -R\ddot\theta_p+{-Mg+m(r_{l,P}\dot\theta_p^2+g\sin\theta_p) \over M+m}.}\end{equation} 以及 \begin{equation}\label{eq-theta_p-ddot}\boxed{\ddot\theta_p = {-MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p)-2mr_{l,P}\dot r_{l,P}\dot\theta_p-(M+m)R\ddot r_{l,P} \over ((M+m)R^2+mr_{l,P}^2)}.}\end{equation} 有了 (\ref{eq-rlp-ddot})、(\ref{eq-theta_p-ddot}) 這些式子就可以數值方法(如[[四階 Runge-Kutta 方法 -- 二階微分方程]])解出兩個變數隨時間的變化,再用這些結果算出兩物體的運動狀態。
}}}
{{Subsection{
! 結果與討論
}}}
{{Subparagraph{
[[Looping Pendulum Simulation]] 做了兩種方式的模擬,結果如圖三所示。圖三 A(左)顯示以彈簧回復力來模擬繩子張力((\ref{eq-T}) 式),並直接以向量計算求解牛頓運動方程的結果;圖三 B(右)則顯示以 Lagrange 方法得到兩個純量方程((\ref{eq-rlp-ddot})、(\ref{eq-theta_p-ddot}) 二式),求其聯立解,並據以計算物體位置((\ref{eq-rhp-0})、(\ref{eq-rlo})、(\ref{eq-constraint}) 三式)的結果。圖中明顯可以看出兩個方法產生的路徑非常相近,且__計算過程中能量守恆也在 0.1% 之內符合__,表示
# 以彈簧來模擬繩子的做法是可行的,
# 在沒有摩擦力的簡化情況下【不一定】會有螺旋軌跡。
事實上,在沒有摩擦力的簡化情況下,重物可以上下來回運動而不是往下之後停止在某處,輕物的軌跡也可能沒有簡單的規律性,整體看起來頗有點複雜度。
}}}
{{Figure{
|noborder|k
|width:45%;height:430px; [img(300px,)[image/YPT/Fig-IYPT-2019-14-03A.JPG]] |width:45%; [img(300px,)[image/YPT/Fig-IYPT-2019-14-03B.JPG]] |
|>|圖三 A(左)以【彈簧模型】計算繩子張力之後求解向量方程的結果。B(右)求解 Lagrange 方法得到的聯立純量方程,再據以計算位置的結果。兩種結果頗為一致,且計算過程中能量守恆也在 0.1% 之內符合,顯示 1) 以彈簧模型來計算繩子張力是可行的,2) 在【沒有】摩擦力的簡化情況下【不一定會】出現螺旋軌跡。計算參數:總繩長 \(L_0=2.0\)m,橫桿半徑 5.0mm,初始輕物段繩長為 \(0.5L_0\),質量比為 5:1。在以彈簧模擬繩子的做法中,繩子的彈性係數 \(k \sim 34560\) N/m,是以半徑為 1.0mm 的木頭(楊氏模數 \(1.1\times 10^{10}\) Pa)計算出來的[6]。|
}}}
{{Subsection{
!!! 摩擦力【不】可忽略的情況
}}}
{{Paragraph{
如果摩擦力不可忽略(實際上也應該是如此),我們就得知道如何計算摩擦力的影響。由於橫桿的半徑不是 0,繩子跨過橫桿的那一段必定跟橫桿有摩擦力,因此重物與輕物所受的繩子張力必然有所不同,\(T_h \neq T_l\),至於兩個張力之間的關係該如何描述,可以先用一個簡單的模型試試,如果無法符合實驗結果再來修正。有一個簡單的模型 [5] \begin{equation}\label{eq-Capstan}T_\text{load} = T_\text{hold}\ e^{\mu\phi},\end{equation} 其中 \(T_\text{load}\) 是【負載】端的,\(T_\text{hold}\) 是施力端的張力。

要將這個模型用在迴旋擺系統,首先得釐清負載端與施力端是在哪一邊。我們注意到 (\ref{eq-Capstan}) 式裡的 \(e^{\mu\phi} > 1\),也就是負載端的張力比較大,\[T_\text{load} > T_\text{hold},\] 只要找出迴旋擺系統裡哪一段的張力比較大,就可以套用此模型來計算張力。我們仔細觀察一下運動過程,可以合理推測
# 在重物下落過程中應該重物端的張力較大,
# 在重物停止之後應該輕物端的張力較大。
}}}
{{Subparagraph{
接下來如果我們能夠判斷何時重物會停止,就能夠知道這個模型該如何套用在這個系統裡。至於重物在何時會停住呢?我們曉得重物下落會牽連繩子滑動,當繩子無法滑動的時候,就是重物停止的時候。繩子何時會無法滑動,明顯是當繩子繞橫桿的部分足夠長,使得摩擦力大到張力無法克服的時候。從【繩子兩端的張力差就是摩擦力】這件事,我們可以得到繩子跨橫桿的長度 \(\Delta L\) 和摩擦力 \(f\) 之間的關係,\[Te^{\mu\phi}-T = T(e^{\mu\phi}-1) = f.\] 如此我們可以明確地知道摩擦力大於張力的條件:\begin{equation}\label{eq-critical}T(e^{\mu\phi}-1)>T \quad \to \quad T(e^{\mu\phi}-2)>0 \quad \to \quad \boxed{e^{\mu\phi} > 2.}\end{equation} 當這個條件還沒發生(繩子纏繞橫桿的長度還不夠),繩子還能滑動,重物可以持續下落;但是當這個條件發生時(繩子纏繞橫桿的長度已足夠),繩子將很難滑動,重物也就很難繼續下滑。}}}
{{Subparagraph{
圖四顯示以彈簧模擬繩子來計算張力,並根據 (\ref{eq-Capstan}) 及 (\ref{eq-critical}) 式加入摩擦力的結果,的確有出現螺旋狀軌跡,不過重物並沒有中途停下來而是緩慢下落到最後,這和實驗結果並不符合,表示模型還有未盡之處尚待改進。
}}}
{{Figure{
|noborder|k
|height:480px; [img(300px,)[image/YPT/Fig-IYPT-2019-14-04.JPG]] |
|圖四 以彈簧模型計算繩子張力,並以 (\ref{eq-Capstan}) 及 (\ref{eq-critical}) 式來計算繩子與橫桿摩擦力之結果,可以清楚看見螺旋軌跡,不過重物並沒有中途停下來而是緩慢下落到最後,這和實驗結果並不符合,表示模型還有未盡之處尚待改進。計算參數:繩子與橫桿間的動摩擦係數 \(\mu_k = 0.3\),最大靜摩擦係數 \(\mu_s = 0.35\),繩子張力以彈簧模型計算,參數和圖三的計算相同。|
}}}
!!! 繩子滑動的時候
> [>img(25%,)[https://upload.wikimedia.org/wikipedia/commons/2/2e/Capstan_force_diagram_4.jpg]] 如果我們仔細讀一下[[文獻 [5]|https://en.wikipedia.org/wiki/Capstan_equation]],可以看到 (\ref{eq-Capstan}) 式其實是得在【繩子靜止的時候】才成立的。而在迴旋擺的運動過程中,只有最後重物停止落下之後的階段才符合這個情況,在重物仍持續下落的期間,繩子並不是靜止而是滑動的,並不符合這個情況。換句話說,只有重物停止下落之後才能使用 (\ref{eq-Capstan}) 式,在那之前 (\ref{eq-Capstan}) 式可能是不合用的,我們需要找出,或者導出,重物還在下落的過程中所需的公式,才能更貼近實際狀況。
>
> 這裡我們討論如何導出所需的公式。我們可以參考[[文獻 [5]|https://en.wikipedia.org/wiki/Capstan_equation]] 裡得到 (\ref{eq-Capstan}) 式的過程,將其中 \(\vec a=0\) 的地方換成重物下落的加速度 \(\vec a_h\) 即可。
>
> 正向力 \[N = (T_\text{hold}+T_\text{load})\sin\left(d\phi \over 2\right),\] 當 \(d\phi \to 0\) 的時候,\(\sin\left(\phi \over 2\right) \to {\phi \over 2},\) 且 \(T_\text{hold}\) 和 \(T_\text{load}\) 之間的差異很小,我們把它們寫成 \(T\) 和 \(T + dT\),如此則正向力近似於 \begin{equation}N \sim (T + (T+dT))\left(d\phi \over 2\right) \sim Td\phi.\end{equation}
>
> 切線方向的受力情況,在同樣 \(d\phi \to 0\) 的情況下,應該是 \begin{equation}T+dT - T - f = dm\ a_h,\end{equation} 其中 \(f\) 為繩子與桿子之間的摩擦力,\(dm\) 為對應展開角度 \(d\phi\) 一小段繩子質量,\(a_h\) 為重物的加速度。一般而言我們將摩擦力寫成 \(f = \mu N \sim \mu Td\phi\),其中 \(\mu\) 為繩子與桿子之間的磨擦係數;並且我們可以把一小段繩子的質量寫成 \(dm = \lambda r_0 d\phi\),其中 \(\lambda\) 為繩子的線密度,而 \(r_0\) 為桿子的半徑。如此一來則 (18) 式可以寫成 \begin{eqnarray*}& dT &=& \mu T d\phi + \lambda r_0 d\phi a_h \\ &&=& \left(T + {\lambda r_0 a_h \over \mu}\right) \mu d\phi \\ \to & {dT \over T + {\lambda r_0 a_h \over \mu}} &=& \mu d\phi \\ \to & \int_{T_\text{hold}}^{T_\text{load}} {dT \over T + {\lambda r_0 a_h \over \mu}} &=& \int_0^\phi \mu d\phi.\end{eqnarray*}
>
> 這個積分的結果是 \begin{eqnarray*} & \ln \left(T_\text{load} + {\lambda r_0 a_h \over \mu}\right) - \ln \left(T_\text{load} + {\lambda r_0 a_h \over \mu}\right) &=& \mu \phi \nonumber\\ \to & \ln\left(T_\text{load} + {\lambda r_0 a_h \over \mu} \over T_\text{hold} + {\lambda r_0 a_h \over \mu}\right) &=& \mu \phi \nonumber\\ \to & T_\text{load} + {\lambda r_0 a_h \over \mu} &=& \left(T_\text{hold} + {\lambda r_0 a_h \over \mu}\right)e^{\mu\phi}.\end{eqnarray*} 上式整理一下便得到 \begin{equation}\boxed{T_\text{load} = T_\text{hold}e^{\mu\phi} + {\lambda r_0 a_h \over \mu}(e^{\mu\phi}-1).}\end{equation}
>
> 和 (15) 式比較一下,(19) 式只是多了 \[{\lambda r_0 a_h \over \mu}(e^{\mu\phi}-1)\] 這一項而已,而這一項包含了重物下落的影響,可以讓我們探討重物【尚未靜止】時的運動狀態。
>>注意在獲得 (19) 式的積分過程中 \(a_h\) 是被當成常數看待,理由是在任何滑動的瞬間整條繩子應該都是以 \(a_h\) 這樣的加速度被重物拉動,而這個積分是在那個瞬間對繩子跨過桿子的部分進行的。
{{Section{
實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
計算模型
}}}
{{Paragraph{
...
}}}
{{Section{
結果與討論
}}}
{{Subsection{
!!! 摩擦力可忽略的簡單情況
}}}
{{Section{
結論
}}}
{{Paragraph{
...
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# [[Demonstration: The Looping Pendulum|http://www.csaapt.org/uploads/3/1/2/7/3127390/mungan-2014-pendulum.pdf]]
# [[Interrupted Pendulum|https://www.av8n.com/physics/loop-de-loop.htm]]
# [[Looping Pendulum - KUPDF|https://kupdf.net/download/looping-pendulum_5b45c01fe2b6f52f2865e487_pdf]]
# [[Golden Spiral|https://en.wikipedia.org/wiki/Golden_spiral]] / [[Another Golden Spiral|https://www.intmath.com/blog/mathematics/golden-spiral-6512]]
# [[Capstan equation|https://en.wikipedia.org/wiki/Capstan_equation]]
# [[Young's Modulus|https://en.wikipedia.org/wiki/Young%27s_modulus]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# [[Lagrangian Mechanics|https://en.wikipedia.org/wiki/Lagrangian_mechanics]] / [[Advanced Classical Mechanics|https://en.wikiversity.org/wiki/Advanced_Classical_Mechanics/Constraints_and_Lagrange%27s_Equations]]
# [[A non-conservative Lagrangian framework for statistical fluid registration -- SAFIRA (2010)|http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.186.2665]]
# [[Lagrange's Equation of Motion|https://studylib.net/doc/5841517/lagrange-s-equation-of-motion]]
}}}
{{Subsection{
!!! 附錄:單變數純量方程的代數過程
}}}
{{Subparagraph{
在運用 Lagrange 方法的過程中,我們首先得寫下系統的動能與未能。根據 (\ref{eq-rhp-0}) 式,我們知道重物的動能及位能可以寫成 \[KE_h = {1 \over 2}Mv_h^2 = {1 \over 2}M\dot z_{h,P}^2, \qquad UE_h = Mgz_{h,P},\] 其中 \(\dot  z_{h,P}\) 只是 \(d z_{h,P} / dt\) 的簡化寫法。而輕物的動能不容易一眼看出,我們先寫下它的速度:\begin{eqnarray}\dot{\vec r}_{l,O} = &(& \nonumber\\ && 0, \nonumber\\ && R\cos\theta_p\dot\theta_p+\dot r_{l,P}\cos\theta_p+r_{l,P}(-\sin\theta_p)\dot\theta_p, \nonumber\\ && R(-\sin\theta_p)\dot\theta_p-\dot r_{l,P}\sin\theta_p-r_{l,P}\cos\theta_p\dot\theta_p \nonumber\\ \label{eq-rlo-dot} &),\end{eqnarray} 再來寫其動能 \begin{eqnarray*}KE_l = {1 \over 2}m\dot r_{l,O}^2 &=& {1 \over 2}m&(\\ &&&& R^2\cos^2\theta_p\dot\theta_p^2+\dot r_{l,P}^2\cos^2\theta_p+r_{l,P}^2\sin^2\theta_p\dot\theta_p^2 \\ &&&& \quad +2R\dot r_{l,P}\cos^2\theta_p\dot\theta_p-2Rr_{l,P}\cos\theta_p\sin\theta_p\dot\theta_p^2-2r_{l,P}\dot r_{l,P}\cos\theta_p \sin\theta_p\dot\theta_p+ \\ &&&& +R^2\sin^2\theta_p\dot\theta_p^2+\dot r_{l,P}^2\sin^2\theta_p+r_{l,P}^2\cos^2\theta_p\dot\theta_p^2 \\ &&&& \quad +2R\dot r_{l,P}\sin^2\theta_p\dot\theta_p+2Rr_{l,P}\sin\theta_p\cos\theta_p\dot\theta_p^2+2r_{l,P}\dot r_{l,P}\cos\theta_p \sin\theta_p\dot\theta_p \\ &&&) \\ &=& {1 \over 2}m&(&R^2\dot\theta_p^2+\dot r_{l,P}^2+r_{l,P}^2\dot\theta_p^2+2R\dot r_{l,P}\dot\theta_p).\end{eqnarray*} 位能的話倒是可以直接寫下來:\[UE_l = mg(R\cos\theta_p-r_{l,P}\sin\theta_p).\]
}}}
{{Subparagraph{
假設繩子總長 \(L_0\) 不變的話,\begin{eqnarray} \label{eq-rhp} & -z_{h,P}+r_{l,P}+R\left(\theta_p+{\pi \over 2}\right) = L_0 \\ \to \quad & z_{h,P} = -L_0 + r_{l,P}+R\left(\theta_p+{\pi \over 2}\right) \nonumber\\ \label{eq-rhp-dot}\to \quad & \dot z_{h,P} = \dot r_{l,P}+R\dot\theta_p \\ \to \quad & \dot z_{h,P}^2 = \dot r_{l,P}^2 + R^2\dot\theta_p^2 + 2R\dot r_{l,P}\dot\theta_p. \nonumber \end{eqnarray} 也就是說,在繩長不變的情況下,重物的動能與位能可以用輕物支點的天頂角 \(\theta_p\) 以及輕物端的繩長 \(r_{l,P}\) 來改寫成 \[KE_h = {1 \over 2}M(\dot r_{l,P}^2 + R^2\dot\theta_p^2 + 2R\dot r_{l,P}\dot\theta_p), \quad UE_h = Mg\left(-L_0 + r_{l,P}+R\left(\theta_p+{\pi \over 2}\right)\right).\]
}}}
{{Subparagraph{
如此則此系統的 Lagrangian 為 \begin{eqnarray} L = T - U &=& {1 \over 2}M(\dot r_{l,P}^2 + R^2\dot\theta_p^2 + 2R\dot r_{l,P}\dot\theta_p) + {1 \over 2}m(R^2\dot\theta_p^2+\dot r_{l,P}^2+r_{l,P}^2\dot\theta_p^2+2R\dot r_{l,P}\dot\theta_p) \nonumber \\ && +Mg\left(L_0 - r_{l,P}-R\left(\theta_p+{\pi \over 2}\right)\right)-mg(R\cos\theta_p-r_{l,P}\sin\theta_p) \nonumber \\ &=& {1 \over 2}(M+m)(\dot r_{l,P}^2 + R^2\dot\theta_p^2+2R\dot r_{l,P}\dot\theta_p)+{1 \over 2}mr_{l,P}^2\dot\theta_p^2 \nonumber \\ && \label{eq-L} +Mg\left(L_0 - r_{l,P}-R\left(\theta_p+{\pi \over 2}\right)\right)-mg(R\cos\theta_p-r_{l,P}\sin\theta_p).\end{eqnarray} 接下來我們就要用 Lagrange's equations (\ref{eq-LagrangesEq}) 式來得到對應個別變數 \(q\) 的運動方程。
}}}
{{Subparagraph{
前面的討論中我們已經選擇 \(r_{l,P}\) 及 \(\theta_p\) 兩個變數,所以我們要來得到對應這兩個變數的運動方程。我們先來做出對應 \(r_{l,P}\) 的方程,過程如下:\begin{eqnarray*}{d \over dt}{\partial L \over \partial \dot r_{l,P}} &=& {d \over dt}(M+m)(\dot r_{l,P}+R\dot\theta_p) = (M+m)(\ddot r_{l,P}+R\ddot\theta_p), \\ {\partial L \over \partial r_{l,P}} &=& mr_{l,P}\dot\theta_p^2-Mg+mg\sin\theta_p,\end{eqnarray*} 套入 (\ref{eq-LagrangesEq}) 式則得到 \[(M+m)(\ddot r_{l,P}+R\ddot\theta_p) - (mr_{l,P}\dot\theta_p^2 - Mg + mg\sin\theta_p) = 0,\] 習慣上我們讓最高冪次項的係數變成 1,並且為了方便數值計算將其它項移到等號右邊,這樣就得到對應變數 \(r_{l,P}\) 的純量方程:\begin{equation}\boxed{\ddot r_{l,P} = -R\ddot\theta_p+{-Mg+m(r_{l,P}\dot\theta_p^2+g\sin\theta_p) \over M+m}.}\end{equation}
}}}
{{Subparagraph{
再來我們按照相同的步驟得到 \(\theta_p\) 的對應方程,\begin{eqnarray*}{d \over dt}{\partial L \over \partial \dot\theta_p} &=& {d \over dt}\left[((M+m)R^2+mr_{l,P}^2)\dot\theta_p+(M+m)R\dot r_{l,P}\right] \\ &=& ((M+m)R^2+mr_{l,P}^2)\ddot\theta_p+2mr_{l,P}\dot r_{l,P}\dot\theta_p+(M+m)R\ddot r_{l,P} \\ {\partial L \over \partial \theta_p} &=& -MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p),\end{eqnarray*} 套入 (\ref{eq-LagrangesEq}) 式則得到 \[((M+m)R^2+mr_{l,P}^2)\ddot\theta_p+2mr_{l,P}\dot r_{l,P}\dot\theta_p+(M+m)R\ddot r_{l,P} - [-MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p)] = 0.\] 同樣讓最高冪次項的係數為 1,其它項移到等號右邊,就得到 \begin{equation}\boxed{\ddot\theta_p = {-MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p)-2mr_{l,P}\dot r_{l,P}\dot\theta_p-(M+m)R\ddot r_{l,P} \over ((M+m)R^2+mr_{l,P}^2)}.}\end{equation}
}}}
{{Subparagraph{
從 (\ref{eq-rlp-ddot})、(\ref{eq-theta_p-ddot}) 兩式解出 \((r_{l,P}, \theta_p)\) 及 \((\dot r_{l,P}, \dot \theta_p)\) 之後,將需要的項代入 (\ref{eq-rhp}) 及 (\ref{eq-rlo}) 式便可分別算出重物與輕物的位置,代入 (\ref{eq-rhp-dot}) 及 (\ref{eq-rlo-dot}) 式便可算出重物與輕物的速度。重物的加速度可以從 (\ref{eq-rhp-dot}) 式很容易看出來是 \begin{equation}\label{eq-zhp-ddot}\ddot z_{h,P} = \ddot r_{l,P} + R\ddot\theta_p.\end{equation} 輕物的加速度則可以從 (\ref{eq-rlo-dot}) 式的微分得到:\begin{eqnarray}\ddot{\vec r}_{l,O} &= &(& \nonumber\\ &&& 0, \nonumber\\ &&& R(-\sin\theta_p)\dot\theta_p^2+R\cos\theta_p\ddot\theta_p+\ddot r_{l,P}\cos\theta_p+\dot r_{l,P}(-\sin\theta_p)\dot\theta_p \nonumber\\ &&& \quad +\dot r_{l,P}(-\sin\theta_p)\dot\theta_p+r_{l,P}(-\cos\theta_p)\dot\theta_p^2+r_{l,P}(-\sin\theta_p)\ddot\theta_p, \nonumber\\ &&& R(-\cos\theta_p)\dot\theta_p^2+R(-\sin\theta_p)\ddot\theta_p-\ddot r_{l,P}\sin\theta_p-\dot r_{l,P}\cos\theta_p \dot\theta_p \nonumber\\ &&& \quad -\dot r_{l,P}\cos\theta_p\dot\theta_p-r_{l,P}(-\sin\theta_p)\dot\theta_p^2-r_{l,P}\cos\theta_p\ddot\theta_p \nonumber\\ &&) \nonumber\\ &= &(& \nonumber\\ &&& 0, \nonumber\\ &&&\ddot r_{l,P}\cos\theta_p+(R\cos\theta_p-r_{l,P}\sin\theta_p)\ddot\theta_p-(R\sin\theta_p+r_{l,P}\cos\theta_p)\dot\theta_p^2-2\dot r_{l,P}\sin\theta_p\dot\theta_p, \nonumber\\ &&& -\ddot r_{l,P}\sin\theta_p-(R\sin\theta_p+r_{l,P}\cos\theta_p)\ddot\theta_p-(R\cos\theta_p-r_{l,P}\sin\theta_p)\dot\theta_p^2-2\dot r_{l,P}\cos\theta_p\dot\theta_p \nonumber\\ \label{eq-rlo-ddot} &&).\end{eqnarray} 加速度算出來之後就可以依據 (\ref{eq-motion}) 式算出繩子的張力。
}}}
{{Title{
IYPT 2019 第 十五 題:牛頓擺(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
It is...
}}}
{{Section{
! 問題敘述
}}}
{{Paragraph{
牛頓擺是在 [[IYPT 2019 的競賽題目|http://iypt.org/Problems]] 中的第十五題,其敘述為:
>The oscillations of a Newton's cradle will gradually decay until the spheres come to rest. Investigate how the rate of decay of a Newton's cradle depends on relevant parameters such as the number, material, and alignment of the spheres.
>牛頓擺的搖擺會逐漸減弱最後停止,探究此減弱的快慢與相關參數如球體數量、材質、對齊等之間的關係。
}}}
{{Paragraph{
......。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[3]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[4]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{Section{
! 現象描述
}}}
{{Paragraph{
[>img(20%,)[https://upload.wikimedia.org/wikipedia/commons/e/e8/Newtons_cradle_animation_book.gif]] 牛頓擺是一個常見的小裝置,常見的樣貌如右圖(出處:[[維基百科 牛頓擺|https://zh.wikipedia.org/zh-tw/%E7%89%9B%E9%A1%BF%E6%91%86]] 或 [[Wikipedia Newton's Cradle|https://en.wikipedia.org/wiki/Newton%27s_cradle]]),其典型的運動過程就如右圖所示一般,另外還有幾種過程也很容易產生,如[[這個影片|https://www.youtube.com/watch?v=0LnbyjOyEQ8]]所顯示。
* Ref[3]
** 繩子會震動
** 一開始不一定有接觸
* Ref[4]
** 隔壁球會跟著彈開
}}}
{{Section{
! 原理與模型
}}}
{{Subsection{
!! 定性說明
}}}
{{Subparagraph{
觀察牛頓擺的運動,很容易發現【碰撞】是其中很重要的一個過程,中學的物理告訴我們,碰撞可分為【彈性】以及【非彈性】兩類,主要的區隔在於碰撞過程中【動能是否守恆】。如果動能是守恆的,稱為彈性碰撞,動能不損耗,其結果應如上圖顯示一般。但與實際的牛頓擺比較一下,很快便可看出上圖的運動和實際並不一樣,也就是實際的牛頓擺運動不光只是完全彈性碰撞而已。
}}}
{{Subsection{
!! 定量說明
}}}
{{Subparagraph{
牛頓擺的運動,基本上是擺動加上碰撞,擺動的部分可以用單擺運動來模擬,如同[[單擺運動的一般性數值解法]]裡面所談的一般(參考實例:[[2018-11 前後左右擺]]或是[[2019-14 迴旋擺]]);而碰撞的部分則可以有【套用碰撞公式】以及【計算碰撞力道】兩種方式可以選擇:
}}}
{{Subsection{
套用碰撞公式:一維完全彈性碰撞
}}}
<<<
{{Subparagraph{
三維空間裡若有兩個粒子發生碰撞,在__''碰撞的瞬間''__,我們可以將運動過程分解為「沿著連心線」方向及「垂直於連心線」方向兩個分量來討論,沿著連心線方向會是「正向對撞」,而垂直於連心線方向的運動則是「剛剛好擦邊」的情況。正向對撞的結果有現成公式可用,如果是完全彈性碰撞,則公式為(參考維基百科[[彈性碰撞|https://zh.wikipedia.org/zh-tw/%E5%BD%88%E6%80%A7%E7%A2%B0%E6%92%9E]]) \begin{aligned}\hat n &= {\vec r_2 - \vec r_1 \over |\vec r_2 - \vec r_1|}, \\ v'_{1,n} &= {m_1-m_2 \over m_1+m_2} v_{1,n} + {2m_2 \over m_1+m_2} v_{2,n} \\ v'_{2,n} &= {2m_1 \over m_1+m_2}v_{1,n} + {m_2-m_1 \over m_1+m_2}v_{2,n}.\end{aligned} 而剛好擦邊的結果,在兩個粒子間沒有摩擦力的簡單情況下,應該沒有任何改變。結合這兩個結果,我們可以寫下三維空間中兩粒子在完全彈性碰撞,且接觸面沒有摩擦力情況下的速度改變公式:\begin{equation}\boxed{\Delta \vec v_1 = {2m_2 \over m_1+m_2}(v_{2,n}-v_{1,n})\hat n.}\end{equation} 及 \begin{equation}\boxed{\Delta \vec v_2 = {-2m_1 \over m_1+m_2}(v_{2,n}-v_{1,n})\hat n.}\end{equation}
}}}
<<<
{{Subsection{
計算碰撞力道:赫茲模型 Hertzian Model
}}}
<<tiddler "Simulation Codes##Hertzian Contact Force"">>
{{Section{
! 實驗
}}}
{{Paragraph{
實驗器材、實驗方式或步驟
}}}
{{Section{
! 計算模型
}}}
{{Paragraph{
...
}}}
{{Section{
! 結果與討論
}}}
{{Subsection{
!! 碰撞前後能量的損失
}}}
一維碰撞通用公式:\[{v'}_1 = {C_{R} m_2(v_2-v_1)+m_1v_1+m_2v_2 \over m_1+m_2} \\ {v'}_2 = {C_{R} m_1(v_1-v_2)+m_1v_1+m_2v_2 \over m_1+m_2}\] 當 \(m_1 = m_2\) 的時候,碰撞公式簡化成 \[{v'}_1 = {C_{R} m(v_2-v_1)+mv_1+mv_2 \over m+m} = {1 \over 2}[(1-C_{R})v_1+(1+C_{R})v_2] \\ {v'}_2 = {C_{R} m(v_1-v_2)+mv_1+mv_2 \over m+m} = {1 \over 2}[(1+C_{R})v_1+(1-C_{R})v_2]\] 碰撞後雙方的動能則為  \[\left({1 \over 2}m\right){1 \over 4}\left[(1-C_{R})v_1+(1+C_{R})v_2\right]^2\] \[\left({1 \over 2}m\right){1 \over 4}\left[(1+C_{R})v_1+(1-C_{R})v_2\right]^2\] 假如碰撞前 \(v_2 \ll v_1\),那麼碰撞後的總動能可近似為\[\left({1 \over 2}m\right){1 \over 4}\left[(1-C_{R})^2+(1+C_{R})^2\right]v_1^2 = \left({1 \over 2}mv_1^2\right){1 \over 2}(1+C_{R}^2)\] 如此則碰撞前後能量差異約為 \[\left({1 \over 2}mv_1^2\right)\left({1+C_{R}^2 \over 2}-1\right) = \left({1 \over 2}mv_1^2\right)\left(C_{R}^2-1 \over 2\right) \propto \left({1 \over 2}mv_1^2\right)\]

{{Section{
! 結論
}}}
{{Paragraph{
...
}}}
{{Section{
! 參考文獻
}}}
{{Paragraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# https://www.youtube.com/watch?v=8dgyPRA86K0
# https://www.youtube.com/watch?v=0LnbyjOyEQ8
# Friction of the string is [[main loss of energy|https://www.youtube.com/watch?v=uWChuDS-CbQ]]
# 傳遞 [[MythBusters|https://www.youtube.com/watch?v=BiLq5Gnpo8Q]]
# [[some parameters changed|https://www.youtube.com/watch?v=dCTo53kE3gs]]
# [[Slow Motion|https://www.youtube.com/watch?v=eUSTw8nLfZM]]
# 楊氏模數之定義為【彈性材料承受正向應力時會產生正向應變】,進一步細節可參考維基百科[[楊氏模數|https://zh.wikipedia.org/zh-tw/%E6%9D%A8%E6%B0%8F%E6%A8%A1%E9%87%8F]]([[Yang's Modulus|https://en.wikipedia.org/wiki/Young%27s_modulus]])。
# 泊松比(或蒲松比)的定義為【材料受拉伸或壓縮力時,材料會發生變形,而其橫向應變與縱向應變的比值】,進一步細節可參考維基百科[[泊松比|https://zh.wikipedia.org/zh-tw/%E6%B3%8A%E6%9D%BE%E6%AF%94]]([[Poisson's ratio|https://en.wikipedia.org/wiki/Poisson%27s_ratio]])
}}}
!!! 可能不需要了?
碰撞後 \(m_1\) 的動能為 \begin{aligned}{1 \over 2} m_1{v'}_1^2 &=& {1 \over 2} m_1 \left({C_{R} m_2(v_2-v_1)+m_1v_1+m_2v_2 \over m_1+m_2}\right)^2 \nonumber\\ &=& {1 \over 2}{m_1 \over (m_1+m_2)^2}(C_{R} m_2v_2-C_{R} m_2v_1+m_1v_1+m_2v_2)^2 \nonumber\\ &=& {1 \over 2}{m_1 \over (m_1+m_2)^2}((m_1-C_{R} m_2)v_1+(1+C_{R})m_2v_2)^2 \nonumber\\ &=& {1 \over 2}{m_1 \over (m_1+m_2)^2}\left[(m_1-C_{R} m_2)^2v_1^2+(1+C_{R})^2m_2^2v_2^2+2(m_1-C_{R} m_2)(1+C_{R})m_2v_1v_2\right]\end{aligned} 同理可得碰撞後 \(m_2\) 的動能為 \begin{equation*}{1 \over 2}m_2{v'}_2^2 = {1 \over 2}{m_2 \over (m_1+m_2)^2}\left[(m_2-C_{R} m_1)^2v_2^2+(1+C_{R})^2m_1^2v_1^2+2(m_2-C_{R} m_1)(1+C_{R})m_1v_1v_2\right]\end{equation*}
{{MOSTTitle{
IYPT 2020 第 七 題:繩子上的球(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇 ^^1^^,...,曾賢德 ^^1^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
It is...
}}}
{{MOSTSection{
! 問題敘述
}}}
{{MOSTParagraph{
[>img(30%,)[https://www.iypt.org/wp-content/uploads/2019/07/problem.jpg]] 迴旋擺是在 [[IYPT 2020 的競賽題目|https://www.iypt.org/problems/problems-for-the-33rd-iypt-2020/]] 中的第七題,其敘述為:
> Put a string through a ball with a hole in it such that the ball can move freely along the string. Attach another ball to one end of the string. When you move the free end periodically, you can observe complex movements of the two balls. Investigate the phenomenon.
> 將一條繩子穿過一個中間有洞的球,使得球可以用沿著繩子自由滑動,在繩子的一端綁上另一顆球,當你抓著繩子沒有綁球的那端並做週期性運動,可以觀察到複雜的球運動,探討這個現象。
(圖片來源:[[IYPT Problems 2020|https://www.iypt.org/problems/problems-for-the-33rd-iypt-2020/]])
}}}
{{MOSTParagraph{
......。使用程式語言為一般瀏覽器都有支援的 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]] [7],數據作圖則使用 [[D3.js|https://d3js.org/]] [8],可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 現象描述
}}}
{{MOSTParagraph{
從這個 [[加拿大做的展示影片|https://www.youtube.com/watch?v=J_qSH3plftg]] 裡可以看出,球的運動一般來講很複雜,但在某些特定條件下,可以呈現幾乎穩定的周期性運動。
}}}
{{MOSTSection{
! 原理與模型
}}}
{{MOSTSubSection{
!! 定性說明
}}}
{{MOSTSubParagraph{
球的運動一般來講很複雜,但在某些特定條件下,可以呈現幾乎穩定的周期性運動。
}}}
{{MOSTSubSection{
!! 定量說明
}}}
{{MOSTSubParagraph{
力圖
}}}
{{MOSTSubParagraph{
運動方程
}}}
{{MOSTSubParagraph{
至於橫跨段的繩長 \(\Delta L\),我們可以利用圓弧長度的公式計算出來 \(\Delta L = R\Delta\theta\),其中 \(R\) 為橫桿的半徑,\(\Delta \theta\) 為這段弧線所對應的張角。此張角的大小,從圖一 B 可以看出,就是輕物支點的天頂角 \(\theta_{p,l}\)(polar angle,從 +//z//-軸量起的角度,圖中黃色張角)與重物支點天頂角 \(\theta_{p,h}\)(圖中紅色張角)之間的角度,我們把它寫成 \begin{equation}\label{eq-delta-theta}\Delta \theta = \theta_{p,l} + \theta_{p,h} = \theta_p + {\pi \over 2},\end{equation} 其中 \(\theta_{p,h} = \pi/2\) 是因為我們假設重物只在垂直方向運動,也因此我們把 \(\theta_{p,l}\) 的下標 \(l\) 給省去,只寫 \(\theta_p\)。這個角度其實和輕物段繩子與水平線的夾角完全一樣(參考圖一 B,灰色張角黃色張角大小一樣),因此我們直接將輕物段與水平線的夾角寫成 \(\theta_p\)(圖一 A)。
}}}
{{MOSTFigure{
|noborder|k
| [img(300px,)[image/YPT/Fig-IYPT-2019-14-01A.JPG]] | [img(300px,457px)[image/YPT/Fig-IYPT-2019-14-01B.JPG]] |
|>|圖一 A(左)運動過程中的某個瞬間,重物端繩子長度為 \(r_{h,P}\),垂直向下;輕物端繩子長度為 \(r_{l,P}\),與水平夾角為 \(\theta_p\)。B(右)這個瞬間繩子跨過橫桿(白色弧線)的長度為 \(\Delta L = R\Delta\theta\),其中 \(R\) 為橫桿半徑,\(\Delta\theta\) 為白色弧線的張角。這個張角 \(\Delta\theta\) 和輕物端繩子的水平夾角 \(\theta_p\) 之間有個簡單的關係:\(\Delta\theta = \theta_p+\pi/2\)(參考前段內文說明)。|
}}}
{{MOSTSubParagraph{
要得到定量的結果,我們可以用數值方法直接求解向量方程,也可以依照個別變數的純量方程去求解,以下會分別就兩種方式做說明並比較其結果。
}}}
{{MOSTSubSection{
!!! 摩擦力可忽略的簡單情況
}}}
{{MOSTSubParagraph{
首先我們討論沒有磨擦力的簡單情況,此時重物與輕物所受的繩子張力應是幾乎一樣的,也就是 \(T_h \sim T_l = T\)(參考圖二)。
}}}
{{MOSTSubSection{
!!!! 向量方程數值求解
}}}
{{MOSTSubParagraph{
牛頓第二運動定律 \[\vec F = m\vec a = m{d^2 \vec r \over dt^2} \quad \text{(質量不變)}\] 本身就是個向量方程,如果使用能夠處理向量的數值工具便可以直接求解這條方程,求解的過程原則上只要''知道如何計算加速度向量''即可,至於如何計算加速度,一般的做法就是
# 對個別物體畫出受力圖,寫下它的運動方程,
# 計算合力(向量),並據以計算加速度。
這兩個步驟對所有力學系統都適用,不同系統的差異只在於「所受的力不同」罷了。圖二顯示出迴旋擺的物體受力圖,據此我們可以很容易寫下重物與輕物的運動方程:\begin{eqnarray}\vec T_h + M\vec g &=& M\vec a_h, \nonumber \\ \label{eq-motion} \vec T_l + m\vec g &=& m\vec a_l.\end{eqnarray}
}}}
{{MOSTFigure{
|noborder|k
|width:45%; [img(,350px)[image/YPT/Fig-IYPT-2019-14-02A.JPG]] |width:45%;[img(,250px)[image/YPT/Fig-IYPT-2019-14-02B.JPG]] |
|>|圖二、迴旋擺系統的物體受力圖,在任何一個瞬間,兩個物體分別受到重力以及繩子的張力影響而進行運動(忽略空氣阻力)。A(左)重物受力,B(右)輕物受力。|
}}}
{{MOSTSubParagraph{
計算物體所受的力,概念上很簡單,很多時候也都有現成的公式可以套用,例如重力、電力、空氣阻力等,但也有一些力在【一般情況】下是沒有現成公式可以套用的,例如繩子張力、正向力等便是。遇到這種一般情況無現成公式可套用的力,一種簡單的方式便是【以彈簧來模擬】,例如__底下我們將以彈簧來模擬系統中的繩子__,利用虎克定律來計算彈簧回復力,並把它當成繩子的張力代入牛頓運動方程進行計算。
}}}
{{MOSTSubParagraph{
以彈簧來模擬繩子,__好處是處理起來相對簡單,''缺點是模擬的時間間隔必須夠小,否則很容易出現過大的力量使運動出現不合理的現象''__。

這個系統我們把整條繩子當成一個彈簧,當繩子被拉長的時候會產生線性回復力,沒有被拉長的時候就不會,因此整個過程中只要能算出繩子長度,就可以算出當下繩子的回復力,這個回復力就是繫在繩子上物體所受的張力。
}}}
{{MOSTSubParagraph{
繩子的長度該如何計算?只要參考圖一以及 (1) 式將三段的長度加總起來便可,也就是 \[L = L_l + L_h + \Delta L = r_{l,P} + r_{h,P} + \Delta L.\] 當這個總長超過繩子的原長 \(L_0\) 時,繩子便會產生一個回復力(張力),其大小為 \begin{equation}\label{eq-T}T = k(L-L_0),\end{equation} 其中 \(k\) 是繩子的彈性係數;回復力方向是朝著【拉回原繩長】的方向。至於彈性係數 \(k\) 值應設定為多少,就看我們預設繩子是否容易拉長來決定。簡單起見我們假設繩子很難拉長,也就是在運動過程中繩長幾乎不變,那麼給它一個很大的 \(k\) 值就可以模擬出這樣的結果。至於要多大才合理,可以參考實際物質的楊氏模數(Young's modulus)來計算出一個合理的 \(k\) 值,例如以木頭或鋼鐵的楊氏模數計算出來的 \(k\) 值,應該足以代表很難拉長的繩子。另外,也可以把它當成一個參數和實驗值擬合出一個合理的 \(k\) 值。

有了這個張力之後,把它代回 (3) 式便可據此計算物體的加速度,算出加速度就可以解出運動方程的結果。
}}}
{{MOSTSubSection{
!!!! 單變數純量方程求解
}}}
{{MOSTSubParagraph{
傳統上處理力學問題並不是直接求解向量方程,而是將運動方程改寫成幾個單變數純量方程再去求解(在手動計算的年代這可能是唯一可行的辦法?),一般而言其做法為:
# 找出決定系統動能 \(T\) 與位能 \(U\) 的變數,用這些變數寫下系統的 Lagrangian \(L = T - U\) ,
# 使用 Lagrage 方程 \begin{equation}\label{eq-LagrangesEq}{d \over dt}{\partial L \over \partial \dot q} - {\partial L \over \partial q} = 0\end{equation} 來得到對應各個變數 \(q\) 的單變數純量方程(\(\dot q \equiv dq / dt\)),
# 對這些方程求解(多個變數就會有多條方程聯立)。
** 在手動計算的年代大多只能做【簡單情況】或者【漸進行為】的解析解,
** 在現代可以用電腦做出一般清況的數值解。
這裡''第三個步驟是具有通用性的,針對個別系統只需做前兩個步驟即可'',接下來我們便討論此系統的變數與其對應的單變數純量方程。
}}}
{{MOSTSubParagraph{
__要討論影響系統的變數,原則上找出【決定位置的變數】以及【限制條件】就足夠__。我們沿用前面一段的變數(見圖一),並將原點的位能定為 0,在這簡化情況下重物只在 \(z\)-軸方向運動(正 \(z\) 垂直向上),其位置只需要 \begin{equation}\label{eq-rhp-0}r_{h,P} = -z_{h,P}\end{equation} ''一個變數''便可決定,至於輕物的運動則是在 \(y-z\) 平面上,屬於二維運動,一般而言需要''兩個變數''來決定其位置。若我們讓正 \(y\)-軸水平向右,並使用球座標的標準定義,則在 \(y-z\) 平面(\(\phi = \pi / 2\))上的__輕物支點__位置可以寫成,: \[\vec p_{l,O} = (0, \quad R\sin\theta_p, \quad R\cos\theta_p),\] 其中 \(R\) 是橫桿的半徑,\(\theta_p\) 是輕物支點的天頂角(polar angle)。由於 \(\theta_p\) 和輕物段繩子與水平線之間的夾角是相同的(參考圖一 B 及 (2) 式的說明),輕物本身的位置可以寫成 \begin{eqnarray}\vec r_{l,O} &=& \vec p_{l,O} + (0, \quad r_{l,P}\cos\theta_p, \quad -r_{l,P}\sin\theta_p) \nonumber \\ \label{eq-rlo} &=& (0, \quad R\sin\theta_p+r_{l,P}\cos\theta_p, \quad R\cos\theta_p-r_{l,P}\sin\theta_p).\end{eqnarray} 這裡的兩個變數明顯是 \(r_{l,p}\) 和 \(\theta_p\)。
}}}
{{MOSTSubParagraph{
從上面的討論我們知道要決定重物及輕物的位置需要三個變數,但是有沒有什麼限制條件呢?如果我們假設繩子總長 \(L_0\) 不變,這就是一個限制條件,它會限制這三個變數必須滿足 \begin{equation}\label{eq-constraint} -z_{h,P}+r_{l,P}+R\left(\theta_p+{\pi \over 2}\right) = L_0.\end{equation} 三個變數受到一個限制條件的約束,表示只有其中兩個變數是可以互相獨立的,這裡我們選擇和輕物直接相關的變數 \(r_{l,p}\) 及 \(\theta_p\)。經過一些代數過程(參考下方 【附錄:單變數純量方程的代數過程】一節)我們便得到這兩個變數的運動方程:\begin{equation}\label{eq-rlp-ddot}\boxed{\ddot r_{l,P} = -R\ddot\theta_p+{-Mg+m(r_{l,P}\dot\theta_p^2+g\sin\theta_p) \over M+m}.}\end{equation} 以及 \begin{equation}\label{eq-theta_p-ddot}\boxed{\ddot\theta_p = {-MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p)-2mr_{l,P}\dot r_{l,P}\dot\theta_p-(M+m)R\ddot r_{l,P} \over ((M+m)R^2+mr_{l,P}^2)}.}\end{equation} 有了 (\ref{eq-rlp-ddot})、(\ref{eq-theta_p-ddot}) 這些式子就可以數值方法(如[[四階 Runge-Kutta 方法 -- 二階微分方程]])解出兩個變數隨時間的變化,再用這些結果算出兩物體的運動狀態。
}}}
{{MOSTSubSection{
! 結果與討論
}}}
{{MOSTSubParagraph{
[[Looping Pendulum Simulation]] 做了兩種方式的模擬,結果如圖三所示。圖三 A(左)顯示以彈簧回復力來模擬繩子張力((\ref{eq-T}) 式),並直接以向量計算求解牛頓運動方程的結果;圖三 B(右)則顯示以 Lagrange 方法得到兩個純量方程((\ref{eq-rlp-ddot})、(\ref{eq-theta_p-ddot}) 二式),求其聯立解,並據以計算物體位置((\ref{eq-rhp-0})、(\ref{eq-rlo})、(\ref{eq-constraint}) 三式)的結果。圖中明顯可以看出兩個方法產生的路徑非常相近,且__計算過程中能量守恆也在 0.1% 之內符合__,表示
# 以彈簧來模擬繩子的做法是可行的,
# 在沒有摩擦力的簡化情況下【不一定】會有螺旋軌跡。
事實上,在沒有摩擦力的簡化情況下,重物可以上下來回運動而不是往下之後停止在某處,輕物的軌跡也可能沒有簡單的規律性,整體看起來頗有點複雜度。
}}}
{{MOSTFigure{
|noborder|k
|width:45%;height:430px; [img(300px,)[image/YPT/Fig-IYPT-2019-14-03A.JPG]] |width:45%; [img(300px,)[image/YPT/Fig-IYPT-2019-14-03B.JPG]] |
|>|圖三 A(左)以【彈簧模型】計算繩子張力之後求解向量方程的結果。B(右)求解 Lagrange 方法得到的聯立純量方程,再據以計算位置的結果。兩種結果頗為一致,且計算過程中能量守恆也在 0.1% 之內符合,顯示 1) 以彈簧模型來計算繩子張力是可行的,2) 在【沒有】摩擦力的簡化情況下【不一定會】出現螺旋軌跡。計算參數:總繩長 \(L_0=2.0\)m,橫桿半徑 5.0mm,初始輕物段繩長為 \(0.5L_0\),質量比為 5:1。在以彈簧模擬繩子的做法中,繩子的彈性係數 \(k \sim 34560\) N/m,是以半徑為 1.0mm 的木頭(楊氏模數 \(1.1\times 10^{10}\) Pa)計算出來的[6]。|
}}}
{{MOSTSubSection{
!!! 摩擦力【不】可忽略的情況
}}}
{{MOSTParagraph{
如果摩擦力不可忽略(實際上也應該是如此),我們就得知道如何計算摩擦力的影響。由於橫桿的半徑不是 0,繩子跨過橫桿的那一段必定跟橫桿有摩擦力,因此重物與輕物所受的繩子張力必然有所不同,\(T_h \neq T_l\),至於兩個張力之間的關係該如何描述,可以先用一個簡單的模型試試,如果無法符合實驗結果再來修正。有一個簡單的模型 [5] \begin{equation}\label{eq-Capstan}T_\text{load} = T_\text{hold}\ e^{\mu\phi},\end{equation} 其中 \(T_\text{load}\) 是【負載】端的,\(T_\text{hold}\) 是施力端的張力。

要將這個模型用在迴旋擺系統,首先得釐清負載端與施力端是在哪一邊。我們注意到 (\ref{eq-Capstan}) 式裡的 \(e^{\mu\phi} > 1\),也就是負載端的張力比較大,\[T_\text{load} > T_\text{hold},\] 只要找出迴旋擺系統裡哪一段的張力比較大,就可以套用此模型來計算張力。我們仔細觀察一下運動過程,可以合理推測
# 在重物下落過程中應該重物端的張力較大,
# 在重物停止之後應該輕物端的張力較大。
}}}
{{MOSTSubParagraph{
接下來如果我們能夠判斷何時重物會停止,就能夠知道這個模型該如何套用在這個系統裡。至於重物在何時會停住呢?我們曉得重物下落會牽連繩子滑動,當繩子無法滑動的時候,就是重物停止的時候。繩子何時會無法滑動,明顯是當繩子繞橫桿的部分足夠長,使得摩擦力大到張力無法克服的時候。從【繩子兩端的張力差就是摩擦力】這件事,我們可以得到繩子跨橫桿的長度 \(\Delta L\) 和摩擦力 \(f\) 之間的關係,\[Te^{\mu\phi}-T = T(e^{\mu\phi}-1) = f.\] 如此我們可以明確地知道摩擦力大於張力的條件:\begin{equation}\label{eq-critical}T(e^{\mu\phi}-1)>T \quad \to \quad T(e^{\mu\phi}-2)>0 \quad \to \quad \boxed{e^{\mu\phi} > 2.}\end{equation} 當這個條件還沒發生(繩子纏繞橫桿的長度還不夠),繩子還能滑動,重物可以持續下落;但是當這個條件發生時(繩子纏繞橫桿的長度已足夠),繩子將很難滑動,重物也就很難繼續下滑。}}}
{{MOSTSubParagraph{
圖四顯示以彈簧模擬繩子來計算張力,並根據 (\ref{eq-Capstan}) 及 (\ref{eq-critical}) 式加入摩擦力的結果,的確有出現螺旋狀軌跡,不過重物並沒有中途停下來而是緩慢下落到最後,這和實驗結果並不符合,表示模型還有未盡之處尚待改進。
}}}
{{MOSTFigure{
|noborder|k
|height:480px; [img(300px,)[image/YPT/Fig-IYPT-2019-14-04.JPG]] |
|圖四 以彈簧模型計算繩子張力,並以 (\ref{eq-Capstan}) 及 (\ref{eq-critical}) 式來計算繩子與橫桿摩擦力之結果,可以清楚看見螺旋軌跡,不過重物並沒有中途停下來而是緩慢下落到最後,這和實驗結果並不符合,表示模型還有未盡之處尚待改進。計算參數:繩子與橫桿間的動摩擦係數 \(\mu_k = 0.3\),最大靜摩擦係數 \(\mu_s = 0.35\),繩子張力以彈簧模型計算,參數和圖三的計算相同。|
}}}
!!! 繩子滑動的時候
> [>img(25%,)[https://upload.wikimedia.org/wikipedia/commons/2/2e/Capstan_force_diagram_4.jpg]] 如果我們仔細讀一下[[文獻 [5]|https://en.wikipedia.org/wiki/Capstan_equation]],可以看到 (\ref{eq-Capstan}) 式其實是得在【繩子靜止的時候】才成立的。而在迴旋擺的運動過程中,只有最後重物停止落下之後的階段才符合這個情況,在重物仍持續下落的期間,繩子並不是靜止而是滑動的,並不符合這個情況。換句話說,只有重物停止下落之後才能使用 (\ref{eq-Capstan}) 式,在那之前 (\ref{eq-Capstan}) 式是不合用的,我們需要找出,或者導出,重物還在下落的過程中所需的公式,才能更貼近實際狀況。
>
> 這裡我們討論如何導出所需的公式。我們可以參考[[文獻 [5]|https://en.wikipedia.org/wiki/Capstan_equation]] 裡得到 (\ref{eq-Capstan}) 式的過程,將其中 \(\vec a=0\) 的地方換成重物下落的加速度 \(\vec a_h\) 即可。
>
> 正向力 \[N = (T_\text{hold}+T_\text{load})\sin\left(d\phi \over 2\right),\] 當 \(d\phi \to 0\) 的時候,\(\sin\left(\phi \over 2\right) \to {\phi \over 2},\) 且 \(T_\text{hold}\) 和 \(T_\text{load}\) 之間的差異很小,我們把它們寫成 \(T\) 和 \(T + dT\),如此則正向力近似於 \begin{equation}N \sim (T + (T+dT))\left(d\phi \over 2\right) \sim Td\phi.\end{equation}
>
> 切線方向的受力情況,在同樣 \(d\phi \to 0\) 的情況下,應該是 \begin{equation}T+dT - T - f = dm\ a_h,\end{equation} 其中 \(f\) 為繩子與桿子之間的摩擦力,\(dm\) 為對應展開角度 \(d\phi\) 一小段繩子質量,\(a_h\) 為重物的加速度。一般而言我們將摩擦力寫成 \(f = \mu N \sim \mu Td\phi\),其中 \(\mu\) 為繩子與桿子之間的磨擦係數;並且我們可以把一小段繩子的質量寫成 \(dm = \lambda r_0 d\phi\),其中 \(\lambda\) 為繩子的線密度,而 \(r_0\) 為桿子的半徑。如此一來則 (18) 式可以寫成 \begin{eqnarray*}& dT &=& \mu T d\phi + \lambda r_0 d\phi a_h \\ &&=& \left(T + {\lambda r_0 a_h \over \mu}\right) \mu d\phi \\ \to & {dT \over T + {\lambda r_0 a_h \over \mu}} &=& \mu d\phi \\ \to & \int_{T_\text{hold}}^{T_\text{load}} {dT \over T + {\lambda r_0 a_h \over \mu}} &=& \int_0^\phi \mu d\phi.\end{eqnarray*}
>
> 這個積分的結果是 \begin{eqnarray*} & \ln \left(T_\text{load} + {\lambda r_0 a_h \over \mu}\right) - \ln \left(T_\text{load} + {\lambda r_0 a_h \over \mu}\right) &=& \mu \phi \nonumber\\ \to & \ln\left(T_\text{load} + {\lambda r_0 a_h \over \mu} \over T_\text{hold} + {\lambda r_0 a_h \over \mu}\right) &=& \mu \phi \nonumber\\ \to & T_\text{load} + {\lambda r_0 a_h \over \mu} &=& \left(T_\text{hold} + {\lambda r_0 a_h \over \mu}\right)e^{\mu\phi}.\end{eqnarray*} 上式整理一下便得到 \begin{equation}\boxed{T_\text{load} = T_\text{hold}e^{\mu\phi} + {\lambda r_0 a_h \over \mu}(e^{\mu\phi}-1).}\end{equation}
>
> 和 (15) 式比較一下,(19) 式只是多了 \[{\lambda r_0 a_h \over \mu}(e^{\mu\phi}-1)\] 這一項而已,而這一項包含了重物下落的影響,可以讓我們探討重物【尚未靜止】時的運動狀態。
>>注意在獲得 (19) 式的積分過程中 \(a_h\) 是被當成常數看待,理由是在任何滑動的瞬間整條繩子應該都是以 \(a_h\) 這樣的加速度被重物拉動,而這個積分是在那個瞬間對繩子跨過桿子的部分進行的。
{{MOSTSection{
實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
計算模型
}}}
{{MOSTParagraph{
...
}}}
{{MOSTSection{
結果與討論
}}}
{{MOSTSubSection{
!!! 摩擦力可忽略的簡單情況
}}}
{{MOSTSection{
結論
}}}
{{MOSTParagraph{
...
}}}
{{MOSTSection{
參考文獻
}}}
{{MOSTParagraph{
# [[Demonstration: The Looping Pendulum|http://www.csaapt.org/uploads/3/1/2/7/3127390/mungan-2014-pendulum.pdf]]
# [[Interrupted Pendulum|https://www.av8n.com/physics/loop-de-loop.htm]]
# [[Looping Pendulum - KUPDF|https://kupdf.net/download/looping-pendulum_5b45c01fe2b6f52f2865e487_pdf]]
# [[Golden Spiral|https://en.wikipedia.org/wiki/Golden_spiral]] / [[Another Golden Spiral|https://www.intmath.com/blog/mathematics/golden-spiral-6512]]
# [[Capstan equation|https://en.wikipedia.org/wiki/Capstan_equation]]
# [[Young's Modulus|https://en.wikipedia.org/wiki/Young%27s_modulus]]
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# [[Lagrangian Mechanics|https://en.wikipedia.org/wiki/Lagrangian_mechanics]] / [[Advanced Classical Mechanics|https://en.wikiversity.org/wiki/Advanced_Classical_Mechanics/Constraints_and_Lagrange%27s_Equations]]
# [[A non-conservative Lagrangian framework for statistical fluid registration -- SAFIRA (2010)|http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.186.2665]]
# [[Lagrange's Equation of Motion|https://studylib.net/doc/5841517/lagrange-s-equation-of-motion]]
}}}
{{MOSTSubSection{
!!! 附錄:單變數純量方程的代數過程
}}}
{{MOSTSubParagraph{
在運用 Lagrange 方法的過程中,我們首先得寫下系統的動能與位能。根據 (\ref{eq-rhp-0}) 式,我們知道重物的動能及位能可以寫成 \[KE_h = {1 \over 2}Mv_h^2 = {1 \over 2}M\dot z_{h,P}^2, \qquad UE_h = Mgz_{h,P},\] 其中 \(\dot  z_{h,P}\) 只是 \(d z_{h,P} / dt\) 的簡化寫法。而輕物的動能不容易一眼看出,我們先寫下它的速度:\begin{eqnarray}\dot{\vec r}_{l,O} = &(& \nonumber\\ && 0, \nonumber\\ && R\cos\theta_p\dot\theta_p+\dot r_{l,P}\cos\theta_p+r_{l,P}(-\sin\theta_p)\dot\theta_p, \nonumber\\ && R(-\sin\theta_p)\dot\theta_p-\dot r_{l,P}\sin\theta_p-r_{l,P}\cos\theta_p\dot\theta_p \nonumber\\ \label{eq-rlo-dot} &),\end{eqnarray} 再來寫其動能 \begin{eqnarray*}KE_l = {1 \over 2}m\dot r_{l,O}^2 &=& {1 \over 2}m&(\\ &&&& R^2\cos^2\theta_p\dot\theta_p^2+\dot r_{l,P}^2\cos^2\theta_p+r_{l,P}^2\sin^2\theta_p\dot\theta_p^2 \\ &&&& \quad +2R\dot r_{l,P}\cos^2\theta_p\dot\theta_p-2Rr_{l,P}\cos\theta_p\sin\theta_p\dot\theta_p^2-2r_{l,P}\dot r_{l,P}\cos\theta_p \sin\theta_p\dot\theta_p+ \\ &&&& +R^2\sin^2\theta_p\dot\theta_p^2+\dot r_{l,P}^2\sin^2\theta_p+r_{l,P}^2\cos^2\theta_p\dot\theta_p^2 \\ &&&& \quad +2R\dot r_{l,P}\sin^2\theta_p\dot\theta_p+2Rr_{l,P}\sin\theta_p\cos\theta_p\dot\theta_p^2+2r_{l,P}\dot r_{l,P}\cos\theta_p \sin\theta_p\dot\theta_p \\ &&&) \\ &=& {1 \over 2}m&(&R^2\dot\theta_p^2+\dot r_{l,P}^2+r_{l,P}^2\dot\theta_p^2+2R\dot r_{l,P}\dot\theta_p).\end{eqnarray*} 位能的話倒是可以直接寫下來:\[UE_l = mg(R\cos\theta_p-r_{l,P}\sin\theta_p).\]
}}}
{{MOSTSubParagraph{
假設繩子總長 \(L_0\) 不變的話,\begin{eqnarray} \label{eq-rhp} & -z_{h,P}+r_{l,P}+R\left(\theta_p+{\pi \over 2}\right) = L_0 \\ \to \quad & z_{h,P} = -L_0 + r_{l,P}+R\left(\theta_p+{\pi \over 2}\right) \nonumber\\ \label{eq-rhp-dot}\to \quad & \dot z_{h,P} = \dot r_{l,P}+R\dot\theta_p \\ \to \quad & \dot z_{h,P}^2 = \dot r_{l,P}^2 + R^2\dot\theta_p^2 + 2R\dot r_{l,P}\dot\theta_p. \nonumber \end{eqnarray} 也就是說,在繩長不變的情況下,重物的動能與位能可以用輕物支點的天頂角 \(\theta_p\) 以及輕物端的繩長 \(r_{l,P}\) 來改寫成 \[KE_h = {1 \over 2}M(\dot r_{l,P}^2 + R^2\dot\theta_p^2 + 2R\dot r_{l,P}\dot\theta_p), \quad UE_h = Mg\left(-L_0 + r_{l,P}+R\left(\theta_p+{\pi \over 2}\right)\right).\]
}}}
{{MOSTSubParagraph{
如此則此系統的 Lagrangian 為 \begin{eqnarray} L = T - U &=& {1 \over 2}M(\dot r_{l,P}^2 + R^2\dot\theta_p^2 + 2R\dot r_{l,P}\dot\theta_p) + {1 \over 2}m(R^2\dot\theta_p^2+\dot r_{l,P}^2+r_{l,P}^2\dot\theta_p^2+2R\dot r_{l,P}\dot\theta_p) \nonumber \\ && +Mg\left(L_0 - r_{l,P}-R\left(\theta_p+{\pi \over 2}\right)\right)-mg(R\cos\theta_p-r_{l,P}\sin\theta_p) \nonumber \\ &=& {1 \over 2}(M+m)(\dot r_{l,P}^2 + R^2\dot\theta_p^2+2R\dot r_{l,P}\dot\theta_p)+{1 \over 2}mr_{l,P}^2\dot\theta_p^2 \nonumber \\ && \label{eq-L} +Mg\left(L_0 - r_{l,P}-R\left(\theta_p+{\pi \over 2}\right)\right)-mg(R\cos\theta_p-r_{l,P}\sin\theta_p).\end{eqnarray} 接下來我們就要用 Lagrange's equations (\ref{eq-LagrangesEq}) 式來得到對應個別變數 \(q\) 的運動方程。
}}}
{{MOSTSubParagraph{
前面的討論中我們已經選擇 \(r_{l,P}\) 及 \(\theta_p\) 兩個變數,所以我們要來得到對應這兩個變數的運動方程。我們先來做出對應 \(r_{l,P}\) 的方程,過程如下:\begin{eqnarray*}{d \over dt}{\partial L \over \partial \dot r_{l,P}} &=& {d \over dt}(M+m)(\dot r_{l,P}+R\dot\theta_p) = (M+m)(\ddot r_{l,P}+R\ddot\theta_p), \\ {\partial L \over \partial r_{l,P}} &=& mr_{l,P}\dot\theta_p^2-Mg+mg\sin\theta_p,\end{eqnarray*} 套入 (\ref{eq-LagrangesEq}) 式則得到 \[(M+m)(\ddot r_{l,P}+R\ddot\theta_p) - (mr_{l,P}\dot\theta_p^2 - Mg + mg\sin\theta_p) = 0,\] 習慣上我們讓最高冪次項的係數變成 1,並且為了方便數值計算將其它項移到等號右邊,這樣就得到對應變數 \(r_{l,P}\) 的純量方程:\begin{equation}\boxed{\ddot r_{l,P} = -R\ddot\theta_p+{-Mg+m(r_{l,P}\dot\theta_p^2+g\sin\theta_p) \over M+m}.}\end{equation}
}}}
{{MOSTSubParagraph{
再來我們按照相同的步驟得到 \(\theta_p\) 的對應方程,\begin{eqnarray*}{d \over dt}{\partial L \over \partial \dot\theta_p} &=& {d \over dt}\left[((M+m)R^2+mr_{l,P}^2)\dot\theta_p+(M+m)R\dot r_{l,P}\right] \\ &=& ((M+m)R^2+mr_{l,P}^2)\ddot\theta_p+2mr_{l,P}\dot r_{l,P}\dot\theta_p+(M+m)R\ddot r_{l,P} \\ {\partial L \over \partial \theta_p} &=& -MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p),\end{eqnarray*} 套入 (\ref{eq-LagrangesEq}) 式則得到 \[((M+m)R^2+mr_{l,P}^2)\ddot\theta_p+2mr_{l,P}\dot r_{l,P}\dot\theta_p+(M+m)R\ddot r_{l,P} - [-MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p)] = 0.\] 同樣讓最高冪次項的係數為 1,其它項移到等號右邊,就得到 \begin{equation}\boxed{\ddot\theta_p = {-MgR+mg(R\sin\theta_p+r_{l,P}\cos\theta_p)-2mr_{l,P}\dot r_{l,P}\dot\theta_p-(M+m)R\ddot r_{l,P} \over ((M+m)R^2+mr_{l,P}^2)}.}\end{equation}
}}}
{{MOSTSubParagraph{
從 (\ref{eq-rlp-ddot})、(\ref{eq-theta_p-ddot}) 兩式解出 \((r_{l,P}, \theta_p)\) 及 \((\dot r_{l,P}, \dot \theta_p)\) 之後,將需要的項代入 (\ref{eq-rhp}) 及 (\ref{eq-rlo}) 式便可分別算出重物與輕物的位置,代入 (\ref{eq-rhp-dot}) 及 (\ref{eq-rlo-dot}) 式便可算出重物與輕物的速度。重物的加速度可以從 (\ref{eq-rhp-dot}) 式很容易看出來是 \begin{equation}\label{eq-zhp-ddot}\ddot z_{h,P} = \ddot r_{l,P} + R\ddot\theta_p.\end{equation} 輕物的加速度則可以從 (\ref{eq-rlo-dot}) 式的微分得到:\begin{eqnarray}\ddot{\vec r}_{l,O} &= &(& \nonumber\\ &&& 0, \nonumber\\ &&& R(-\sin\theta_p)\dot\theta_p^2+R\cos\theta_p\ddot\theta_p+\ddot r_{l,P}\cos\theta_p+\dot r_{l,P}(-\sin\theta_p)\dot\theta_p \nonumber\\ &&& \quad +\dot r_{l,P}(-\sin\theta_p)\dot\theta_p+r_{l,P}(-\cos\theta_p)\dot\theta_p^2+r_{l,P}(-\sin\theta_p)\ddot\theta_p, \nonumber\\ &&& R(-\cos\theta_p)\dot\theta_p^2+R(-\sin\theta_p)\ddot\theta_p-\ddot r_{l,P}\sin\theta_p-\dot r_{l,P}\cos\theta_p \dot\theta_p \nonumber\\ &&& \quad -\dot r_{l,P}\cos\theta_p\dot\theta_p-r_{l,P}(-\sin\theta_p)\dot\theta_p^2-r_{l,P}\cos\theta_p\ddot\theta_p \nonumber\\ &&) \nonumber\\ &= &(& \nonumber\\ &&& 0, \nonumber\\ &&&\ddot r_{l,P}\cos\theta_p+(R\cos\theta_p-r_{l,P}\sin\theta_p)\ddot\theta_p-(R\sin\theta_p+r_{l,P}\cos\theta_p)\dot\theta_p^2-2\dot r_{l,P}\sin\theta_p\dot\theta_p, \nonumber\\ &&& -\ddot r_{l,P}\sin\theta_p-(R\sin\theta_p+r_{l,P}\cos\theta_p)\ddot\theta_p-(R\cos\theta_p-r_{l,P}\sin\theta_p)\dot\theta_p^2-2\dot r_{l,P}\cos\theta_p\dot\theta_p \nonumber\\ \label{eq-rlo-ddot} &&).\end{eqnarray} 加速度算出來之後就可以依據 (\ref{eq-motion}) 式算出繩子的張力。
}}}
{{MOSTTitle{
IYPT 2021 第二題:轉圈磁鐵(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對繞圈磁鐵的問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
繞圈磁鐵是在 [[IYPT 2021 的競賽題目|https://www.iypt.org/problems/problems-for-the-34th-iypt-2021/]]中的第二題,其敘述為:
>Button magnets with different diameters are attached to each end of a cylindrical battery. When placed on an aluminum foil the object starts to circle. Investigate how the motion depends on relevant parameters.
>將不同大小的鈕扣型磁鐵吸附在圓柱型小電池兩端,放在一張鋁箔上便會開始繞圈圈。探討這個運動和相關參數的關係。
網路上可以找到許多影片展示這個現象,例如[[建中物理辯論社展示影片|https://m.facebook.com/story.php?story_fbid=978197619260127&id=136007673479130]]便很清楚地展現出主要的現象。
}}}
{{MOSTParagraph{
這題的實作相對容易。
這篇文章對繞圈磁鐵的運動進行定量模擬,計算磁鐵(以及電池)在鋁箔上面的受力,並據此計算其運動狀態,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTSubParagraph{
兩端吸附磁鐵的電池在鋁箔上之所以會繞圈,定性上可以這樣說明:
# 有電流從電池正極經過磁鐵流進鋁箔,並經由鋁箔流到負極的磁鐵而回到電池形成迴路(圖一),
# 電流在鋁箔裡流動時會受到磁鐵磁場的磁力,而磁鐵也同時受到其反作用力而運動(圖二)。
** 這裡也可以說成是【電流產生磁場,與磁鐵的磁場交互作用而互相施力】。
}}}
{{Figure{
|noborder |k
| [img(80%,)[image/YPT/Fig-IYPT-2021-02-01A.jpg]] |
|圖一、繞圈磁鐵的組成:兩個強力磁鐵分別吸附在電池的正極(右方)與負極(左方),並放置在鋁箔上形成迴路,其電流(綠色箭頭)從電池正極經吸附磁鐵(右方黃色部份)流出至鋁箔,沿鋁箔流至吸附在負極的磁鐵(左方青色部份),再流回負極。為了計算受力,我們將電流路徑切割成許多小段,在每一小段的位置計算磁場(黃色及青色箭頭)以及磁力(圖二)。磁場計算為將單個磁鐵依圓柱座標切割成 20 &times; 60 &times; 10 個小塊,每個小塊套用微小磁偶極的磁場公式積分而成。電流路徑則簡單假設為直線。|
}}}
{{MOSTParagraph{
上列圖一顯示一個磁鐵-電池系統在運動過程中電流的流動情形,圖中電池的正極在右方,負極在左方,電流(圖中綠色箭頭)從正極流出,經由磁鐵、鋁箔回到負極。電流的路徑被簡單假設為直線,並且被切割成多個小段,在每一小段的位置我們計算磁鐵產生的磁場(圖中黃色及青色箭頭)。磁場計算為將單個磁鐵依圓柱座標切割成 20 &times; 60 &times; 10 個小塊,每個小塊套用微小磁偶極的磁場公式積分而成。

下列圖二顯示的則為依照圖一的電流路徑以及磁鐵之切割方式所計算,''各個小段電流在兩個強力磁鐵的磁場下流動時所受到的磁力''(圖中白色箭頭),依照勞倫茲力公式 \(d\vec F_\text{B,I} = Id\vec l \times \vec B\)計算。電流所受的總磁力為全部小段受力之向量和,也就是 \(\vec F_\text{B,I} = \int d\vec F_\text{B,I}\),而磁鐵所受的力則為其反作用力 \(\vec F_\text{B,magnet} = -\vec F_\text{B,I}\)。''注意圖中可見在__磁鐵內部__流動的電流所受的磁力方向朝左,而在__磁鐵外部__的則以朝右為主,且電流在磁鐵內部所受的磁力是大於外部的。''
}}}
{{Figure{
|noborder|k
| [img(80%,)[image/YPT/Fig-IYPT-2021-02-01B.jpg]] |
|圖二、電池正極端局部放大,顯示''各個小段電流在磁鐵內部以及附近鋁箔流動時所受到的磁力'' \(d\vec F_\text{B,I}\)(白色箭頭),依照電流受磁力公式(勞倫茲力) \(d\vec F_\text{B,I}  = Id\vec l \times \vec B\) 計算。電流所受的總磁力為全部小段受力之向量和,也就是 \(\vec F_\text{B,I} = \int d\vec F_\text{B,I}\),而磁鐵所受的力則為其反作用力 \(\vec F_\text{B,magnet} = -\vec F_\text{B,I}\)。''注意圖中可見在__磁鐵內部__流動的電流所受的磁力方向朝左,而在__磁鐵外部__的則以朝右為主,且電流在磁鐵內部所受的磁力是大於外部的。''|
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 計算模型
}}}
{{MOSTSubParagraph{
模擬計算的想法是
<<<
# 將鋁箔想像成很多小塊鋁箔的組合,每一小塊鋁箔體積為 \(d\tau;\)
# 將磁鐵想像成非常多【整齊排列】的微小磁偶極矩,每個微小磁偶極矩佔據體積 \(d\tau'\),擁有磁矩 \(d\vec \mu = \vec Md\tau'\)(\(\vec M\) 為單位體積內的磁矩,也稱為磁化量),套用微小磁偶極矩的磁場公式,積分計算出磁鐵在每一小塊鋁箔處所產生的磁場 \[\vec B(\vec r) = {\mu_0 \over 4\pi} \int d\tau' \left[{3(\vec M \cdot \hat r)\hat r - \vec M \over r^3}+{8\pi \over 3}\vec M\delta(\vec r)\right] \tag{1};\]
# 將電流分佈簡化成直線,且侷限在沿著兩端磁鐵和鋁箔接觸點的連線附近,以簡化計算。
# 計算流經每一小塊鋁箔的電流 \[I d\vec l = \vec j d\tau;\]
# 計算每一小段電流所受的磁力^^[3]^^ \[d\vec F_\text{B,I} = Id\vec l \times \vec B;\]
** 較嚴謹的做法應該是要求解 Laplace 方程 \(\nabla^2 V = 0\),計算出電池在每一小塊鋁箔所產生的電位勢 \(V(\vec r)\),再據此算出電場 \(\vec E (\vec r) = -\vec \nabla V\),然後依照 \(I d\vec l= \sigma \vec E d\tau\) 來計算電流,如此則小段電流所受磁力為 \(d\vec F_\text{B,I} = \sigma d\tau \vec E \times \vec B。\)
# 將所有小段電流所受的磁力加起來便是電流的總受力,其反作用力便是電流作用於磁鐵的力。
<<<
}}}
{{MOSTParagraph{
實際的計算中的磁場將以銣鐵硼強力磁鐵表面實際測量到的磁場值來校正,鋁箔、電池等之尺寸與質量皆由實驗值代入,鋁箔與磁鐵之電阻率分別為 \(2.65 \times 10^{-8} \Omega \cdot \text{m}\) 及 \(140 \times 10^{-6} \Omega \cdot \text{m}\),由維基百科獲得^^[4,5]^^,電阻的估計則是假設電流為【均勻通過整個截面】,電池內電阻則使用一般電池在室溫時的內電阻 0.15 &Omega;^^[6]^^,且暫時假設電池溫度沒有明顯變化。
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTSubSection{
!! ''移動與轉動''
}}}
{{MOSTParagraph{
實驗顯示,當磁鐵順向排列(磁矩同向)時,整個電池-磁鐵系統主要的運動模式是【原地轉圈】,轉圈方向與磁矩方向有關。例如,若以俯視角度來看,當兩個磁鐵的磁矩方向都是【負極到正極】,此時電池-磁鐵系統地轉圈方向為順時鐘方向;而當磁矩方向都是【正極到負極】時,轉圈的方向則為逆時鐘方向。}}}
{{MOSTSubParagraph{
而當磁鐵逆向排列時(磁矩反向),則電池-磁鐵系統的主要運動模式變成【前進後退】,移動的方向則與磁鐵方向及電流方向有關。同樣以俯視角度來看(正極朝右),如果正極的磁矩方向是由正到負,負極磁矩方向相反,我們就會看到鄭個系統往前進,反之則往後退。}}}
{{MOSTSubParagraph{
我們可以很容易定性地了解為何磁鐵逆向排列時小火車可以移動。線圈中電流產生的磁場等效於 N 極在電池負極側的磁矩所產生的,則當負極側磁鐵的 N 極朝向負極,也就是磁鐵 N 極對著等效磁矩的 N 極時,兩個 N 極會產生互斥作用;此時如果正極側磁鐵的 N 極是朝向正極,也就是對著等效磁矩的 S 極,則此兩個磁矩會產生相吸作用。負極側相斥以及正極側相吸,對小火車而言受力方向是相同的(圖一的右方),因此小火車以負極在前正極在後的方式前進。如果把兩個磁鐵都反向,則小火車受力方向也跟著反向(圖一的左方),就會變成正極在前負極在後的方式前進。反過來說,如果磁鐵是順向排列時,我們也能夠很快地理解兩端都是相吸或者都是相斥,受力互相抵消而使得小火車無法前進。}}}
{{MOSTSubSection{
!! ''移動速度''
}}}
{{MOSTParagraph{
實驗結果顯示小火車的移動速度
}}}
{{MOSTSubSection{
!! ''終端速度''
}}}
{{MOSTParagraph{
* 終端速度的現象
* 到達終端速度的時間
* 終端速度的大小
台灣區比賽時大部分的實驗結果都呈現有終端速度的現象,
物理上要有終端速度,通常需要一個【與速度呈正相關】,也就是速度越快就越大的阻力存在,最簡單的情況便是阻力與速度成正比,
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
此篇文章採取計算磁鐵對電流產生的磁力來獲得其反作用力,也就是電流對磁鐵的磁力,比起位能微分的計算方式,相對較為節省計算資源。計算結果可以定量說明靠近磁鐵的電流對磁鐵受力有主要的影響,亦可說明磁鐵排列方式與移動與否的關聯。藉由將運動軌跡擬合至與實驗相符,可推算出線圈與小火車之間摩擦力的形式為...,此摩擦力的可能來源出了移動摩擦,還有線圈的縱向震盪,以及磁力產生的力矩。此處計算忽略電池的升溫可能造成電流的改變。
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTParagraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# 電磁學課本,維基百科
# [[Hyperphysics Resistivity|http://hyperphysics.phy-astr.gsu.edu/hbase/Tables/rstiv.html]] and  [[Wikipedia Electrical Resistivity and Conductivity|https://en.wikipedia.org/wiki/Electrical_resistivity_and_conductivity]]
# [[Wikipedia Neodymium magnets|https://en.m.wikipedia.org/wiki/Neodymium_magnet]]
# [[Wikipedia Internal Resistance|https://en.m.wikipedia.org/wiki/Capacitor_types]]
}}}
{{MOSTTitle{
IYPT 2021 第十二題:威伯佛斯擺
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對繞圈磁鐵的問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
繞圈磁鐵是在 [[IYPT 2021 的競賽題目|https://www.iypt.org/problems/problems-for-the-34th-iypt-2021/]]中的第十二題,其敘述為:
>A Wilberforce pendulum consists of mass hanging from a vertically oriented helical spring. The mass can both move up and down on the spring and rotate about its vertical axis. Investigate the behavior of such a pendulum and how it depends on relevant parameters.
>威伯佛斯擺的組成是一個質量掛在垂直的彈簧下端,且此質量除了能夠上下震盪,還能夠對彈簧的軸旋轉。探討這樣一個擺的行為,以及相關參數如何影響這些行為。
}}}
{{MOSTSubParagraph{
這題的實作重點在於【找出適當的配重方式】使得耦合的現象可以被清楚地看見。【耦合現象可以看得很清楚】的意思是,兩種運動同時存在,但有時候其中一種比較明顯,有時候另外一種比較明顯。

在實作中可以發現討論的重點包含......
}}}
{{MOSTSubParagraph{
這篇文章對威伯佛斯擺的運動進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTParagraph{
威伯佛斯擺的特徵,主要是在擺錘上下震盪的過程中,還帶有扭轉的成分。在這個過程裡【上下震盪】以及【扭轉】這兩部分的運動,如果沒有相互影響的話,個別都可以從大一普通物理或是高中物理的【簡諧震盪】來理解,困難度不是很高。但實際上這兩個運動會有相互影響(專有名詞稱為【耦合】),使得運動的能量在兩種運動模式之間轉換,有時候上下震盪比較明顯,有時候卻是扭轉比較明顯,而大部分的時間是兩種運動都可以觀察到。因為這個耦合,使得威伯佛斯擺的運動過程,比起單純的上下震盪或是單純的扭轉,要來得有趣很多!值得讓對物理有興趣的學生們深入探討一番。只是這個耦合到底從哪裡來的?在下面的討論裡我們試著從彈簧的構造來理解它。
}}}
{{MOSTSubSection{
!!! 彈簧的扭轉與伸縮運動
}}}
{{MOSTSubParagraph{
一般彈簧是將一條金屬線繞成螺旋狀而成,通常所講的彈簧長度指的是【成螺旋狀之後的長度】,並不是【原來的金屬線長度】,很明顯原來金屬線的長度要比做成螺旋狀之後的長度大很多,且只要彈簧本身的溫度變化不大,我們應可合理假設
#【做成彈簧的金屬線長度不變】,或者至少【金屬線的長度變化可忽略】;而且
#在彈性限度內應可合理預期螺旋的圈數不會有明顯的改變。
}}}
{{MOSTSubParagraph{
當我們在談虎克定律 \[\vec f = -k\vec x\] 時所說的【伸長量 \(\vec x\)】指的並不是【金屬線的長度變化】,而是【螺旋狀的長度變化】,也就是【螺旋之間的距離】在變大或變小,或者說是【每個螺旋的角度在變大或變小】:螺旋狀的長度越長,表示螺旋間的距離越遠,也就是螺旋的角度越大。
}}}
{{MOSTSubParagraph{
假設我們將彈簧的一個端點固定住,抓住另一個可移動端點來改變彈簧螺旋狀的長度(不放開這個端點所以沒有震盪),則當螺旋長度改變的同時,除了明顯可知螺旋間距以及螺旋角度會有相對應的變化之外,還可能會有下列的影響:
# 如果螺旋半徑維持不變,則當螺旋間距變大,也就是螺旋角度變大的時候,每完成一圈螺旋所需的金屬線長度將會更長,因此在總長不變的前提下,彈簧可完成的圈數會稍微少一點點(但不會少太多所以圈數並沒有明顯變化),導致的結果就是移動端點的位置將會稍微沿螺旋方向退縮一點;反之則前進一點。
# 如果可移動端點的位置沿螺旋方向維持不變,表示螺旋圈數要維持不變,那麼每完成一圈螺旋所需的金屬線長度就不能改變(因為金屬線長度不變),這樣一來在螺旋角度變大時,螺旋的半徑就必須減小才能維持一圈螺旋的長度不變;反之則變大。
實際上當我們掛重物讓彈簧運動的時候,我們並不會刻意固定彈簧的螺旋半徑,也沒有強加限制可移動端點沿螺旋方向的位置,所以可預期實際運動時【可移動端點位置沿螺旋方向改變】以及【螺旋半徑改變】這兩個現象都有可能發生。
> 當然也可能還有其它方向的運動,不過這裡我們暫時只討論簡單的情況。
}}}
{{MOSTSubParagraph{
現在我們讓彈簧成垂直方向,上端固定,並把重物掛在下端,我們知道這個系統會受到重力及彈力影響而上下震盪,震盪的平衡點是彈力跟重力相互抵消的地方。前面的討論告訴我們,彈簧伸縮的過程會伴隨著扭轉,這個扭轉自然也會帶動重物跟著一起扭轉。但由於重物扭轉快慢會受到它的轉動慣量影響,不會是即時的,也就牽制著彈簧的扭轉,使得在任何時刻彈簧扭轉的角度 \(\phi\) 不見得會剛好等於它在那個長度時應該有的角度 \(\phi_0\),此時就會有【讓角度 \(\phi\) 趨向應有角度 \(\phi_0\) 】的扭力產生,而這個「趨向應有角度 \(\phi_0\)」的扭力所產生的效果,就和「趨向平衡點的彈力造成對平衡點的來回震盪」一樣,會產生「以 \(\phi_0\) 為平衡角度的來回扭轉震盪」。
}}}
{{MOSTSubParagraph{
這就是為什麼威伯佛斯擺會有【扭轉】運動的原因。由於這個扭轉和伸縮是有明確關聯的,或者說是有耦合的,這個耦合關係要能以代數寫出來才能夠據以計算運動過程。實際的關係如果能夠在實驗上測量得出,那是最好,如果實驗上不容易,我們就從最簡單的關係,也就是線性關係開始嘗試,計算出結果後和實驗比較,如果吻合得不好,再進一步調整。
}}}
{{MOSTSubSection{
!!! 扭轉與伸縮的耦合
}}}
{{MOSTSubParagraph{
不管有沒有實驗上測量的耦合關係,我們其實都可以嘗試以最簡單的線性關係來計算。首先我們選擇使用圓柱座標,讓 \(z\)-軸方向和彈簧的中心軸一致,這樣一來彈簧的伸長量以及扭轉角度可以分別用圓柱座標系的 \(z\) 以及 \(\phi\) 來描述。我們''將原點選在【垂直方向平衡點】的位置,並讓此時彈簧可移動端點正好落在 \(x\)-軸上,也就是說 \(\bf{\phi_0(0) = 0}\)''。如此我們可以把平衡角度 \(\phi_0\) 和伸長量 \(z\) 之間的關係簡單寫成 \begin{equation}\phi_0(z) = Cz,\end{equation} 其中 \(C\) 是比例係數,代表彈簧伸長一單位長度時應有的平衡角度。此係數的數值可以由【擬合實驗數據來決定】(如果實驗上有測得耦合關係的數據),或者由【擬合計算結果和實驗結果來決定】(可以是軌跡、週期、或者任何實驗上方便獲得的結果)。
> 當然我們也可以隨自己喜好把這個關係寫成 \(z_o = C'\phi\),並不會影響將要討論的物理。
}}}
{{MOSTSubSection{
!!! 運動方程
}}}
{{MOSTSubParagraph{
前面提到彈簧的運動包含【伸縮】以及【扭轉】,我們也曉得伸縮部分可以用伸長量 \(z\) 來描述,扭轉部分則使用扭轉角度 \(\phi\) 來描述,也就是這個系統有兩個參數,或者講【兩個廣義座標】,我們需要兩條運動方程來完整描繪運動過程。一般來講''如果能夠知道物體的受力(或者力矩),就直接使用牛頓運動方程即可'',不必繞圈採取別的方法來獲得運動方程。不過實際上我們常常無法知道受力的細節,例如正向力、繩張力、摩擦力等,尤其是在有耦合的情況,受力細節一般無法在彈指之間就想清楚,這種時候我們有兩種做法:
# 認真找出/想出一個模型來討論合理的受力細節,比如說用彈簧模型來討論繩張力,又如用 [[Hertzian 模型|https://en.wikipedia.org/wiki/Contact_mechanics]] 來討論正向力等。或者
# 如果受力細節不易討論,可套用拉格朗日方程(Lagrange's equation)來得到運動方程。
__在這個例子裡我們要套用拉格朗日方程來得到運動方程。__
}}}
{{MOSTSubParagraph{
要套用拉格朗日方程,首先得寫下系統的拉格朗日量(Lagrangian) \[L \equiv T - U,\] 其中 \(T\) 是系統的動能,\(U\) 則是其位能。
}}}
{{MOSTSubParagraph{
''伸縮運動''的動能就是 \begin{equation}T_\text{ 伸縮} = {1 \over 2}m\dot z^2,\end{equation} 其中 \(m\) 是掛在彈簧下方使彈簧發生伸縮運動的重物質量,\(\dot z\) 表示 \(z\) 方向的速率;位能則是彈力位能加上重力位能 \begin{equation}U_\text{ 伸縮} = {1 \over 2}k_z z^2 + mgz,\end{equation} 其中 \(k_z\) 是彈簧的伸縮彈性係數。
}}}
{{MOSTSubParagraph{
''扭轉運動''的動能則是 \begin{equation}T_\text{ 扭轉} = {1 \over 2} I \dot\phi^2,\end{equation} 其中 \(I\) 是掛在彈簧下方重物對彈簧對稱軸的轉動慣量;位能則是 \begin{equation}U_\text{ 扭轉} = {1 \over 2}k_\phi (\phi - \phi_0)^2 = {1 \over 2}k_\phi (\phi - Cz)^2,\end{equation} 其中 \(k_\phi\) 是扭轉運動的彈性係數。
}}}
{{MOSTSubParagraph{
將 (2) 到 (5) 式合併起來則得到此系統的 Lagragian 為 \begin{align*} L &= \left({1 \over 2}m \dot z^2 - {1 \over 2}k_z z^2 - mgz\right) + \left({1 \over 2}I\dot\phi^2 -{1 \over 2}k_\phi(\phi-Cz)^2\right) \\ &= \left({1 \over 2}m \dot z^2 - {1 \over 2}k_z z^2 - mgz\right)+\left({1 \over 2}I\dot\phi^2-{1 \over 2}k_\phi\phi^2+k_\phi Cz\phi - {1\over 2}k_\phi C^2 z^2\right).\end{align*} 我們把各個變數相關的項和耦合項分開來寫的話,\begin{equation}L = \left({1 \over 2}m \dot z^2 - {1 \over 2}k_z z^2 - mgz - {1\over 2}k_\phi C^2 z^2\right)+\left({1 \over 2}I\dot\phi^2-{1 \over 2}k_\phi\phi^2\right)+k_\phi Cz\phi,\end{equation} 可以看到耦和項就是最後一項 \[k_\phi C z\phi,\] 其中''耦合常數為 \(\bf{k_\phi C}\)。''}}}
{{MOSTSubParagraph{
接下來我們就套用拉格朗日方程來分別得到兩個變數的運動方程。

關於伸縮運動的方程為:\[{d \over dt}{\partial L \over \partial \dot z} = {\partial L \over \partial z} \quad \to \quad m\ddot z = -kz-k_\phi C^2z + k_\phi C\phi - mg,\] \begin{equation}\to \quad \boxed{\ddot z = {-kz + k_\phi C(\phi-Cz) \over m} - g.}\end{equation} 關於扭轉運動的方程則為 \[{d \over dt}{\partial L \over \partial \dot\phi} = {\partial L \over \partial \phi} \quad \to \quad I\ddot\phi = -k_\phi\phi + k_\phi Cz,\] \begin{equation}\to \quad \boxed{\ddot \phi = {-k_\phi(\phi - Cz) \over I}}.\end{equation}

有了運動方程 (7) 和 (8),我們就可以進行數值計算來模擬運動軌跡。以下所列為計算加速度的 Python 程式碼,建議搭配[[榮格庫塔方|四階 Runge-Kutta 方法 -- 二階微分方程]]法計算運動軌跡:
}}}
{{{
def calcA(r,v,t,a):
	z = (r[0]-spring.pos).mag - spring.Leq
	dphi = r[1].z - spring.phi_eq - spring.C * z
	F_l = -spring.Kz * z
	F_phi = spring.Kphi*spring.C*dphi

	# z acceleration
	a[0] = (r[0] - spring.pos).norm()*(F_l+F_phi)/(rod.mass+nut.mass) + g	# g must be a vector

	# phi acceleration
	a[1] = vector(0,0,-spring.Kphi*dphi/(spring.I+rod.I+nut.I))

	n = 0
	while n < len(nut):
		a[2+n] = a[0]
		n++

	return a
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# 維基百科 
# Miro Plavčić, Paško Županović and Željana Bonačić Lošić, //The resonance of the Wilberforce pendulum and the period of beats//, Lat. Am. J. Phys. Educ. ''Vol. 3, No. 3'', 547-549, 2009.
}}}
{{MOSTTitle{
IYPT 2022 第七題:三面骰
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對三面骰子問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
三面骰是在 [[IYPT 2022 的競賽題目|https://www.iypt.org/problems/problems-for-the-35th-iypt-2022/]]中的第七題,其敘述為:
>To land a coin on its side is often associated with the idea of a rare occurrence. What should be the physical and geometrical characteristics of a cylindrical dice so that it has the same probability to land on its side and one of its faces?
>要讓一個拋出的硬幣以側面落地,通常是不太可能發生的。如果要讓一個圓柱形骰子側面落地的機率和其它兩面是一樣的,它需要有什麼樣的物理和幾何特質?
}}}
{{MOSTSubParagraph{
這題的實作重點在於【xxx】。

在實作中可以發現討論的重點包含......
}}}
{{MOSTSubParagraph{
這篇文章對三面骰問題進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTParagraph{
投擲一個圓柱形骰子...
}}}
{{MOSTSubSection{
!!! 骰子的運動
}}}
{{MOSTSubParagraph{
當一個圓柱形骰子在空中自由落下時,......
}}}
{{MOSTSubSection{
!!! 運動方程
}}}
{{MOSTSubParagraph{
xx
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# [[Wikipedia Euler's equations (rigid body dynamics)|https://en.wikipedia.org/wiki/Euler%27s_equations_(rigid_body_dynamics)]]
# [[3D Rigid Body Dynamics: Euler Angles - MIT OpenCourseWare|https://ocw.mit.edu/courses/aeronautics-and-astronautics/16-07-dynamics-fall-2009/lecture-notes/MIT16_07F09_Lec29.pdf]]
# [[Berkeley: The Euler angle parametrization of rotation|https://rotations.berkeley.edu/the-euler-angle-parameterization/]]
# [[Wikipedia Angular velocity|https://en.wikipedia.org/wiki/Angular_velocity]]
# [[Kinematics: Single body|https://ethz.ch/content/dam/ethz/special-interest/mavt/robotics-n-intelligent-systems/rsl-dam/documents/RobotDynamics2016/KinematicsSingleBody.pdf]]
}}}
{{MOSTTitle{
IYPT 2022 第十四題:彈性膜上的球
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對彈性膜上一顆球體的運動問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
彈性膜上的球是在 [[IYPT 2022 的競賽題目|https://www.iypt.org/problems/problems-for-the-35th-iypt-2022/]]中的第十四題,其敘述為:
>When dropping a metal ball on a rubber membrane stretched over a plastic cup, a sound can be heard. Explain the origin of this sound and explore how its characteristics depend on relevant parameters.
>當一顆金屬球掉落在一個拉緊後包覆在塑膠杯口上的彈性薄膜上,會發出一個聲音。解釋這個聲音的來並探究此聲音的特質和相關參數之間的關係。
}}}
{{MOSTSubParagraph{
這題的實作重點在於【找出聲音的特質及影響此聲音的參數】。

在實作中可以發現討論的重點包含......
}}}
{{MOSTSubParagraph{
這篇文章對彈性膜上的球體運動進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[D3.js|https://d3js.org/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTParagraph{
彈性膜上的球
}}}
{{MOSTSubSection{
!!! 彈性膜的震動
}}}
{{MOSTSubParagraph{
當一顆金屬球落在繃緊的彈性膜上面時,會讓彈性膜產生震動,
}}}
{{MOSTSubSection{
!!! 運動方程
}}}
{{MOSTSubParagraph{
xx
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[D3.js|https://d3js.org/]]
# [[Dynamics of a ball bouncing on a vibrated membrane|https://hal.archives-ouvertes.fr/hal-00502329/document]]
}}}
{{MOSTTitle{
IYPT 2023 第二題:懸吊導電球的扭轉震盪
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對懸吊導電球體在磁場中的扭轉震盪問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
懸吊導電球的扭轉震盪是在 [[IYPT 2023 的競賽題目|https://www.iypt.org/problems/problems-for-the-36th-iypt-2023/]]中的第二題,其敘述為:
>A light sphere with a conducting surface is suspended from a thin wire. When the sphere is rotated about its vertical axis (thereby twisting the wire) and then released, it starts to oscillate. Investigate how the presence of a magnetic field affects the motion.
>一個夠輕的球(表面可導電),懸吊於一根細金屬線下,當此球對垂直軸有旋轉角度(也就扭轉金屬線),放開後它會開始震盪。探討外加磁場對這個運動的影響。
}}}
{{MOSTSubParagraph{
這題的觀念重點在於【磁場產生的阻尼效果】。

在實作中可以發現討論的重點包含【阻尼震盪】、【磁力】、【渦電流】等。
}}}
{{MOSTSubParagraph{
這篇文章對懸吊導電球的扭轉震盪問題進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^ [[1|https://threejs.org/]]^^,數據作圖則使用 [[Plotly.js|https://plotly.com/javascript/]]^^ [[2|https://plotly.com/javascript/]]^^,可以在一般瀏覽器中進行即時運算與繪圖。
}}}
{{MOSTSection{
!原理
}}}
{{MOSTParagraph{
這個過程中需要討論的物理包含了【扭轉震盪】與【摩擦造成的能量損耗】,以及【導體在磁場中運動產生渦電流】和【渦電流造成的能量損耗】等,以下分別討論其物理【觀念】以及【可計算模型】。
}}}
{{MOSTSubSection{
!!!扭轉震盪
}}}
{{MOSTParagraph{
當懸吊球體具有【偏轉角度】(相對於靜止平衡點)時,會扭轉其懸繩使之產生【回復力矩】,且可以很容易觀察到【偏轉角度越大,回復力矩越大】這樣的現象。因為有這樣的回復力矩,使球體在釋放後便具有回復的角加速度,進而開始往平衡點扭轉回去(偏轉角減小)。當偏轉角度越來越小,回復力矩也就越來越小,回到平衡點的那一刻,沒有偏轉角,也就沒有回復力矩與角加速度,但因為此時仍然具有角速度而繼續往另一個偏轉方向扭轉過去,之後開始產生另一個方向的回復力矩,並逐漸減慢角速度直到停止。停止時已經在另一方向具有偏轉角度,所以又開始往平衡點轉回去,如此周而復始地扭轉震盪。通常過程中運動能量會因為有損耗(可能是由於【懸吊線與固定點間的摩擦力】或【懸吊線本身內部結構因素】),使得震盪的幅度越來越小,直到能量全部損耗為止。
}}}
{{MOSTSubParagraph{
描述此運動的物理模型,根據【偏轉角度越大,回復力矩越大】這個現象,在【假設無耗損】的前提下,最簡單的形式便是【回復力矩''正比於''偏轉角度】,寫成數學式則為 \begin{equation}\tau_\text{回復} = -k_\phi\phi,\end{equation} 其中 \(\tau_\text{回復}\) 便是回復力矩,\(\phi\) 為偏轉角度,\(k_\phi\) 是比例常數,可稱為【懸吊繩的扭轉彈性係數】,負號表示力矩方向和偏轉角度相反,所以稱回復力矩。此模型和平移運度的虎克定律是完全相似的(其實應該說虎克定律也是這種最簡單的模型之一)。
}}}
{{MOSTSubSection{
!!!摩擦造成的能量損耗
}}}
{{MOSTParagraph{
摩擦造成的能量損耗,經常會和速率有關,且通常有【速率越大,能量損耗越大】這樣的現象。理論上若要計算其效果,我們得將損耗寫入運動方程(如上述 (1) 式)中,也就是要有一個對應的力(平移運動)或力矩(旋轉運動)才行。這題為旋轉運動,所以我們將能量損耗轉換成【某種損耗力矩所做的功】,如此便將【速率越大,能量損耗越大】轉換成【速率越大,損耗力矩越大】這樣的描述。比照上一段我們可以說損耗力矩最簡單的物理模型便是【損耗力矩''正比於''速率】,寫成數學式則為 \begin{equation}\tau_\text{摩擦損耗} = -\beta \dot\phi,\end{equation} 其中 \(\tau_\text{損耗}\) 便是損耗力矩,\(\dot\phi\) 為角速率(也稱角頻率),\(\beta\) 是比例常數,負號表示力矩方向和角速度相反,所以稱損耗力矩。此模型和平移運動中最簡單的空氣阻力模型是完全相似的。
}}}
{{MOSTSubParagraph{
將 (1)、(2) 兩式合併,便得到最簡單的,包含摩擦產生的損耗之扭轉震盪運動方程:\begin{equation}\tau = \tau_\text{回復} + \tau_\text{摩擦耗損} = -k_\phi\phi - \beta\dot\phi.\end{equation}
}}}
{{MOSTSubSection{
!!!導體在磁場中運動產生的渦電流
}}}
{{MOSTParagraph{
如果球體表面具有導電性,則球體旋轉時其表面的導電粒子會跟著球體一起旋轉,也就相當於在其表面有帶電粒子在繞著垂直軸做(近似)圓周運動(類似於地表的物體跟著地球自轉一起運動的情況)。此時若有外加磁場 \(\vec B_\text{ex}\) 存在,則這些帶電粒子除了那些將它們束縛在導體內部的作用力之外,還會受到磁力(勞倫茲力) \begin{equation}\vec F_{B} = q\vec v \times \vec B_\text{ex},\end{equation} 其中 \(q\) 是一個導電粒子所攜帶的電荷量,\(\vec v\) 為該粒子繞垂直軸做(近似)圓周運動的【線速度】,其大小正比於球體的角速率,方向則沿著旋轉的切線方向(球座標的 \(\hat\phi\)方向)。對一般導體而言導電粒子便是電子,每個電子攜帶的電荷量為 \(q = -e\)。
}}}
{{MOSTSubParagraph{
這個磁力 \(\vec F_{B}\) 的方向垂直於 \(\vec v\) 且垂直於 \(\vec B_\text{ex}\),我們曉得【垂直於速度的力會改變速度的方向】,所以不難理解這個力在這裡的效果是在導體表面產生渦電流(eddy current^^ [[3|https://en.wikipedia.org/wiki/Eddy_current]]^^)。此渦電流的存在會因為導體的電阻而逐漸消耗運動能量,也就是除了上述的摩擦損耗之外,這裡還會多一項損耗能量的因素,使得運動能量更快地耗散。
}}}
{{MOSTSubSection{
!!!渦電流造成的損耗
}}}
{{MOSTParagraph{
電流 \(i\) 流經導體所造成的能量損耗可以寫成 \begin{equation}P = i^2R,\end{equation} 其中 \(P\) 為【單位時間的能量損耗】,也稱功率,\(R\) 為導體中【電流經過的路徑之電阻】。如同前面【摩擦造成的能量損耗】一段中的討論,我們需要一個對應這個能量損耗的力矩來將之寫入運動方程中,以便計算其效果。通常導體的電阻 \(R\) 是由導體本身的材質決定的,一般情況下我們不預期它會因為導體的運動狀態而有所改變。至於電流 \(i\) 受運動狀態的影響為何?其最簡單模型是否可以同樣寫成【損耗力矩''正比於''速率】?我們可以從下面的討論很快地看出來。
}}}
{{MOSTSubParagraph{
在有磁場的環境下,符合歐姆定律的電流可寫成 \[\vec j = \sigma(\vec E_\text{ex} + \vec v \times \vec B_\text{ex}),\] 其中 \(\vec j\) 為電流密度(單位截面積的電流),\(\vec E_\text{ex}\) 為外加電場,\(\vec B_\text{ex}\) 為外加磁場,\(\sigma\) 稱為電導率,基本上由導體的材質決定,在簡單的情況下可視為常數。本題沒有外加電場,\(\vec E_\text{ex} = 0\),故 \begin{equation}\vec j = \sigma\vec v \times \vec B_\text{ex},\end{equation} 可以很容易看出''電流大小正比於速率'',套用到 (5) 式便是''渦電流造成的能量損耗__功率__正比於速率平方'',再如同前面所述,我們把它轉換成''【對應到渦電流的損耗力矩所做的__功率__】正比於速率的平方''。
}}}
{{MOSTSubParagraph{
我們已知【力矩 \(\vec \tau\) 作功的功率】為 \[P_\text{力矩} = \tau \dot\phi \propto \dot\phi^2,\] 所以可以直接說【對應渦電流的損耗力矩''正比於''速率】,寫成數學式就是 \[\tau_\text{渦電流損耗} = -\gamma\dot\phi.\] 併入 (3) 式我們就得到本題完整的,最簡單版本的運動方程,\begin{equation}I\ddot \phi = \tau = \tau_\text{回復} + \tau_\text{摩擦損耗} + \tau_\text{渦電流損耗} = -k_\phi\phi - \beta\dot\phi -\gamma\dot\phi = -k_\phi\phi - (\beta+\gamma)\dot\phi,\end{equation} 其中 \(I\) 為球體的轉動慣量。
>假設__均勻球體__的話,轉動慣量可以簡單地如下計算出來^^[[4|https://en.wikipedia.org/wiki/List_of_moments_of_inertia]]^^ \begin{equation}\begin{aligned}I &= {2 \over 5}mR^2, \qquad & \text{半徑為 }R\text{ 的實心球} \\ I &= {2 \over 5}m{R_2^5-R_1^5 \over R_2^3-R_1^3}, \qquad & \text{厚度為 }R_2 - R_1\text{ 的球殼} \\ I &= {2 \over 3}mR^2, \qquad & \text{半徑為 }R\text{ 的極薄球殼}.\end{aligned}\end{equation}
}}}
{{MOSTSubParagraph{
根據[[阻尼震盪]]裡的討論,運動方程 (7) 的解為 \begin{equation}\phi(t) = \phi_0 e^{-\lambda t} \cos(\omega t), \quad \lambda = {\beta+\gamma \over 2I}, \quad \omega = \sqrt{\omega_0^2-\lambda^2}, \quad \omega_0 = \sqrt{k_\phi \over I}.\end{equation}
}}}
----
{{MOSTParagraph{
@@font-size:2em;color:red;''底下尚未完成''@@
}}}
----
{{MOSTSubSection{
!!!! 球體的旋轉運動
}}}
{{MOSTSubParagraph{
對於球體表面位於天頂角 \(\theta\) 處(緯度 \(\pi - \theta\))的導電粒子而言,這個運動相當於這些導電粒子在做近似於具有切線速度 \begin{equation}\vec v = r_\theta\dot\phi\ \hat\phi = R\sin\theta\ \dot\phi\ \hat\phi\end{equation} 的圓周運動,其中 \(r_\theta = R\sin\theta\) 是球面在天頂角 \(\theta\) 處對應的圓半徑,而 \(\dot\phi = d\phi/dt\) 即旋轉運動的角速率。為了簡化計算,我們就假設它是這樣的圓周運動,以 (6) 式來描述這些導電粒子跟著球體旋轉的切線速度。
}}}
{{MOSTSubSection{
!!!! 外加磁場下導電粒子所受磁力
}}}
{{MOSTSubParagraph{
當有外來磁場 \(\vec B_\text{ex}\) 存在的時候,導電粒子受到的磁力應為,結合 (1) 式和 (6) 式,\begin{equation}\vec F_{B} = -e\dot\phi R\sin\theta\ \hat\phi \times \vec B_\text{ex}.\end{equation} 我們先討論簡單的情況:均勻的磁場。
}}}
{{MOSTSubSection{
!!!!! 均勻磁場的情況
}}}
{{MOSTSubParagraph{
假如磁場是均勻的,且其方向為 \(\vec B_\text{ex} = B_x\ \hat x + B_y\ \hat y + B_z\ \hat z\),此時 (7) 式會變成 \begin{align}\vec F_{B} &= -e\dot\phi R\sin\theta\ \hat\phi \times \vec B_\text{ex} \nonumber\\ &= -e\dot\phi R\sin\theta\ (B_x\ \hat\phi \times \hat x + B_y\ \hat\phi \times \hat y + B_z\ \hat\phi \times \hat z) \nonumber\\ &= -e\dot\phi R\sin\theta\ (B_x\sin(\pi/2+\phi)\ (\mp \hat z) + B_y\sin(\pi-\phi)\ (\pm \hat z) + B_z\ \hat \rho) \nonumber\\ &= -e\dot\phi R\sin\theta\ (\mp B_x\cos\phi\ \hat z \pm B_y\sin\phi\ \hat z + B_z\ \hat \rho) \nonumber\\ &= -e\dot\phi R\sin\theta\ [(\mp B_x \cos\phi \pm B_y \sin\phi)\ \hat z + B_z\ \hat\rho].\end{align}
}}}
{{FrameHalfRight{
|multilined|k
|<<tw3DCommonPanel "Spherical System Control">> / <<tw3DCommonPanel "Cylindrical System Control">>|
| <<tw3DScene [[Oscillating Sphere Fig 1]]>> |
|圖一、球面上位於天頂角 \(\theta\) 處(圖中紅點)圓柱座標的 \(\hat z\) 及 \(\hat\rho\) 和球座標的 \(\hat r\) 與 \(\hat\theta\) 等向量之間的關係。從圖中的幾何關係可以直接看出 \(\hat z\) 和 \(\hat\rho\) 可以分別寫成 \begin{aligned}\hat z = \cos\theta\ \hat r - \sin\theta\ \hat\theta, \\ \hat\rho = \sin\theta\ \hat r + \cos\theta\ \hat\theta.\end{aligned}|
}}} {{MOSTSubParagraph{
從 (8) 式中我們可以看到磁場的 \(x\) 及 \(y\) 分量產生的磁力是沿著 \(\hat z\) 軸方向,而磁場 \(z\) 分量產生的磁力則沿著圓柱座標的半徑方向 \(\hat\rho\)。由圖一可以看出,在__球面上__天頂角為 \(\theta\) 處 \(\hat z\) 和 \(\hat\rho\) 向量都可以分解為 \(\hat r\) 及 \(\hat\theta\) 的線性組合: \begin{aligned}\hat z = \cos\theta\ \hat r - \sin\theta\ \hat\theta, \\ \hat\rho = \sin\theta\ \hat r + \cos\theta\ \hat\theta.\end{aligned} 也就是磁力 \(\vec F_{B}\) 可以寫成 \[\vec F_{B} = -e\dot\phi R\sin\theta[(\mp B_x \cos\phi \pm B_y \sin\phi)(\cos\theta\ \hat r - \sin\theta\ \hat\theta) + B_z(\sin\theta\ \hat r + \cos\theta\ \hat\theta)].\]
}}}
{{MOSTSubParagraph{
''如果我們讓導電部分僅有表面薄薄一層,也就是導電層厚度 \(h\) 遠小於球體半徑(\(h \ll R\)),那麼 \(\hat r\) 分量應無法產生明顯的渦電流,此時我們僅需要討論 \(\hat\theta\) 分量即可: \begin{equation}\vec F_{B,\theta} = -e\dot\phi R\sin\theta\ [(\mp B_x \cos\phi \pm B_y \sin\phi)(-\sin\theta) + B_z\cos\theta]\ \hat\theta.\end{equation} ''
}}}
{{MOSTSubSection{
!!!!! 均勻磁場下的渦電流
}}}
{{MOSTSubParagraph{
一般情況下歐姆定律^^[[5|https://en.wikipedia.org/wiki/Ohm%27s_law]]^^可寫成 \[\vec j = \sigma(\vec E_\text{ex} + \vec v \times \vec B_\text{ex}),\] 其中 \(\vec j\) 為電流密度,\(\vec E_\text{ex}\) 為外加電場,\(\vec B_\text{ex}\) 為外加磁場,\(\sigma\) 稱為電導率,和導體的材質有關。在這個例子裡
# 我們【沒有】外加電場(\(\vec E_\text{ex} = 0\)),只有外加磁場(\(\vec B_\text{ex} \ne 0\));
# 根據上述討論 \(\vec v \times \vec B_\text{ex} = \vec F_{B} / (-e)\) 僅需討論其 \(\hat\theta\) 分量即可。
}}}
{{MOSTSubParagraph{
也就是說,在我們的簡化條件下渦電流為 \begin{equation}\boxed{\vec j_\theta = \sigma \dot\phi R\sin\theta\ [(\mp B_x \cos\phi \pm B_y \sin\phi)(-\sin\theta) + B_z\cos\theta]\ \hat\theta.}\end{equation} }}}
{{MOSTSubParagraph{
以上討論觀念上和文獻 [[6|https://doi.org/10.1119/1.4936633]] 是一致的,只是代數過程稍有不同。}}}
{{MOSTSubSection{
!!!!!不均勻磁場的情況(不隨時間而變)
}}}
{{MOSTSubParagraph{
......
}}}
{{MOSTSubSection{
!!!! 隨時間變動的磁場
}}}
{{MOSTSubParagraph{
時間足夠再來討論吧......
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[Plotly.js|https://plotly.com/javascript/]]
# [[Wikipedia: Eddy current|https://en.wikipedia.org/wiki/Eddy_current]]
# [[Wikipedia: List of moments of inertia|https://en.wikipedia.org/wiki/List_of_moments_of_inertia]]
# [[Wikipedia: Ohm's law|https://en.wikipedia.org/wiki/Ohm%27s_law]]
# Robert C. Youngquist, Mark A. Nurge, Stanley O. Starr, et al., //American Journal of Physics// ''84'', 181 (2016); [[doi: 10.1119/1.4936633|https://doi.org/10.1119/1.4936633]]
}}}
{{MOSTTitle{
IYPT 2023 第六題:磁彈簧震盪
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對磁彈簧震盪問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
磁彈簧震盪是在 [[IYPT 2023 的競賽題目|https://www.iypt.org/problems/problems-for-the-36th-iypt-2023/]]中的第六題,其敘述為:
>Secure the lower ends of two identical leaf springs to a non-magnetic base and attach magnets to the upper ends such that they repel and are free to move. Investigate how the movement of the springs depends on relevant parameters.
>將兩片板狀彈簧底部固定在一個非磁性的基座上,並於其上方端點附近黏貼磁鐵使其磁性互斥,但仍可以產生震盪運動。探討相關參數對此運動的影響。
}}}
{{MOSTSubParagraph{
這題的重點在於【耦合震盪】與【阻尼震盪】。

在實作過程中可以發現討論的重點包含【耦合項的特性】以及【耦合震盪的 normal modes】。
}}}
{{MOSTSubParagraph{
這篇文章對兩個附加磁鐵的板彈簧之間的耦合震盪進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[Plotly.js|https://plotly.com/javascript/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTParagraph{
板彈簧震盪...
}}}
{{MOSTSubSection{
!!!板彈簧震盪
}}}
{{MOSTSubParagraph{
板彈簧的震盪,在【振幅不是特別大】的情況下,可以簡化地用虎克定律來描述 \begin{equation}F_\text{回復} = -k\Delta x,\end{equation} 其中 \(F_\text{回復}\) 為板彈簧彎曲之後的回復力,\(k\) 為回復係數,\(\Delta x\) 為彎曲之後端點的水平位移。
}}}
{{MOSTSubSection{
!!!兩個磁場間的交互作用
}}}
{{MOSTSubParagraph{
根據文獻[?],在【磁鐵距離遠大於磁鐵大小】的情況下,兩磁鐵間的交互作用力可以簡化成 \begin{equation}F_\text{磁鐵} = {\kappa \over (x_1-x_2)^4},\end{equation} 其中 \(\kappa\) 為交互作用常數,\(x_1\)、\(x_2\) 分別為兩個磁鐵的位置(水平)。
}}}
{{MOSTSubSection{
!!!運動方程
}}}
{{MOSTSubParagraph{
綜合上述討論,若再加上最簡單的摩擦損耗模型(參考[[阻尼震盪]]中的討論),本題的運動方程可寫為 \begin{align}m \ddot x_1 = -kx_1 -\beta\dot x_1 + {\kappa \over (x_1-x_2)^4}, \\ m \ddot x_2 = -kx_2 - \beta\dot x_2 - {\kappa \over (x_1 - x_2)^4},\end{align} 其中 \(\beta\dot x_1\) 及 \(\beta\dot x_2\) 是磨擦造成的損耗。這裡很容易看出 \(x_1\) 的方程裡面有 \(x_2\),反之亦然,這種情況稱為【耦合】。當兩物體的運動出現耦合的時候,對於數值計算影響不大,照著算就是了;但是對於手動計算而言,不把耦合去除是無法進行的。耦合的去除,一般常見的做法就是【normal modes 轉換】,其結果是把運動方程改寫成只有 normal mode 座標的方程,這樣可以保證不會出現耦合。
}}}
{{MOSTSubParagraph{
Normal modes 轉換的__標準程序__,首先是以 normal mode 座標 \begin{align}X_1 = {x_1 + x_2 \over 2}, \\ X_2 = {x_1 - x_2 \over 2}\end{align} 來重寫原來的座標 \(x_1\) 與 \(x_2\)。可以容易看出 \begin{align}x_1 = X_1 + X_2, \\ x_2 = X_1 - X_2.\end{align}
<<<
這兩個 normal mode 座標的意義:
#\(X_1\) 代表【兩物體的中心點】,而中心點的震盪就對應到【兩物體同相震盪】的情形;
#\(X_2\) 代表【兩物體的相對距離】,而相對距離的震盪對應到【兩物體反相震盪】的情形。
<<<
}}}
{{MOSTSubParagraph{
然後將 (7)、(8) 式代回運動方程 (3)、(4) 式,可得到 \begin{align}m(\ddot X_1 + \ddot X_2) = -k(X_1 + X_2) -\beta(\dot X_1 + \dot X_2) + {\kappa \over (2X_2)^4}, \\ m(\ddot X_1 - \ddot X_2) = -k(X_1 - X_2) -\beta(\dot X_1 - \dot X_2) - {\kappa \over (2X_2)^4}\end{align}
}}}
{{MOSTSubParagraph{
接著將 (9)、(10) 做加減處裡便可得到只有 normal mode 座標 \(X_1\)(或 \(X_2\)),而沒有耦合的方程。\(X_1\) 的方程可由 \((9)+(10)\) 得到:\begin{equation}m\ddot X_1 = -kX_1 - \beta\dot X_1,\end{equation} 而 \(X_2\) 的方程則可由 \((9)-(10)\) 得到:\begin{equation}m\ddot X_2 = -kX_2 -\beta\dot X_2 + {\kappa \over (2X_2)^4}.\end{equation} 這裡可以肯定 \(X_1\) 的方程裡【沒有】 \(X_2\),反之亦然,也就是【沒有耦合】。
}}}
{{MOSTSubParagraph{
我們可以很快看出 (11) 式就是個標準的阻尼震盪方程,直接把[[阻尼震盪]]的(次阻尼)結果 \begin{equation}X_1 = X_{1,0}\ e^{-\lambda t}\cos(\omega t), \quad \lambda = {\beta \over 2m}, \quad \omega = \sqrt{\omega_0^2-\lambda^2}, \quad \omega_0 = \sqrt{k \over m}\end{equation} 拿來用即可。而 __(12) 式除了阻尼震盪之外還有額外由原來的耦合項產生的一項,''這一項讓解析過程變得困難'',因此@@建議直接用數值方法求解@@比較容易__。
}}}
{{MOSTSubParagraph{
這兩個 normal mode 座標的意義分別為:
#\(X_1\) 是兩個板彈簧端點的【中心點】,也就是 (11) 式是在描述【中心點的震盪】,這種模式下兩個端點會呈現【同相震盪】的情形;
#\(X_2\) 是兩個板彈簧端點的【相對距離】,也就是 (12) 式是在描述【相對距離的震盪】,這種模式下兩個端點會呈現【反向震盪】的情形。
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[Plotly.js|https://plotly.com/javascript/]]
#
}}}
{{MOSTTitle{
IYPT 2023 第八題:歐拉擺(磁懸擺)
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對歐拉擺(磁懸擺)問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
歐拉擺(磁懸擺)是在 [[IYPT 2023 的競賽題目|https://www.iypt.org/problems/problems-for-the-36th-iypt-2023/]]中的第八題,其敘述為:
>Take a thick plate of non-magnetic ma­terial and fix a neodymium magnet on top of it. Suspend a magnetic rod (which can be assembled from cylindrical neo­dy­mium magnets) underneath it. Deflect the rod so that it touches the plate only with highest edge and release it. Study the motion of such a pendulum under various conditions.
>找一個厚度足夠的非磁性板狀物體(上板),將一個強力磁鐵固定在它的一面(上面),在另一面(下面)懸一個棒狀磁鐵(可以用圓柱狀強力磁鐵組成)。讓磁棒傾斜使其僅有一個點接觸上板然後放開任期運動。探討這個運動在不同條件下的結果。
}}}
{{MOSTSubParagraph{
這題的實作重點在於【xxx】。

在實作中可以發現討論的重點包含......
}}}
{{MOSTSubParagraph{
這篇文章對三面骰問題進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[Plotly.js|https://plotly.com/javascript/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTParagraph{
球體旋轉...
}}}
{{MOSTSubSection{
!!! 骰子的運動
}}}
{{MOSTSubParagraph{
磁場存在,......
}}}
{{MOSTSubSection{
!!! 運動方程
}}}
{{MOSTSubParagraph{
xx
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[Plotly.js|https://plotly.com/javascript/]]
#
}}}
{{MOSTTitle{
IYPT 2025 第三題:拉托球
(撰寫中)
}}}
{{MOSTAuthor{
葉旺奇^^[1]^^,......,曾賢德^^[1]^^
}}}{{MOSTAffiliation{
# [[國立東華大學物理學系|https://phys.ndhu.edu.tw/]]
}}}{{MOSTAddress{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{MOSTAbstract{
此篇文章利用一般瀏覽器都可執行的 Javascript 語言對拉托球問題進行定量模擬計算,並與實驗結果互相比較。
}}}
{{MOSTSection{
! 前言
}}}
{{MOSTParagraph{
拉托球是在 [[IYPT 2025 的競賽題目|https://www.iypt.org/problems/problems/]]中的第三題,其敘述為:
>Lato Lato
>拉托球
}}}
{{MOSTSubParagraph{
這題的實作重點在於【xxx】。

在實作中可以發現討論的重點包含......
}}}
{{MOSTSubParagraph{
這篇文章對三面骰問題進行定量模擬,......,使用程式語言為 Javascript,3D 繪圖引擎為 [[three.js|https://threejs.org/]]^^[1]^^,數據作圖則使用 [[Plotly.js|https://plotly.com/javascript/]]^^[2]^^,可以在一般瀏覽器中進行即時運算。
}}}
{{MOSTSection{
! 原理
}}}
{{MOSTParagraph{
單擺、球體碰撞...
}}}
{{MOSTSubSection{
!!!球體碰撞
}}}
{{MOSTSubParagraph{
Hertzian 模型,......
}}}
{{MOSTSubSection{
!!! 運動方程
}}}
{{MOSTSubParagraph{
xx
}}}
{{MOSTSection{
! 實驗
}}}
{{MOSTParagraph{
實驗器材、實驗方式或步驟
}}}
{{MOSTSection{
! 結果與討論
}}}
{{MOSTParagraph{
x
}}}
{{MOSTSection{
! 結論
}}}
{{MOSTParagraph{
xx
}}}
{{MOSTSection{
! 參考文獻
}}}
{{MOSTSubParagraph{
# [[three.js|https://threejs.org/]]
# [[Plotly.js|https://plotly.com/javascript/]]
#
}}}
以數值方法來模擬物理上的 3D 力學運動,基本上只要照著下面幾個步驟進行就可以:
# 設定場景
# 設定初始條件
# 解運動方程
!!! 設定場景
如同拍攝電影一般:場地、攝影機、燈光、道具、配角、主角
場地、攝影機、燈光已經自動產生,需要添加道具、配角、主角
3D 物體:球、方塊、圓柱、箭頭、螺旋、平面、多邊形等
!!! 設定初始條件
接近真實的初始條件
搜尋接近真實,或是合理的運動條件
!!! 解運動方程
分析受力,寫下向量式,翻譯成程式語言
例如:
!!!! 拋體運動
受力為重力+空氣阻力:\[\vec F = m\vec g - {1 \over 2}\rho C_{D}Av^2 \hat v,\] 翻譯成 Python 語言的話就是
{{{
F = m * g - 0.5 * rho * CD * A * v.mag2 * v.norm()
}}}
其中 {{{g}}} 和 {{{v}}} 都是向量。
!!!! 單擺運動
平面單擺的受力為 \[\vec F = m\vec g + m{v^2 \over r}(-\hat r),\] 翻譯成 Python 語言就成為
{{{
F = m * g - m * v.mag2 / r.mag * r.norm()
}}}
其中 {{{g}}} 和 {{{v}}} 都是向量。
!!! 解運動方程:牛頓運動定律
一旦物體的受力可以寫下,物體的加速度便可以計算出來,也就可以對牛頓運動方程 \[f = ma = m{d^2x \over dt^2} \quad \text{(1D)}, \quad or \quad \vec f = m\vec a = m{d^2\vec r \over dt^2} \quad \text{(3D)}\] 進行求解。一般請況可參考[[微分方程的數值求解]]。
/***
!! Calculate tension
***/
//{{{
let Tension = (r,v,T) => {
	/***
	Calculate the first order tension of the rod at the current bob
	position, which would be the the gravitational component plus
	the elastic force along the rod.
	***/
	if(!T) T = vector();
	T.copy(r[1]).sub(r[0]);
	let dl = T.length() - rod.L0;	// current length - original length
	let Telastic = dl > 0 ? -rod.k * dl : 0;	// current elastic force
	return T.normalize().multiplyScalar(Telastic);
}
//}}}
/***
!! Calculate accelerations
***/
//{{{
let calculateA = (r,v,t,a) => {
	// calculate the current tension
	let T = Tension(r,v);		// tension in string
	rod.arrT.setAxis(tmpV.copy(T).multiplyScalar(0.1));
	let Fp = vector();		// force from the springs attached to the pivot
	let fn = vector();
	for(let n=0,N=pivot.spring.length; n<N; n++){
		fn.copy(r[0]).sub(pivot.spring[n].position);
		let L = fn.length();
		pivot.arrF[n].setAxis(
			tmpV.copy(
				fn.normalize().multiplyScalar(
					-pivot.spring[n].k*(L-pivot.spring[n].L0)
				)
			).multiplyScalar(0.1)
		);
		Fp.add(fn);
	}
	// calculate the current acceleration of the pivot and the bob
	if(!a) a = [];
	a[0] = Fp.sub(T).multiplyScalar(1/pivot.mass);	// a[0] is the acc of pivot
	if(chkAirDrag.checked){
		a[1] = T.add(bob.Fg).add($tw.physics.airDragSphere(v[1],bob.getRadius()))
				.multiplyScalar(1/bob.mass)	// a[1] is the acc of bob
	}else{
		a[1] = T.add(bob.Fg).multiplyScalar(1/bob.mass);	// a[1] is the acc of bob
	}
	return a;
}
//}}}
/***
!!!! oscillationType(rod,bob)
<<<
* The oscillation type of a pendulum can be one of the followings:
## ''planar'': the simplest case that all the textbook would talk about
## ''conical'': another simple case that most of the textbook would talk about
## ''vertical'': more like a vertical spring oscillation
## ''general'': all other cases
* To test whether a pendulum motion is planar, we check if the following three vectors are coplanar:
## \(\vec r,\) the bob's position vector from the pivot;
## \(\hat z,\) the vertical axis;
## \(\vec v,\) the bob's velocity vector.
* To test whether it's conical, we check its speed with the expected conical speed.
* To test whether it's vertical, we check if both \(v_x\) and \(v_y\) are 0.
<<<
***/
//{{{
/***
let oscillationType = (rod,bob) => {
	if(Math.abs(bob.velocity.y) < $tw.physics.__epsilon__ &&
		Math.abs(bob.velocity.x) < $tw.physics.__epsilon__){
		return rod.k > 0 ? 'vertical' : 'none';
	}
	if(bob.velocity.length())
			if(rod.k === 0)
				return 'planar';
			else
				return 'planar-complex';
		}
	}else{
		// non-planar pendulum
		let speed = bob.velocity.length();
		let conicalv = conicalSpeed(rod.L0,rod.A0);
		return (Math.abs(speed-conicalv) < __algoerr__)
			? 'conical' : 'non-planar';
	}
}
***/
//}}}
//{{{
const radialPeriod = (rod,bob) => {
	return 2.0*Math.PI*Math.sqrt(bob.mass/rod.k);
},
//}}}
//{{{
swingPeriod = (rod,bob) => {
	return 2.0*Math.PI*Math.sqrt(rod.L0/9.8)*
		(1.0+1.0/16.0*rod.A0*rod.A0+1.0/3072.0*Math.pow(rod.A0,4));
},
//}}}
//{{{
theoreticalPeriod = (rod,bob,type) => {
	if(!type) type = oscillationType(rod,bob);
	if(type === 'vertical')
		return radialPeriod(rod,bob);
	else if(type === 'planar')
		return swingPeriod(rod,bob);
	else if(type === 'conical')
		return 2.0*Math.PI*Math.sqrt(rod.L0*Math.cos(rod.A0)/9.8);
	//else if(type === 'planar-complex')
	//	return swingPeriod(rod,bob)
		//return {
		//	'swing': swingPeriod(rod,bob),
		//	'radial': radialPeriod(rod,bob)
		//}
	//else if(type === 'non-planar')
	//	return swingPeriod(rod,bob);
	else
		// Don't know what to do...
		return 0;
},
//}}}
//{{{
determinePeriod = (obj,t_now,r0,type) => {
	/***
	Determine the period of a moving object by finding the turning
	point. The turning point is determined by a sign change in the
	radial compoent of the velocity.
	***/

	if(!type) type = oscillationType(rod,bob);
	let T = 0;
	let rnorm = obj.position.clone().sub(r0).normalize();
	let turning = false;
	if(obj.v_last){
		if(type === 'vertical')
			turning = (obj.v_last.y*obj.velocity.y < 0);
		else if(type === 'planar' || type === 'conical')
			turning = (obj.v_last.x*obj.velocity.x < 0);
		if(turning){
			T = (t - obj.t_last)*2;
			obj.t_last = t_now;
		}
	}else{
		obj.v_last = vector();
		obj.t_last = t_now;
	}
	obj.v_last.copy(obj.velocity);

	return T;
}
//}}}
//{{{
let __rate__ = 2500;

//let normal_range = scene.range / 1.8;
//let normal_center = vector(scene.center);
//let zoom_in_range = scene.range / 15;

//x_t = gcurve(color=color.magenta);
//y_t = gcurve(color=color.green);
//z_t = gcurve(color=color.cyan);

//Eplot = gdisplay();
// kinetic energy
//KE_t = [
//	gcurve(display=Eplot.display,color=color.magenta),
//	gcurve(display=Eplot.display,color=color.magenta)
//];
// gravitational potential energy
//UE_t = gcurve(display=Eplot.display,color=color.green);
// elastic potential energy
//Uel_t = gcurve(display=Eplot.display,color=color.cyan);
// total energy
//TotE_t = gcurve(display=Eplot.display);
//}}}
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'Theta (&deg;)' : '\\(\\theta\\ (^\\circ)\\)')
).setTitle('Theta vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'Phi (&deg;)' : '\\(\\phi\\ (^\\circ)\\)')
).setTitle('Phi vs Time');

labelPlot[0].innerHTML = '\\(\\theta\\) (&deg;):';
labelPlot[1].innerHTML = '\\(\\phi\\) (&deg;):';
//}}}
/***
!! Status checking function
***/
//{{{
let arrv = arrow({
	color: 0x00ff00
});
let arra = arrow({
	color: 0xff00ff
});
scene.checkStatus = () => {
	getTrailParam(bob);
	let showspring = typeof chkPivotSpring !== 'undefined',
		showK = typeof chkPivotSpringK !== 'undefined',
		showforce = chkShowForce !== 'undefined';
	for(let n=0,N=pivot.spring.length; n<N; n++){
		if(showspring){
			pivot.spring[n].visible = chkPivotSpring.checked;
		}
		if(showK){
			pivot.spring[n].label.show(chkPivotSpringK.checked);
		}
		if(showforce){
			pivot.arrF[n].visible = chkShowForce.checked;
		}
	}
	if(showforce) rod.arrT.visible = chkShowForce.checked;
	CM.visible = chkCM.checked;
	arrv.visible = chkShowVelocity.checked;
	arra.visible = chkShowAcceleration.checked;

	let k_now = checkKpivot();
	if(pivot.spring[2].k !== k_now){
		//pivot.spring[2].k = k_now;
		pivot.spring[2].label.setText('k='+k_now,2);
	}
}
//}}}
/***
!! Update function
***/
//{{{
scene.update = (t_cur,dt) => {
//}}}
/***
!!!! Check status change
***/
//{{{
	/*
	let k_now = checkKpivot();
	if(pivot.spring[2].k !== k_now){
		//pivot.spring[2].k = k_now;
		pivot.spring[2].label.setText('k='+k_now,2);
	}
	*/
//}}}
/***
!!!! next positions and velocities
***/
//{{{
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.numeric.ODE.nextValue(
			__r,__v,calculateA,t_cur,dt,__a,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	rod.position.copy(pivot.position.copy(__r[0]));
	for(let n=0,N=pivot.spring.length; n<N; n++){
		pivot.arrF[n].position.copy(pivot.position);
	}
	pivot.velocity.copy(__v[0]);
	pivot.acceleration.copy(__a[0]);
	bob.position.copy(__r[1]);
	bob.velocity.copy(__v[1]);
	bob.acceleration.copy(__a[1]);
	arrv.position.copy(bob.position);
	arrv.setAxis(tmpV.copy(bob.velocity).multiplyScalar(0.1));
	arra.position.copy(bob.position);
	arra.setAxis(tmpV.copy(bob.acceleration).multiplyScalar(0.1));
	rod.setAxis(
		tmpV.copy(bob.position).sub(pivot.position)
	);
	rod.arrT.position.copy(bob.position);
	//scene.camera.lookAt(bob.position);

	//scene.center = bob.position
//}}}
/***
!!!! data recording
***/
//{{{
	//saveSimulationData(scene.currentTime(),__r,__v,__a,'AR Pendulum Data');

	dataPlot[0].addXPoint(scene.currentTime());
	let domain = dataPlot[0].xscale.domain();
	if(scene.currentTime()> domain[1]){
		let t0 = dataPlot[0].getXPoint(0);
		let tnext = dataPlot[0].getXPoint();
		if(tnext){
			t0 = Math.min(t0,tnext);
		}
		domain[0] = t0;
		domain[1] = scene.currentTime()+scene.timeInterval()*100;
		dataPlot[0].rescaleX(domain);
		dataPlot[1].rescaleX(domain);
	}

	let theta = 180*(1-$tw.threeD.angleFromZ(tmpV)/Math.PI);
	let phi = 180/Math.PI*$tw.threeD.angleFromX(tmpV);
	if(phi>180) theta=-theta;	
	dataPlot[0].addYPoint(theta);
	domain = dataPlot[0].yscale.domain();
	if(theta<domain[0] || theta>domain[1])
		dataPlot[0].rescaleY([
			dataPlot[0].getDataMin(),dataPlot[0].getDataMax()
		]);

	dataPlot[1].addYPoint(phi);
	domain = dataPlot[1].yscale.domain();
	if(phi<domain[0] || phi>domain[1])
		dataPlot[1].rescaleY([
			dataPlot[1].getDataMin(),dataPlot[1].getDataMax()
		]);
	let almost_done = (scene.maxTime()>0 && (scene.maxTime()-scene.currentTime())<=scene.timeInterval());
	if(almost_done || chkPlot0.checked)
		dataPlot[0].refresh();
	if(almost_done || chkPlot1.checked)
		dataPlot[1].refresh();
//}}}
/***
!!!! check periods
***/
//{{{
	/*
	if(!__type){
		__type = oscillationType(rod,bob);
		return;
	}
	let T_this = determinePeriod(bob,t,pivot.position,__type);
	if(T_this){
		cycles += 1;
		T[T.length] = T_this;
		let N = T.length;
		let Tavg = T.reduce(sum,t => sum+t)/N;
		let Ttheory = theoreticalPeriod(rod,bob,__type);
		//if(isinstance(__Ttheory,dict))
		//	Ttheory = __Ttheory['swing'];
		//else
		//	Ttheory = __Ttheory;
		if(Ttheory > 0)
			print "Type: {} L:{} A0:{} deg T({}):{}, Tavg:{}. Ttheory:{} ({}%)".format(
				__type,rod.axis.mag,rod.A0/pi*180,N,T_this,Tavg,
				Ttheory,round((Tavg-Ttheory)/Ttheory*100,3)
			);
		else
			print "Type: {} L:{} A0:{} deg T({}):{}, Tavg:{}.".format(
				__type,rod.axis.mag,rod.A0/pi*180,N,T_this,Tavg
			);
	}
	*/
//}}}
/***
!!!! calculate energies
***/
//{{{
	let h = bob.position.z-pivot.position.z;

	bob.KE = 0.5*bob.mass*bob.velocity.lengthSq();
	pivot.KE = 0.5*pivot.mass*pivot.velocity.lengthSq();
	let KE = bob.KE + pivot.KE;
	bob.UE = bob.mass*9.8*h;
	let UE = bob.UE;
	rod.Uel = 0;
	pivot.Uel = 0;
	for(let n=0,N=pivot.spring.length; n<N; n++){
		pivot.spring[n].setAxis(
			tmpV.copy(pivot.position).sub(pivot.spring[n].position)
		);
		pivot.Uel += 0.5*pivot.spring[n].k*Math.pow(
			tmpV.length()-pivot.spring[n].L0,2
		);
	}
	let Uel = rod.Uel + pivot.Uel;
//}}}
/***
!!!! update status
***/
//{{{
	calculateCM();
}
//}}}
//{{{
//scene.range = normal_range
//scene.center = normal_center
// scene.range = zoom_in_range
// scene.center = bob.position
//scene.forward = vector(0,-0.5,-1)
//}}}
//{{{
// 假設木頭棒子,楊氏係數約為 11 GPa(參考 https://en.wikipedia.org/wiki/Young%27s_modulus)
// radius = 0.005 (diameter 1 cm)
let __kmax__ = 1.1e10*(Math.PI*Math.pow(0.005,2))
console.log('__kamx__='+$tw.ve.round(__kmax__,3));
__trail_len__ = +txtTrailLength.value;
__trail_interval__ = +txtTrailInterval.value;
let arrsw = 0.04

let txtPivotMass = document.getElementById("txtPivotMass"),
	txtRodMass = document.getElementById("txtRodMass"),
	txtBobMass = document.getElementById("txtBobMass"),
	txtRodLength = document.getElementById("txtRodLength"),
	txtTheta0 = document.getElementById("txtTheta0"),
	txtPhi0 = document.getElementById("txtPhi0");
txtTmax.value = 50;
txtTrailInterval.value = 20;
chkMovable.checked = true;
txtTolerance.value = '1e-3';
//}}}
//{{{
// Arrange the scene
let pivot = sphere({		// create a sphere to represent the pivot
	radius:0.01,
	opacity:0.2
});
pivot.r0 = pivot.position.clone();
pivot.velocity = vector();
pivot.acceleration = vector();
pivot.springL = 0.15;
pivot.spring = [
	helix({
		pos:vector(1,0,0).multiplyScalar(pivot.springL).add(pivot.position),
		axis:vector(-1,0,0).multiplyScalar(pivot.springL),
		radius:0.01,
		thickness:0.005,
		color:0xFED162,
		//opacity:0.3,
		coils:15
	}),
	helix({
		//pos:vector(1,0,1).normalize().multiplyScalar(pivot.springL).add(pivot.position),
		//axis:vector(-1,0,-1).normalize().multiplyScalar(pivot.springL),
		pos:vector(0,0,1).normalize().multiplyScalar(pivot.springL).add(pivot.position),
		axis:vector(0,0,-1).normalize().multiplyScalar(pivot.springL),
		radius:0.01,
		thickness:0.005,
		color:0xFED162,
		//opacity:0.3,
		coils:15
	}),
	helix({
		pos:vector(0,1,0).multiplyScalar(pivot.springL*2).add(pivot.position),
		axis:vector(0,-1,0).multiplyScalar(pivot.springL*2),
		radius:0.02,
		thickness:0.005,
		color:0xFED162,
		//opacity:0.3,
		coils:15
	})
];
pivot.spring[0].k = __kmax__;
pivot.spring[1].k = __kmax__;
let checkKpivot = function(){
	return (pivot.spring[2].k = chkMovable.checked ? +txtKpivot.value : __kmax__);
}
pivot.arrF = [
	arrow({shaftwidth:arrsw,color:0xffff00}),
	arrow({shaftwidth:arrsw,ycolor:0xffff00}),
	arrow({shaftwidth:arrsw,color:0xffff00})
];
for(let n=0,N=pivot.spring.length; n<N; n++){
	pivot.spring[n].L0 = pivot.spring[n].axis.length();
	//pivot.spring[n].thickness=
	//	pivot.spring[n].radius*pivot.spring[n].k/__kmax__;
	//if(pivot.spring[n].thickness < 1e-3){
	//	pivot.spring[n].thickness = 1e-3;
	//}
	pivot.spring[n].label = label({
		text:('k='+$tw.ve.round(pivot.spring[n].k,2)),
		color:0xffffff,size:'12pt',opacity:0.3,
		pos:pivot.spring[n].position.clone().add(pivot.spring[n].getAxis().multiplyScalar(0.5)),
		height:0.02,depth:0.002,axis:pivot.spring[n].getAxis().multiplyScalar(-1)
	});
	pivot.spring[n].label.position.y += 0.02;
	pivot.arrF[n].visible = false;
}
//}}}
//{{{
/*
let rod = cylinder({				// create a rod
	pos: pivot.position,			// at the pivot's position,
	axis: vector(0,0,-txtRodLength.value),
	radius: 0.002,
	opacity: 0.5
});
rod.preSetAxis = rod.setAxis;
rod.setAxis = axis => {
	rod.position.add(
		axis.clone().multiplyScalar(0.5).add(pivot.position).sub(rod.position)
	);
	return rod.preSetAxis.call(this,axis);
};
rod.position.z -= rod.L0/2;
*/
let rod = helix({
	pos:pivot.position,
	axis:vector(0,0,-1),
	radius:0.001,
	thickness:0.001,
	color:0xFED162,
	//opacity:0.3,
	coils:60
});
//rod.arrL = arrow({
//	shaftwidth: arrsw,
//	axis: vector(0,0,-1)
//});
rod.k = __kmax__;
rod.arrT = arrow({shaftwidth:arrsw,color:0xffff00});
rod.arrT.visible = false;
//}}}
//{{{
let bob = sphere({					// create a bob that is
	pos: vector(),
	radius: 0.025,
	make_trail: false,
	retain: __trail_len__,
	interval: __trail_interval__,
	opacity: 0.5
});
bob.Fg = vector();
bob.velocity = vector();
bob.acceleration = vector();
//}}}
//{{{
let conicalSpeed = (L,A) => {
	return Math.sqrt(L*9.8*Math.sin(A)*Math.tan(A));
}
//}}}
//{{{
let tmpV = vector();
let TotM = 0, __type = '';
let T = [], __r = [], __v = [], __a = [];
let CM = sphere({
	radius: 0.005, opacity: 0.5,
	color: 0xffff00
});
let calculateCM = () => {
	CM.position.copy(bob.position).multiplyScalar(bob.mass).add(
		tmpV.copy(pivot.position).multiplyScalar(pivot.mass)
	).multiplyScalar(1/TotM);
};
scene.init = () => {
	pivot.position.copy(pivot.r0);
	pivot.mass = +txtPivotMass.value;
	pivot.velocity.set(0,0,0);
	pivot.acceleration.set(0,0,0);
	checkKpivot();
	pivot.spring[2].label.setText('k='+pivot.spring[2].k,2);

	rod.L0 = +txtRodLength.value;		// original length of the rod
	rod.theta0 = txtTheta0.value*Math.PI/180.0;
	rod.phi0 = txtPhi0.value*Math.PI/180.0;
	rod.mass = +txtRodMass.value;

	bob.setRadius(+txtBobRadius.value);
	bob.position.set(
		rod.L0*Math.sin(Math.PI-rod.theta0)*Math.cos(rod.phi0),
		rod.L0*Math.sin(Math.PI-rod.theta0)*Math.sin(rod.phi0),
		rod.L0*Math.cos(Math.PI-rod.theta0)
	).add(pivot.position);
	bob.clearTrail();
	bob.mass = +txtBobMass.value;		// kg
	bob.Fg.set(0,0,-9.8*bob.mass);

	rod.position.copy(pivot.position);
	rod.setAxis(
		tmpV.copy(bob.position).sub(pivot.position)
	);
	//rod.arrL.setAxis(tmpV);
	bob.velocity.set(0,0,0);
	//bob.velocity.set(0,0,1).cross(rod.axis).normalize().multiplyScalar(
	//	conicalSpeed(rod.L0,rod.theta0)
	//);
	bob.acceleration.set(0,0,0);

	TotM = bob.mass+pivot.mass;
	calculateCM();

	T.length = 0;
	__r[0] = pivot.position.clone(); __r[1] = bob.position.clone();
	__v[0] = pivot.velocity.clone(); __v[1] = bob.velocity.clone();
	calculateA(__r,__v,0,__a);
	__type = '';
};
//}}}
//{{{
scene.camera.position.multiplyScalar(0.1);
chkGravity.checked = true;
chkGravity.disabled = true;
txtdT.value = 4e-4;
//}}}
!! Pivot Control
<<tiddler "Pendulum Panel##Pivot Control">> / [ =chkMovable] Movable / \(k_\text{pivot}\): <html><input type="number" title="Spring constant of pivot." id="txtKpivot" min="0" max="200" step="0.1" value="81.7" style="width:55px"></html>
!! Rod Control
<<tiddler "Pendulum Panel##Rod Control">>
!! Bob Control
<<tiddler "Pendulum Panel##Bob Control">>
!! Initial Velocity
<<tiddler "Pendulum Panel##Initial Velocity">>
!! Show Control
[ =chkPivotSpring] Springs / [ =chkPivotSpringK] //k//'s
!! Period Label
''T''~~''l''ight~~ (s): <html><label id="labelPeriodLight" title="Oscillation period of the light object." style="font-family:'Courier New'"></label></html> / ''T''~~''h''eavy~~ (s): <html><label id="labelPeriodHeavy" title="Oscillation period of the heavy object." style="font-family:'Courier New'"></label></html>
|AR Pendulum Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "AR Pendulum Panel##Rod Control">>|<<tiddler "AR Pendulum Panel##Pivot Control">>|
|<<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">> / <<tiddler "AR Pendulum Panel##Show Control">>|<<tiddler "AR Pendulum Panel##Bob Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[AR Pendulum Initial]] [[AR Pendulum Codes]]>>|
/***
[img(75%,)[https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg/655px-Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg.png]]
***/
//{{{
'''
---------------------------------------------------------------------------------------------------------
開始:空氣阻力的計算函數
---------------------------------------------------------------------------------------------------------
'''

'''
-------------------------------------------------------------------------------
計算一個移動球體的空氣阻力。
Calculate air drag of a moving SPHERE.
如果物體不是球體,可以將球體的結果乘上一個和形狀有關的係數,這個係數可以從簡單
的模型估計,或者由實驗數據擬和出來。
For non-spherical objects, one simple way is to multiply the result by a
geometrical factor that can be obtained by simple modeling or fitting to
experimental data.
參考 Ref: https://en.wikipedia.org/wiki/Drag_(physics)
用法 Usage:
f_dra = airDragSphere(v, r)
        v: 球體的速度向量 velocity vector of the spherical object,
        r: 球體的半徑 radius of the spherical object.

傳回值為此球體所受的空氣阻力(向量)
The return value is the air drag force of that spherical object in vector form.
-------------------------------------------------------------------------------
'''
eta_air = 1.81e-5   # at 15C, https://en.wikipedia.org/wiki/Viscosity
rho_air = 1.225     # at 15C sea level, https://en.wikipedia.org/wiki/Density_of_air

def ReynoldsNumber(speed,L):
    # 計算直徑為 L 的球狀物體在空氣中速率為 speed 時候的雷諾數
    # Calculates Reynolds number of a sphere with speed v and characteristic length L.
    return speed*L/(eta_air/rho_air);

def dragCoeff(Re):
    '''
    計算對應於雷諾數為 Re 的拖曳係數。拖曳係數是從下圖中估計出來:
    https://en.wikipedia.org/wiki/Drag_(physics)#/media/File:Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg
    雷諾數 Re 需要事先計算出來
    這個函數應該只在 Re > 20 的時候使用,因為上圖的拖曳係數只有在 Re > 20 的部分有值
    
    Calculates drag coefficient C_d according to the Reynolds number.
    The drag coefficient is determined according to the following figure:
    https://en.wikipedia.org/wiki/Drag_(physics)#/media/File:Drag_coefficient_on_a_sphere_vs._Reynolds_number_-_main_trends.svg
    The Reynolds number Re shall be calculated beforehand.
    This function shall be called only for Re > 20, because in the above figure
    there is no information about Cd for Re < 20.
    '''
    
    '''
    As of 2018/05/09, GlowScript 2.7 does not support log10(x), we modified
    the corresponding codes below to log(x)/log(10).        Vincent Yeh
    '''
    
    # 我們將圖中曲線簡化成幾段直線
    # We simplify the curve by viewing it as several linear segments
    if Re < 1e3:
        # Cd linear between 2.0 and 0.5
        # return 2.0+log10(0.5/2.0)/log10(1e3/20)*log10(Re/50)
        return 2.0+(log(0.25)/log(10))/(log(50)/log(10))*(log(Re/50)/log(10))
    elif Re <= 1.5e5:
        # Cd roughly constant
        return 0.5
    elif Re <= 3e5:
        # Cd linear between 0.5 and 0.08
        #return 0.5+log10(0.08/0.5)/log10(3e5/1.5e5)*log10(Re/1.5e5)
        return 0.5+(log(0.16)/log(10))/(log(2)/log(10))*(log(Re/1.5e5)/log(10))
    elif Re < 1.5e6:
        # Cd linear between 0.08 and 0.2
        # return 0.08+log10(0.2/0.08)/log10(1.5e6/3e5)*log10(Re/3e5)
        return 0.08+(log(2.5)/log(10))/(log(5)/log(10))*(log(Re/3e5)/log(10))
    else:
        # Cd roughly constant
        return 0.2

def airDragSphere(v,r):
    '''
    計算一個半徑為 r 速度為 v 的光滑球體之空氣阻力
    Calculate air drag for a spherical object of radius r with velocity v.
    '''
    
    # 首先要計算雷諾數
    # First we calculate the Reynolds number
    Re = ReynoldsNumber(v.mag,r*2)
    
    if Re <= 20:
        # 很低的雷諾數,傳回線性(一次方)阻力
        # very low Reynolds number, return the linear drag
        return -6.0*pi*eta_air*r*v;
    else:
        # 夠高的雷諾數,傳回二次方阻力
        # otherwise return the quadratic drag
        return -0.5*rho_air*v.mag2*dragCoeff(Re)*pi*r*r*v.norm()

'''
-------------------------------------------------------------------------------
結束:空氣阻力的計算函數
-------------------------------------------------------------------------------
'''
//}}}
!!! dV
An object representing a tiny volume, which contains the following properties:
{{{
{
	dx[3]: three tiny displacement vectors representing a tiny volume,
	pos: the corner of this tiny volume where the above three vectors originate,
	center: center of this tiny volume,
	volume: "volume" of this tiny volume.
}
}}}
!!! dtau
(Optional) The //volume// of a tiny volume.
> In any of the field calculation functions, usually //source// could be either ''a point source'' or ''a distribution''. For ''a point source'' this optional argument \(d\tau\) ''must be 0, null/undefined, or simply omitted''. For ''a distribution'', however, this ''must be a positive value'' to calculate the amount of source within a tiny volume.
!!! source
An object representing either a ''point source'' or ''a distribution'', such as //charge// or //current//. Either a point or a distribution, this //source// ''must contain the following two properties'':
* ''source.total'': total amount of source (can be a scalar or a vector, depending on the properties of source), and
* ''source.density'': density of the source, could be
## a scalar for uniformly distributed electric charges, ''or''
## a vector for a __straight line steady__ electric current, ''or''
## a function that returns the density at a given position if non-uniformly distributed.
*** This function will receive the following argument: <<tiddler "Argument Descriptions##dV">>
!!! source_iterator
(Optional) The iterator to ''integrate over the volume of some source''. There are three built-in iterators associated with three coordinate systems:
# {{{CartesianIterator}}} goes through positions using the Cartesian coordinate system.
# {{{SphericalIterator}}} goes through positions using the spherical coordinate system.
# {{{CylindricalIterator}}} goes through positions using the cylindrical coordinate system.
> In any of the field calculation functions, usually //source// could be either ''a point source'' or ''a distribution''. For ''a point source'' this optional argument //source_iterator// ''must be null/undefined, or simply omitted''. For ''a distribution'', however, this ''must be provided'' to integrate over the source.
!!! tinyField
A function to calculate a tiny field at position \(\vec r,\) see the descriptions in
# {{{tinyEPotential()}}}, or
# {{{tinyEField()}}}, or
# {{{tinyBField()}}}
for details.
!!! field_iterator
The iterator to ''go through all positions to calculate a distribution of field''. There are three built-in iterators associated with three coordinate systems:
# {{{CartesianIterator}}} goes through positions using the Cartesian coordinate system.
# {{{SphericalIterator}}} goes through positions using the spherical coordinate system.
# {{{CylindricalIterator}}} goes through positions using the cylindrical coordinate system.
/***
!! Camera position and settings
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(0.25);
chkGravity.checked = chkGravity.disabled = true;
chkAutoCPF.checked = true;
txtTolerance.value = '6e-19';
txtYoungsModulus.value = '1.5e-2';
txtPoissonsRatio.value = '0.5';
txtSphereMass.value = '0.3';
//}}}
/***
!!!! Data Plot
***/
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'Theta~~p~~ (&deg;)' : '\\(\\theta_p\\ (^\\circ)\\)')
).setTitle('Theta vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');

labelPlot[0].innerHTML = '\\(\\theta_p\\) (&deg;):';
labelPlot[1].innerHTML = '\\(v (\\text{m/s})\\):';

activateDAQChannels(2);
attachDAQBuffer(0,0);
attachDAQBuffer(1,1);
//}}}
/***
!! Creation and Definitions
<<<
Creation of a membrane and a ball.
<<<
!!!! ball
***/
//{{{
let ball = $tw.threeD.physicalObject(sphere({
		radius: 5e-3,
		//wireframe: true,
		opacity: 0.5
	}));
attachMotionIndicators(ball);
//}}}
/***
!!!! membrane
***/
//{{{
let membrane = $tw.threeD.physicalObject(box({
	width: 0.2,
	widthSegments: 15,
	height: 0.2,
	heightSegments: 15,
	depth: 0.001,
	depthSegments: 2,
	color: 0xffff00,
	wireframe: true,
	opacity: 0.5
}));

let dw = membrane.getWidth()/membrane.getWidthSegments(),
	dh = membrane.getHeight()/membrane.getHeightSegments(),
	dd = membrane.getDepth()/membrane.getDepthSegments()/2,
	xmax = membrane.geometry.boundingBox.max.x,
	ymax = membrane.geometry.boundingBox.max.y,
	cL0 = Math.sqrt(dw*dw+dh*dh)/2;
//console.log(membrane.geometry.boundingBox);
scene.add(membrane.xy_spring =
	group().add($tw.physics.Spring({
		radius: dd,
		coils: 20,
		axis: vector(dw,dh,0),
		L0: cL0,
		color: 0xff00ff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		radius: dd,
		coils: 20,
		axis: vector(-dw,-dh,0),
		L0: cL0,
		color: 0xff00ff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		radius: dd,
		coils: 20,
		axis: vector(dw,-dh,0),
		L0: cL0,
		color: 0x00ffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		radius: dd,
		coils: 20,
		axis: vector(-dw,dh,0),
		L0: cL0,
		color: 0x00ffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd'))
);
scene.add(membrane.x_spring =
	group().add($tw.physics.Spring({
		pos: vector(-dw-dw/2,-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(dw,0,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(-dw/2,-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(dw,0,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(dw-dw/2,-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(dw,0,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(-dw-dw/2,dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(dw,0,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(-dw/2,dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(dw,0,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(dw-dw/2,dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(dw,0,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd'))
);
scene.add(membrane.y_spring =
	group().add($tw.physics.Spring({
		pos: vector(-dw/2,-dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(0,dh,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(-dw/2,-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(0,dh,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(-dw/2,dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(0,dh,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(dw-dw/2,-dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(0,dh,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(dw-dw/2,-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(0,dh,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd')).add($tw.physics.Spring({
		pos: vector(dw-dw/2,dh-dh/2,0),
		radius: dd,
		coils: 20,
		axis: vector(0,dh,0),
		color: 0xffffff,
		k: 1e-3,
		opacity: 0.5
	},'noadd'))
);
//for(let sp of membrane.xy_spring.children){
//	attachMotionIndicators(sp);
//}
//console.log('w',membrane.getWidthSegments(),'h',membrane.getHeightSegments(),'d',membrane.getDepthSegments());
//console.log('whd',membrane.getWidthSegments()*membrane.getHeightSegments()*membrane.getDepthSegments());
//console.log('nv',membrane.numberOfVertices());
//}}}
/***
!! Initialization
!!!! scene.init
***/
//{{{
scene.init = () => {
	//membrane.calculateVertices();
	membrane.YoungsModulus = +txtYoungsModulus.value*1e9;
	membrane.PoissonsRatio = +txtPoissonsRatio.value;
	membrane.setMass(+txtBoxMass.value);

	ball.position.set(
		membrane.getWidth()/4*Math.random()*(Math.random()<0.5?-1:1),
		membrane.getHeight()/4*Math.random()*(Math.random()<0.5?-1:1),
		membrane.getSize()
	);
	ball.velocity.set(0,0,0);
	ball.acceleration.copy(scene.g);
	ball.YoungsModulus = membrane.YoungsModulus*20;
	ball.setMass(+txtSphereMass.value);

	membrane.xy_spring.position.set(ball.position.x,ball.position.y,0);
	for(let sp of membrane.xy_spring.children) sp.relax();
	membrane.xy_spring.visible = membrane.x_spring.visible = membrane.y_spring.visible = false;
}
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	getTrailParam(ball);
}
//}}}
/***
!!!! ball.onCollision
***/
//{{{
let hertzian = chkHertzianModel.checked,
	arrdf = arrow();
scene.add(arrdf);
ball.onCollision = (obj,fthis,fobj,intersect) => {
	// find the two closest springs in x-dir and y-dir
	let f = intersect[0].face,
		ndx = [f.a, f.b, f.c], drn = 0,
		v = [
			membrane.getVertex(ndx[0]),
			membrane.getVertex(ndx[1]),
			membrane.getVertex(ndx[2])
		],
		xy_axis = [], x_axis = [], y_axis = [],
		p = intersect[0].point.clone(),
		xysg = membrane.xy_spring,
		xsg = membrane.x_spring,
		ysg = membrane.y_spring,
		sp = xysg.children,
		fa1 = f.a+1, a1b = false, a1c = false;

	if(fa1 === f.b){
		a1b = true;
		v[3] = membrane.getVertex(ndx[3]=f.c-1);
		drn = f.b - f.c;
	}else if(fa1 === f.c){
		a1c = true;
		v[3] = membrane.getVertex(ndx[3]=f.b+1);
		drn = f.b - f.a;
	}else{
		console.log('(a,b,c)=(',f.a,',',f.b,',',f.c,')');
	}
	v.center = vector();
	for(let vn of v)
		v.center.add(vn);
	v.center.multiplyScalar(1/v.length);
	//xsg.position.copy(v.center);
	//ysg.position.copy(v.center);

	for(let n=0,N=sp.length; n<N; n++){
		xy_axis[n] = vector();
		//sp[n].relax(xy_axis[n].copy(v[n]).sub(p).length());
	}

	p.sub(ball.position)
		.normalize().multiplyScalar(ball.getRadius())
		.add(ball.position);
	for(let n=0,N=sp.length; n<N; n++)
		sp[n].setAxis(xy_axis[n].copy(v[n]).sub(p));

	let df = vector();
	//for(let n=0,N=sp.length; n<N; n++)
	//	df.add(sp[n].restoringForce());

	for(let n=0,N=sp.length; n<N; n++){
		let spn = sp[n];
		if(spn.axis.mag > spn.L0){
			spn.setAxis(xy_axis[n].normalize().multiplyScalar(spn.L0));
			membrane.setVertex(ndx[n],v[n].copy(p).add(xy_axis[n]));
		}
	}

	sp = xsg.children;
	if(a1b){
		v[4] = membrane.getVertex(f.a-1);
		v[5] = membrane.getVertex(f.b+1);
		v[6] = membrane.getVertex(f.c+1);
		v[7] = membrane.getVertex(f.c-2);
		x_axis[0] = v[6].clone().sub(v[2]);
			x_axis[0].mag = x_axis[0].length();
		x_axis[1] = v[2].clone().sub(v[3]);
			x_axis[1].mag = x_axis[1].length();
		x_axis[2] = v[3].clone().sub(v[7]);
			x_axis[2].mag = x_axis[2].length();
		x_axis[3] = v[5].clone().sub(v[1]);
			x_axis[3].mag = x_axis[3].length();
		x_axis[4] = v[1].clone().sub(v[0]);
			x_axis[4].mag = x_axis[4].length();
		x_axis[5] = v[0].clone().sub(v[4]);
			x_axis[5].mag = x_axis[5].length();

		if(sp[0].visible=x_axis[0].mag < sp[0].L0*2)
			sp[0].setPosition(v[2]).setAxis(x_axis[0]);
		if(sp[1].visible=x_axis[1].mag < sp[1].L0*2)
			sp[1].setPosition(v[3]).setAxis(x_axis[1]);
		if(sp[2].visible=x_axis[2].mag < sp[2].L0*2)
			sp[2].setPosition(v[7]).setAxis(x_axis[2]);
		if(sp[3].visible=x_axis[3].mag < sp[3].L0*2)
			sp[3].setPosition(v[1]).setAxis(x_axis[3]);
		if(sp[4].visible=x_axis[4].mag < sp[4].L0*2)
			sp[4].setPosition(v[0]).setAxis(x_axis[4]);
		if(sp[5].visible=x_axis[5].mag < sp[5].L0*2)
			sp[5].setPosition(v[4]).setAxis(x_axis[5]);
	}else if(a1c){
		v[4] = membrane.getVertex(f.a-1);
		v[5] = membrane.getVertex(f.b-1);
		v[6] = membrane.getVertex(f.c+1);
		v[7] = membrane.getVertex(f.b+2);
		x_axis[0] = v[6].clone().sub(v[2]);
			x_axis[0].mag = x_axis[0].length();
		x_axis[1] = v[2].clone().sub(v[0]);
			x_axis[1].mag = x_axis[1].length();
		x_axis[2] = v[0].clone().sub(v[4]);
			x_axis[2].mag = x_axis[2].length();
		x_axis[3] = v[7].clone().sub(v[3]);
			x_axis[3].mag = x_axis[3].length();
		x_axis[4] = v[3].clone().sub(v[1]);
			x_axis[4].mag = x_axis[4].length();
		x_axis[5] = v[1].clone().sub(v[5]);
			x_axis[5].mag = x_axis[5].length();

		if(sp[0].visible=x_axis[0].mag < sp[0].L0*2)
			sp[0].setPosition(v[2]).setAxis(x_axis[0]);
		if(sp[1].visible=x_axis[1].mag < sp[1].L0*2)
			sp[1].setPosition(v[0]).setAxis(x_axis[1]);
		if(sp[2].visible=x_axis[2].mag < sp[2].L0*2)
			sp[2].setPosition(v[4]).setAxis(x_axis[2]);
		if(sp[3].visible=x_axis[3].mag < sp[3].L0*2)
			sp[3].setPosition(v[3]).setAxis(x_axis[3]);
		if(sp[4].visible=x_axis[4].mag < sp[4].L0*2)
			sp[4].setPosition(v[1]).setAxis(x_axis[4]);
		if(sp[5].visible=x_axis[5].mag < sp[5].L0*2)
			sp[5].setPosition(v[5]).setAxis(x_axis[5]);
	}
	for(let n=0,N=sp.length; n<N; n++)
		df.add(sp[n].restoringForce());

	sp = ysg.children;
	if(a1b){
		v[8] = membrane.getVertex(f.a+drn);
		v[9] = membrane.getVertex(f.b+drn);
		v[10] = membrane.getVertex(f.c-drn);
		v[11] = membrane.getVertex(f.c-1-drn);
		x_axis[0] = v[11].clone().sub(v[3]);
			x_axis[0].mag = x_axis[0].length();
		x_axis[1] = v[3].clone().sub(v[0]);
			x_axis[1].mag = x_axis[1].length();
		x_axis[2] = v[0].clone().sub(v[8]);
			x_axis[2].mag = x_axis[2].length();
		x_axis[3] = v[10].clone().sub(v[2]);
			x_axis[3].mag = x_axis[3].length();
		x_axis[4] = v[2].clone().sub(v[1]);
			x_axis[4].mag = x_axis[4].length();
		x_axis[5] = v[1].clone().sub(v[9]);
			x_axis[5].mag = x_axis[5].length();

		if(sp[0].visible=x_axis[0].mag < sp[0].L0*2)
			sp[0].setPosition(v[3]).setAxis(x_axis[0]);
		if(sp[1].visible=x_axis[1].mag < sp[1].L0*2)
			sp[1].setPosition(v[0]).setAxis(x_axis[1]);
		if(sp[2].visible=x_axis[2].mag < sp[2].L0*2)
			sp[2].setPosition(v[8]).setAxis(x_axis[2]);
		if(sp[3].visible=x_axis[3].mag < sp[3].L0*2)
			sp[3].setPosition(v[2]).setAxis(x_axis[3]);
		if(sp[4].visible=x_axis[4].mag < sp[4].L0*2)
			sp[4].setPosition(v[1]).setAxis(x_axis[4]);
		if(sp[5].visible=x_axis[5].mag < sp[5].L0*2)
			sp[5].setPosition(v[9]).setAxis(x_axis[5]);
	}else if(a1c){
		v[8] = membrane.getVertex(f.a-drn);
		v[9] = membrane.getVertex(f.b+drn);
		v[10] = membrane.getVertex(f.c-drn);
		v[11] = membrane.getVertex(f.b+1+drn);
		x_axis[0] = v[8].clone().sub(v[0]);
			x_axis[0].mag = x_axis[0].length();
		x_axis[1] = v[0].clone().sub(v[1]);
			x_axis[1].mag = x_axis[1].length();
		x_axis[2] = v[1].clone().sub(v[9]);
			x_axis[2].mag = x_axis[2].length();
		x_axis[3] = v[10].clone().sub(v[2]);
			x_axis[3].mag = x_axis[3].length();
		x_axis[4] = v[2].clone().sub(v[3]);
			x_axis[4].mag = x_axis[4].length();
		x_axis[5] = v[3].clone().sub(v[11]);
			x_axis[5].mag = x_axis[5].length();

		if(sp[0].visible=x_axis[0].mag < sp[0].L0*2)
			sp[0].setPosition(v[0]).setAxis(x_axis[0]);
		if(sp[1].visible=x_axis[1].mag < sp[1].L0*2)
			sp[1].setPosition(v[1]).setAxis(x_axis[1]);
		if(sp[2].visible=x_axis[2].mag < sp[2].L0*2)
			sp[2].setPosition(v[9]).setAxis(x_axis[2]);
		if(sp[3].visible=x_axis[3].mag < sp[3].L0*2)
			sp[3].setPosition(v[2]).setAxis(x_axis[3]);
		if(sp[4].visible=x_axis[4].mag < sp[4].L0*2)
			sp[4].setPosition(v[3]).setAxis(x_axis[4]);
		if(sp[5].visible=x_axis[5].mag < sp[5].L0*2)
			sp[5].setPosition(v[11]).setAxis(x_axis[5]);
	}
	for(let n=0,N=sp.length; n<N; n++)
		df.add(sp[n].restoringForce());

	arrdf.setPosition(p).setAxis(df.multiplyScalar(-1));
console.log('df',df.toString());
	fthis.add(df);
	if(fobj) fobj.add(df);

	xysg.position.copy(p);
	xysg.visible = xsg.visible = ysg.visible = true;
	//return false;
}
//}}}
/***
!!!! ball.afterCollision
***/
//{{{
ball.afterCollision = result => {
	ball.velocity.multiplyScalar(0.9);
	//if(ball.velocity.lengthSq()<1e-6) btnStart.click();
}
//}}}
/***
!!!! calculateA
***/
//{{{
const calculateA = (r,v,t,a) => {
	return a;
}
//}}}
/***
!!!! scene.update
***/
//{{{
scene.update = (t_cur,dt) => {
	hertzian = chkHertzianModel.checked;
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.physics.nextPosition(ball,calculateA,t_cur,dt,adaptive,e0),
		result = $tw.physics.objCheckCollisions(ball,hertzian,membrane);
	updateMotionIndicators(ball,0.3);
	if(adaptive) setdT(delta[2]);
}
//}}}
|Ball on Membrane Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">>|<<tw3DCommonPanel "Trail Control">>|
|Ball: <<tw3DCommonPanel "Sphere Properties">> / <<tw3DCommonPanel "Contact Model">>|Membrane: <<tw3DCommonPanel "Box Properties">>|
|Ball: |Membrane: <<tw3DCommonPanel "Elastic Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Ball on Membrane Creation]] [[Ball on Membrane Initialization]] [[Ball on Membrane Iteration]]>>|
/***
!! Calculate accelerations
***/
//{{{
let tmp_r = [], tmp_v = [], tmp_a = [], fex = vector ();
const calculateA = (r,v,t,a) => {
	if(!a) a = [];
	let N = pendulum.length, Nr = 0;
//}}}
/***
<<<
First calculate the force in each of the pendulums,
<<<
***/
//{{{
	// Calculate the forces exerted on each of the bobs and pivots
	pendulum.L = 0;
	for(let n=0,n2=0; n<N; n++){
		Nr = pendulum[n].partsMovable();
		for(let nr=0; nr<Nr; nr++){
			if(tmp_r[nr]) tmp_r[nr].copy(r[n2+nr]);
			else tmp_r[nr] = r[n2+nr].clone();

			if(tmp_v[nr]) tmp_v[nr].copy(v[n2+nr]);
			else tmp_v[nr] = v[n2+nr].clone();

			if(tmp_a[nr]) tmp_a[nr].set(0,0,0);
			else tmp_a[nr] = vector();
		}

		pendulum.L += tmpV.copy(tmp_r[1]).sub(tmp_r[0]).length();

		if(n<N-1 || !pendulum[n].string.soft){
//console.log('0 n=',n,'soft=',pendulum[n].string.soft);
			pendulum[n].calculateForce(tmp_r,tmp_v,tmp_a,chkAirDrag.checked);
		}else{
//console.log('1 n=',n,'soft=',pendulum[n].string.soft);
			if(pendulum.L > pendulum.L0){
				tmp_a[1].copy(tmp_r[1]).sub(tmp_r[0]).normalize().multiplyScalar(
					-pendulum[n].string.k*(pendulum.L-pendulum.L0)
				);
			}else if(pendulum[n].string.soft){
				tmp_a[1].set(0,0,0);
			}
			pendulum[n].string.setTension(tmp_a[1]);
			tmp_a[1].add((pendulum[n].bob[0]||pendulum[n].bob).Fg);
//console.log($tw.ve.round(pendulum.L/pendulum.L0,4));
		}

		for(let nr=0; nr<Nr; nr++){
			if(!a[n2+nr]) a[n2+nr] = vector();
			a[n2+nr].copy(tmp_a[nr]);
//console.log('a[',(n2+nr),']=',a[n2+nr],'n=',n,'Nr=',Nr);
		}
		n2 += Nr;
	}
//}}}
/***
<<<
then maintain the total length
<<<
***/
//{{{
	if(N>1 && pendulum[0].string.soft){
		// If there are more than one pendulums and the rods are soft,
		// consider all the bobs are free to slide except for the last one.
		for(let n=N-2,n2=a.length-Nr; n>=0; n--){
			Nr = pendulum[n].partsMovable();
			n2 -= Nr;
			// Calculate force from the string on the sliding bob.
			a[n2+1].add(
				pendulum[n+1].string.getTension(tmp_a[0])
					.multiplyScalar(-1)
			);
			//tmp_a[0].value = tmp_a[0].length();
			a[n2+1].add(
				tmp_a[1].copy(r[n2]).sub(r[n2+1]).normalize()
					.multiplyScalar(tmp_a[0].length())
			);
			pendulum[n].string.setTension(tmp_a[1]);

			// Add friction from the string on the sliding bob.
			///*
			tmp_a[0].add(tmp_a[1]);
			a[n2+1].add(
				tmp_a[1].copy(v[n2+1]).normalize()
					.multiplyScalar(-tmp_a[0].length()*0.01)
			);
			//*/
		}
	}
//}}}
/***
<<<
finally calculate accelerations
<<<
***/
//{{{
	// The pivot of the first pendulum is not attached to any bob,
	// calculate its acceleration alone, with some external forces,
	// random or periodic.
	let a0 = pendulum.L0*0.01;
	//fex.set(0,0,1).cross(a[0]).normalize().multiplyScalar(d0*w0*w0);
	//a[0].add(fex);
	a[0].multiplyScalar(1/pendulum[0].pivot.mass);
	Nr = pendulum[0].partsMovable();

	// Add the forces of the attached pivot/bob pairs and calculate their accelerations.
	for(let n=1,n2=Nr; n<N; n++){
		// The pivot of this pendulum is attached to the bob of the previous pendulum,
		// add their forces and divide their mass sum to calculate their common
		// acceleration.
		a[n2-Nr+1].add(a[n2]).multiplyScalar(
			1/((pendulum[n-1].bob[0]||pendulum[n-1].bob).mass+pendulum[n].pivot.mass)
		);
		a[n2].copy(a[n2-Nr+1]);
		Nr = pendulum[n].partsMovable();
		n2 += Nr; 
	}

	// The bob of the last pendulum is not attached to any pivot,
	// calculate its acceleration alone.
	a[a.length-Nr+1].multiplyScalar(1/(pendulum[N-1].bob[0]||pendulum[N-1].bob).mass);

	return a;
}
//}}}
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'Theta (&deg;)' : '\\(\\theta\\ (^\\circ)\\)')
).setTitle('Theta vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'Phi (&deg;)' : '\\(\\phi\\ (^\\circ)\\)')
).setTitle('Phi vs Time');

labelPlot[0].innerHTML = '\\(\\theta\\) (&deg;):';
labelPlot[1].innerHTML = '\\(\\phi\\) (&deg;):';
//}}}
/***
!! Status checking function
***/
//{{{
scene.checkStatus = () => {
	for(let n=0,N=pendulum.length; n<N; n++){
		getTrailParam(pendulum[n].pivot);
		if(pendulum[n].bob.length){
			for(let nb=0,NB=pendulum[n].bob.length; nb<NB; nb++){
				getTrailParam(pendulum[n].bob[nb]);
			}
		}else{
			getTrailParam(pendulum[n].bob);
		}
		pendulum[n].showForce(chkShowForce.checked).showPivotSpring(chkPivotSpring.checked)
			.showPivotSpringConstant(chkPivotSpringK.checked);
	}
	checkKpivot(pendulum[0]);
	CM.visible = chkCM.checked;
}
//}}}
/***
!! Update function
***/
//{{{
let bob = [];
scene.update = (t_cur,dt) => {
//}}}
/***
!!!! Check status change
***/
//{{{
	//checkKpivot(pendulum[0]);
//}}}
/***
!!!! next positions and velocities
***/
//{{{
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.numeric.ODE.nextValue(
			__r,__v,calculateA,t_cur,dt,__a,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	let Nr = 0, N = pendulum.length;
	pendulum.L = 0;
	for(let n=0,n2=0; n<N; n++){
		Nr = pendulum[n].partsMovable();
		if(pendulum[n].bob.length){
			pendulum[n].setPosition(__r[n2],__r[n2+1],__r[n2+2]);
			pendulum[n].bob[0].velocity.copy(__v[n2+1]);
			pendulum[n].bob[1].velocity.copy(__v[n2+2]);
			pendulum[n].bob[0].acceleration.copy(__a[n2+1]);
			pendulum[n].bob[1].acceleration.copy(__a[n2+2]);

			$tw.physics.objCheckCollisions(pendulum[n].bob,true);
		}else{
			pendulum[n].setPosition(__r[n2],__r[n2+1]);
			pendulum[n].bob.velocity.copy(__v[n2+1]);
			pendulum[n].bob.acceleration.copy(__a[n2+1]);

			//$tw.physics.objCheckCollisions(pendulum[n].children,true);
		}
		if(N>1 && pendulum[0].string.soft){
			if(n < N-1)
				pendulum.L += (pendulum[n].string.L0 =
					tmpV.copy((pendulum[n].bob[0]||pendulum[n].bob).position)
						.sub(pendulum[n].pivot.position).length());
			else
				pendulum[n].string.L0 = pendulum.L0 - pendulum.L;
		}
		pendulum[n].pivot.velocity.copy(__v[n2]);
		pendulum[n].pivot.acceleration.copy(__a[n2]);
		n2 += Nr;
	}
	if(N>1){
		// Collect all the bobs and check for their collisions.
		if(bob.length === 0)
			for(let n=0; n<N; n++){
				bob[n] = pendulum[n].bob[0] || pendulum[n].bob;
			}
		$tw.physics.objCheckCollisions(bob,chkHertzianModel.checked);
		if(chkHertzianModel.checked)
			update_bob_accelerations();
		else
			update_bob_velocities();
	}
//}}}
/***
!!!! data recording
***/
//{{{
	//saveSimulationData(t_cur,__r,__v,__a,'AR Pendulum Data');

	dataPlot[0].addXPoint(t_cur);
	let domain = dataPlot[0].xscale.domain();
	if(t_cur > domain[1]){
		let t0 = dataPlot[0].getXPoint(0);
		let tnext = dataPlot[0].getXPoint();
		if(tnext){
			t0 = Math.min(t0,tnext);
		}
		domain[0] = t0;
		domain[1] = t_cur+dt*100;
		dataPlot[0].rescaleX(domain);
		dataPlot[1].rescaleX(domain);
	}

	let theta = 180*(1-$tw.threeD.angleFromZ(tmpV)/Math.PI);
	let phi = 180/Math.PI*$tw.threeD.angleFromX(tmpV);
	if(phi>180) theta=-theta;	
	dataPlot[0].addYPoint(theta);
	domain = dataPlot[0].yscale.domain();
	if(theta<domain[0] || theta>domain[1])
		dataPlot[0].rescaleY([
			dataPlot[0].getDataMin(),dataPlot[0].getDataMax()
		]);

	dataPlot[1].addYPoint(phi);
	domain = dataPlot[1].yscale.domain();
	if(phi<domain[0] || phi>domain[1])
		dataPlot[1].rescaleY([
			dataPlot[1].getDataMin(),dataPlot[1].getDataMax()
		]);
	let almost_done = (scene.maxTime()>0 && (scene.maxTime()-t_cur)<=dt);
	if(almost_done || chkPlot0.checked)
		dataPlot[0].refresh();
	if(almost_done || chkPlot1.checked)
		dataPlot[1].refresh();
//}}}
/***
!!!! update status
***/
//{{{
	calculateCM();
}
//}}}
//{{{
//scene.range = normal_range
//scene.center = normal_center
// scene.range = zoom_in_range
// scene.center = pendulun[0].bob[0].position
//scene.forward = vector(0,-0.5,-1)
//}}}
//{{{
// 假設木頭棒子,楊氏係數約為 11 GPa(參考 https://en.wikipedia.org/wiki/Young%27s_modulus)
// radius = 0.005 (diameter 1 cm)
const __kmax__ = 1.1e10*(Math.PI*Math.pow(0.005,2))*0.1;
console.log('__kmax__='+$tw.ve.round(__kmax__,3));
__trail_len__ = +txtTrailLength.value;
__trail_interval__ = +txtTrailInterval.value;
let arrsw = 0.04

let txtPivotMass = document.getElementById("txtPivotMass");
let txtRodMass = document.getElementById("txtRodMass");
let txtBobMass = document.getElementById("txtBobMass");
let txtRodLength = document.getElementById("txtRodLength");
let txtTheta0 = document.getElementById("txtTheta0");
let txtPhi0 = document.getElementById("txtPhi0");
txtTheta0.value = 0;
txtTmax.value = 50;
txtPivotMass.value = 0.01;
txtKpivot.value = 60;
chkMovable.checked = false;
//txtTrailInterval.value = 10;
chkAutoCPF.checked = true;
chkMakeTrail.checked = true;
txtYoungsModulus.value = 1;
txtTolerance.value = '1e-3';
//}}}
/***
!!!! Creation
***/
//{{{
// Arrange the scene
let pendulum = [];
for(let n=0; n<2; n++){
	pendulum[n] = $tw.physics.Pendulum.create({
		pivot: {
			radius:0.005,
			opacity:0.2,
			mass: 0.001,
			make_trail: false,
			retain: __trail_len__,
			interval: __trail_interval__
		},
		rod: {
			axis:vector(0,0,-1),
			radius:+txtRodRadius.value,
			thickness:0.005,
			color:0xFED162,
			//opacity:0.3,
			soft:true,
			coils:60
		},
		bob: {
			radius: +txtBobRadius.value,
			mass: +txtBobMass.value,
			make_trail: false,
			retain: __trail_len__,
			interval: __trail_interval__,
			opacity: 0.5
		}
	});
	scene.add(pendulum[n]);
}
//}}}
//{{{
let d0 = 3.6e-3,
	w0 = 13;
pendulum[0].pivot.r0 = pendulum[0].pivot.position.clone();
pendulum[0].pivot.attachSprings(0.15,__kmax__);
pendulum[0].pivot.addToCSSScene(cssscene,scene.scale);
const checkKpivot = p => {
	if(!p.pivot.spring) return 0;

	let k = +txtKpivot.value;
	for(let n=0,N=p.pivot.spring.length; n<N; n++){
		if(n === 2){
			if(k !== p.pivot.spring[n].k){
				p.pivot.spring[n].k = k;
				p.pivot.spring[n].label.setText('k='+$tw.ve.round(k,2));
				p.pivot.spring[n].setRadius(0.01);
			}
		}else{
			if(__kmax__ !== p.pivot.spring[n].k){
				p.pivot.spring[n].k = __kmax__;
				p.pivot.spring[n].label.setText('k='+$tw.ve.round(__kmax__,2));
				p.pivot.spring[n].setRadius(0.002);
			}
		}
	}
	return k;
}
//}}}
//{{{
for(let n=0,N=pendulum.length; n<N; n++){
	pendulum[n].string.position.copy(pendulum[n].pivot.position);
	pendulum[n].string.k = n ? __kmax__ : 0;
}
//}}}
//{{{
const conicalSpeed = (L,A) => {
	return Math.sqrt(L*scene.g.value*Math.sin(A)*Math.tan(A));
}
//}}}
//{{{
let tmpV = vector();
let __type = '';
const T = [], __r = [], __v = [], __a = [];
const CM = sphere({
	radius: 0.005, opacity: 0.5,
	color: 0xffff00
});
const calculateCM = () => {
	CM.position.set(0,0,0);
	let totalM = 0, tmp = vector();
	for(let n=0,N=pendulum.length; n<N; n++){
		CM.position.add(
			tmp.copy(pendulum[n].calculateCM())
				.multiplyScalar(pendulum[n].totalMass())
		);
		totalM += pendulum[n].totalMass();
	}
	CM.position.multiplyScalar(1/totalM);
	return CM;
};
//}}}
//{{{
scene.init = () => {
	pendulum.L0 = 0;
	for(let n=0,N=pendulum.length; n<N; n++){
		//pendulum[n].init();

		pendulum[n].pivot.setMass(+txtPivotMass.value);
		checkKpivot(pendulum[n]);

		pendulum[n].string.setRadius(+txtRodRadius.value);
		pendulum[n].string.setLength((pendulum[n].string.L0=
			//+txtRodLength.value
			n===0
				?	(txtRodLength.value-txtBobRadius.value)*2
				:	(txtBobRadius.value*2)
		));
		pendulum[n].string.theta0 = txtTheta0.value*Math.PI/180.0;
		pendulum[n].string.phi0 = txtPhi0.value*Math.PI/180.0;
		pendulum[n].string.mass = +txtRodMass.value;
		pendulum.L0 += pendulum[n].string.L0;

		if(pendulum[n].bob.length){
			for(let nb=0,NB=pendulum[n].bob.length; nb<NB; nb++){
				pendulum[n].bob[nb].setRadius(+txtBobRadius.value);
				pendulum[n].bob[nb].setMass(+txtBobMass.value);		// kg
				if(nb===NB-1) pendulum[n].bob[nb].setColor(0xffff00);
				pendulum[n].bob[nb].YoungsModulus = +txtYoungsModulus.value*1e9;
				pendulum[n].bob[nb].PoissonsRatio = +txtPoissonsRatio.value;
			}
		}else{
			pendulum[n].bob.setRadius(+txtBobRadius.value);
			pendulum[n].bob.setMass(+txtBobMass.value);		// kg
			if(n===N-1) pendulum[n].bob.setColor(0xffff00);
			pendulum[n].bob.YoungsModulus = +txtYoungsModulus.value*1e9;
			pendulum[n].bob.PoissonsRatio = +txtPoissonsRatio.value;
		}

		let r_pivot = vector(), r_bob = vector(), L = 0;
		let theta = Math.PI-pendulum[n].string.theta0;
		let sin_theta = Math.sin(theta);
//}}}
//{{{
		// Bob is displaced initially.
		r_pivot.copy(n===0 ? pendulum[n].pivot.r0 : (pendulum[n-1].bob[0]||pendulum[n-1].bob).position);
		L = pendulum[n].string.L0;
		r_bob.set(
			L*sin_theta*Math.cos(pendulum[n].string.phi0),
			L*sin_theta*Math.sin(pendulum[n].string.phi0),
			L*Math.cos(theta)
		).add(r_pivot);
		r_bob.set(0,0,-pendulum[n].string.L0).add(r_pivot);
		pendulum[n].setPosition(r_pivot,r_bob);
//}}}
//{{{
		// The first bob has a non-zero initial velocity.
		/*
		if(n===1)
			(pendulum[n].bob[0]||pendulum[n].bob).velocity
				.set(0*Math.random(),0,0).normalize()
				//.multiplyScalar(1.5);
				.multiplyScalar(Math.sqrt(8*scene.g.value*pendulum[n].string.L0));
		//*/
//}}}
//{{{
		// Pivot is displaced initially.
		///*
		if(pendulum[n].pivot.spring){
			L = d0; //pendulum[n].pivot.spring[0].L0/20;
			r_pivot.set(
				(pendulum[n].pivot.spring[0].k===__kmax__?0:L),
				(pendulum[n].pivot.spring[1].k===__kmax__?0:L),
				(pendulum[n].pivot.spring[2].k===__kmax__?0:L)
			);
			r_bob.set(0,0,-pendulum[n].string.L0).add(r_pivot);
			pendulum[n].setPosition(r_pivot,r_bob);
		}
		//*/
//}}}
//{{{
	
		//pendulum[n].bob.velocity.set(0,0,1).cross(pendulum[n].string.axis)
		//	.normalize().multiplyScalar(
		//		conicalSpeed(pendulum[n].string.L0,pendulum[n].string.theta0)
		//	);
		if(n===0){
			//pendulum[n].pivot.velocity.set(0,0,1).cross(r_pivot)
			//	.normalize().multiplyScalar(d0*w0);
			//pendulum[n].pivot.acceleration.copy(r_pivot,1).cross(r_pivot)
			//	.normalize().multiplyScalar(-d0*w0*w0);
		}
	}
	T.length = 0;
	__type = '';
};
//}}}
//{{{
const update_bob_velocities = () => {
	let NB = 2;
	for(let n=0,n2=0,N=pendulum.length; n<N; n++){
		if(pendulum[n].bob.length){
			__v[n2+1].copy(pendulum[n].bob[0].velocity);
			NB = pendulum[n].bob.length;
		}else{
			__v[n2+1].copy(pendulum[n].bob.velocity);
			NB = 2;
		}
		if(n<N-1){
			pendulum[n+1].pivot.velocity.copy(
				__v[n2+NB].copy(__v[n2+1])
			);
		}
		n2 += NB;
	}
};
const update_bob_accelerations = () => {
	let NB = 2;
	for(let n=0,n2=0,N=pendulum.length; n<N; n++){
		if(pendulum[n].bob.length){
			__a[n2+1].copy(pendulum[n].bob[0].acceleration);
			NB = pendulum[n].bob.length;
		}else{
			__a[n2+1].copy(pendulum[n].bob.acceleration);
			NB = 2;
		}
		if(n<N-1){
			pendulum[n+1].pivot.acceleration.copy(
				__a[n2+NB].copy(__a[n2+1])
			);
		}
		n2 += NB;
	}
};
//}}}
//{{{
scene.initialized = () => {
	for(let n=0,n2=0,N=pendulum.length; n<N; n++){
		//if(pendulum[n].pivot.movable()){
			__r[n2] = pendulum[n].pivot.position.clone();
			__v[n2] = pendulum[n].pivot.velocity.clone();
			n2++;
		//}

		if(pendulum[n].bob.length){
			let NB = pendulum[n].bob.length;
			for(let nb=0; nb<NB; nb++){
				__r[n2+nb] = pendulum[n].bob[nb].position.clone();
				__v[n2+nb] = pendulum[n].bob[nb].velocity.clone();
			}
			n2 += NB;
		}else{
			__r[n2] = pendulum[n].bob.position.clone();
			__v[n2] = pendulum[n].bob.velocity.clone();
			n2++;
		}
	}
	calculateCM();
	calculateA(__r,__v,0,__a);
};
//}}}
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(txtRodLength.value*2);
chkGravity.checked = true;
chkGravity.disabled = true;
txtdT.value = 2e-4;
//}}}
|Balls on a String Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion Status">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "AR Pendulum Panel##Pivot Control">>|<<tiddler "AR Pendulum Panel##Bob Control">>|
|<<tiddler "AR Pendulum Panel##Rod Control">>|<<tw3DCommonPanel "Contact Model">> / <<tw3DCommonPanel "Elastic Properties">>|
|Initial: <<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">> / <<tiddler "AR Pendulum Panel##Show Control">>||
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Balls on a String Initial]] [[Balls on a String Codes]]>>|
/***
!! Creation and Definitions
<<<
Creation of an elastic band and two balls.
<<<
!!!! Two balls
***/
//{{{
let ball = [
	$tw.threeD.physicalObject(sphere({
		radius: 1e-2,
		//wireframe: true,
		color: 0xff0000,
		opacity: 0.8,
		make_trail: false
	})),
	$tw.threeD.physicalObject(sphere({
		radius: 1e-2,
		//wireframe: true,
		color: 0x00ff00,
		opacity: 0.8,
		make_trail: false
	}))
];
for(let n=0,N=ball.length; n<N; n++){
	attachMotionIndicators(ball[n]);
}
ball.updateMotionIndicators = factor => {
	factor = factor || 0.01;
	for(let n=0,N=ball.length; n<N; n++)
		updateMotionIndicators(ball[n],factor);
}
//}}}
/***
!!!! Elastic band
***/
//{{{
let band = $tw.threeD.physicalObject(helix({
	axis: vector(1e-1,0,0),
	radius: 5e-4,
	color: 0xffff00,
	coils: 100,
	opacity: 0.5
}));
band.dr = vector();
band.calculateKs = () => {
	band.k = band.k0+band.dk_dangle*band.angle0;
	band.k_twist = band.k_twist0+band.dk_twist_dangle*band.angle0;
//console.log('k0=',band.k0,'k=',band.k,'k_twist0=',band.k_twist0,'k_twist=',band.k_twist);
};
//}}}
/***
!!!! Floor
***/
//{{{
let floor = box({
	width: 1,
	height: 1,
	depth: 0.01,
	color: 0x999999,
	opacity: 0.3
});
//}}}
/***
!!!! Data Plot
***/
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'Theta~~p~~ (&deg;)' : '\\(\\theta_p\\ (^\\circ)\\)')
).setTitle('Theta vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');

labelPlot[0].innerHTML = '\\(\\theta_p\\) (&deg;):';
labelPlot[1].innerHTML = '\\(v (\\text{m/s})\\):';

activateDAQChannels(2);
attachDAQBuffer(0,0);
attachDAQBuffer(1,1);
//}}}
/***
!! Camera position and settings
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(0.1);
chkGravity.checked = chkGravity.disabled = true;
//chkAutoCPF.checked = true;
txtLength.value = 0.1;
txtMaxTurns.value = 20;
txtTurns.value = 15;
txtTwistK.value = 1e-3;
txtSpringK.value = 100;
txtSphereRadius.value = 0.01;
txtTolerance.value = '1e-6';
//}}}
/***
!! Initialization
!!!! scene.init
***/
//{{{
scene.init = () => {
	let L = +txtLength.value, R = +txtSphereRadius.value;

	band.L0_2 = L/2;
	band.angle_max = +txtMaxTurns.value*2*Math.PI;
	band.angle0 = +txtTurns.value*2*Math.PI;
	band.k_twist0 = +txtTwistK.value;
	band.k0 = +txtSpringK.value;

	band.dk_dangle = band.k0/(2*Math.PI)/2;
	band.dk_twist_dangle = band.k_twist0/(2*Math.PI)/10;
	band.dL_dangle = -L / band.angle_max;
	band.calculateKs();
	L += band.dL_dangle*band.angle0;
console.log('angle_max=',band.angle_max,'angle=',band.angle0,'dL_dangle=',band.dL_dangle,'L=',L);

	ball[0].setPosition(-L/2,0,0);
	ball[1].setPosition(+L/2,0,0);

	band.angular_position.set(band.angle0,0,0);
	band.position.copy(ball[0].position);
	band.dr.copy(ball[1].position).sub(ball[0].position);
	band.setAxis(band.dr).calculateCM();
console.log('band.L0_2=',band.L0_2,'band.CM=',band.CM);

	ball[0].setMass(+txtSphereMass.value);
	ball[1].setMass(+txtSphereMass.value);
	ball[0].setRadius(R);
	ball[1].setRadius(R);
	ball[0].I = 2/5*ball[0].mass*R*R;
	ball[1].I = 2/5*ball[1].mass*R*R;
	ball[0].angular_position.set(band.angle0,0,0);
	ball[0].rotation.set(band.angle0,0,0);
	ball[1].angular_position.set(-band.angle0,0,0);
	ball[1].rotation.set(-band.angle0,0,0);
console.log('ball[0].pos=',ball[0].position,'ball[1].pos=',ball[1].position);

	floor.position.z = -R-floor.getDepth()/2;
	ball.updateMotionIndicators();
}
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	for(let n=0,N=ball.length; n<N; n++){
		ball[n].arrV.show(chkShowVelocity.checked);
		ball[n].arrA.show(chkShowAcceleration.checked);
		ball[n].arrOmega.show(chkShowAngularVelocity.checked);
		ball[n].arrAlpha.show(chkShowAngularAcceleration.checked);
		ball[n].arrF.show(chkShowForce.checked);
		ball[n].arrTorque.show(chkShowTorque.checked);
		getTrailParam(ball[n]);
	}
	ball.updateMotionIndicators();
};
//}}}
/***
!!!! calculateA
***/
//{{{
let calculateA = (r,v,t,a) => {
	let alpha = vector();
	for(let n=0,N=a.length; n<N; n++){
		alpha.copy(a[n].copy(r[n]).sub(band.CM));
		let dr = a[n].length()-band.L0_2;
		a[n].normalize().multiplyScalar(-band.k*dr/ball[n].mass).add(
			alpha.cross(scene.zaxis).normalize().multiplyScalar(
				ball[n].getRadius()*ball[n].angular_acceleration.x*(n?-1:1)
			)
		);
	}
	return a;
};
//}}}
/***
!!!! calculateAlpha
***/
//{{{
let calculateAlpha = (theta,omega,t,alpha) => {
	for(let n=0,N=alpha.length; n<N; n++){
		alpha[n].set(-band.k_twist*theta[n].x/ball[n].I,0,0);
//console.log('k_twist=',band.k_twist,'theta['+n+']=',theta[n],'alpha['+n+']=',alpha[n]);
	}
	return alpha;
};
//}}}
/***
!!!! scene.update
***/
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta_pos = $tw.physics.nextPosition(ball,calculateA,t_cur,dt,adaptive,e0),
		delta_angle = $tw.physics.nextAngle(ball,calculateAlpha,t_cur,dt,adaptive,e0),
		hertzian = chkHertzianModel.checked,
		result = $tw.physics.objCheckCollisions(ball,hertzian);
	if(adaptive) setdT(Math.min(delta_pos[2],delta_angle[2]));
//console.log('ball[0].a=',ball[0].acceleration);
//console.log('ball[1].a=',ball[1].acceleration);
//console.log('ball[0].theta=',ball[0].angular_position.x,'ball[1].alpha=',ball[1].angular_position.x);
//console.log('ball[0].alpha=',ball[0].angular_acceleration.x,'ball[1].alpha=',ball[1].angular_acceleration.x);

	band.dr.copy(ball[1].position).sub(ball[0].position);
	band.position.copy(ball[0].position);
	band.setAxis(band.dr).calculateCM();

	band.calculateKs();
//console.log('ball[0].angular_position=',band.angular_position.x);

	ball.updateMotionIndicators();
};
//}}}
|Balls on an Elastic Band Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">>|<<tw3DCommonPanel "Trail Control">>|
|Ball: <<tw3DCommonPanel "Sphere Properties">> / <<tw3DCommonPanel "Contact Model">>|Rubber band: <<tw3DCommonPanel "Rubber Band Geometry">> / Init: <html><input type="number" title="Number of twist turns" id="txtTwistTurns" min="0" max="50" step="1" value="10" style="width:45px"></html> turns.|
|Init: <<tw3DCommonPanel "Number of Turns">> / Max turns: <html><input type="number" title="Max number of turns" id="txtMaxTurns" min="30" max="100" step="10" value="30" style="width:45px"></html>|band: <<tw3DCommonPanel "Rubber Band Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Balls on an Elastic Band Creation]] [[Balls on an Elastic Band Initialization]] [[Balls on an Elastic Band Iteration]]>>|
/***
!! Creation and Definitions
<<<
Creation of loop and bead.
<<<
!!!! Loop
***/
//{{{
let Rbead = 0.005, Rring = 0.15,
	Hring = 0.02, loop = group();
loop.add(ring({
	Rin: Rring-Hring/2,		// inner radius
	Rout: Rring+Hring/2,	// outer radius
	mass: 0.2,				// mass 0.2 kg
	//wireframe: true,
	color: 0xff0000,
	opacity: 0.5
},'noadd')).add(ring({
	Rin: Rring-Hring/2,		// inner radius
	Rout: Rring+Hring/2,	// outer radius
	mass: 0.2,				// mass 0.2 kg
	//wireframe: true,
	color: 0x00ff00,
	opacity: 0.5
},'noadd')).add(torus({
	radius: Rring+Rbead,	// radius of the torus
	tube: Rbead,			// tube radius 0.01 m
	mass: 0.2,				// mass 0.2 kg
	//wireframe: true,
	color: 0xffffff,
	opacity: 0.5
},'noadd'));
scene.add(loop);
loop.children[0].rotation.y = -Math.PI/2;
loop.children[0].position.set(Rbead,0,0);
loop.children[1].rotation.y = Math.PI/2;
loop.children[1].position.set(-Rbead,0,0);
loop.children[2].rotation.y = Math.PI/2;
loop.angle = vector(0,0,1);
loop.angle.value = 0;
loop.angular_velocity = vector();
loop.angular_acceleration = vector();

//console.log('loop.children[0].quaternion=',$tw.ve.object.toString(loop.children[0].quaternion));
//loop.rotation.y = Math.PI/2;
//console.log('loop.children[0].quaternion=',$tw.ve.object.toString(loop.children[0].quaternion));

loop.arrFace = [];
for(let n=0,N=loop.children.length; n<N; n++){
	loop.arrFace[n] = arrow({
		color: loop.children[n].getColor(),
		visible: false
	}).attachLabel(label({
		text: (n+''),
		size: '10pt',
		color: loop.children[n].getColor()
	}));
};
//}}}
/***
!!!! Bead
***/
//{{{
let bead = $tw.threeD.physicalObject(sphere({
	radius: Rbead,			// radius of the bead
	color: 0xffff00,
	opacity: 0.7,
	mass: 0.2,				// mass 0.2 kg
	make_trail: false
}));
bead.velocity = vector();
bead.acceleration = vector();
bead.arrV = arrow({
	visible: false,
	color: 0x00ffff
});
bead.arrdV = arrow({
	visible: false,
	color: 0xffff00
});
bead.arrA = arrow({
	visible: false,
	color: 0x00ff00
});
bead.arrdA = arrow({
	visible: false,
	color: 0x00ffff
});
//bead.add(bead.arrV).add(bead.arrA).add(bead.arrdV).add(bead.arrdA);

bead.arrV.attachLabel(label({
	text: '\\(\\vec v\\)',
	size: '10pt',
	color: bead.arrV.getColor()
}));
bead.arrdV.attachLabel(label({
	text: '\\(d\\vec v\\)',
	size: '10pt',
	color: bead.arrdV.getColor()
}));
bead.arrA.attachLabel(label({
	text: '\\(\\vec a\\)',
	size: '10pt',
	color: bead.arrA.getColor()
}));
bead.arrdA.attachLabel(label({
	text: '\\(d\\vec a\\)',
	size: '10pt',
	color: bead.arrdA.getColor()
}));
//}}}
/***
!!!! Center of Mass
***/
//{{{
let CM = sphere({
	radius: 0.001,
	opacity: 0.5,
	visible: false,
	color: 0xffff00
});
CM.arrV = arrow({
	visible: false,
	color: 0x0000ff
},'noadd');
CM.arrA = arrow({
	visible: false,
	color: 0x00ff00
},'noadd');
CM.arrTau = arrow({
	visible: false,
	color: 0xff0000
},'noadd');
CM.add(CM.arrV).add(CM.arrA).add(CM.arrTau);
const calculateCM = () => {
	let tmpV = vector();
	CM.position.copy(loop.position).multiplyScalar(loop.mass);
	CM.position.add(
		//battery.anode.localToWorld(
			tmpV.copy(bead.position).multiplyScalar(bead.mass)
		//)
	);
	CM.position.multiplyScalar(1/CM.mass);
};
//}}}
/***
!!!! Opacity
***/
//{{{
if(typeof txtOpacity !== 'undefined'){
	txtOpacity.onchange = function(){
		let op = this.value;
		if(bead.getOpacity() !== op){
			bead.setOpacity(op);
			loop.setOpacity(op);
		}
	}
	txtOpacity.value = 0.7;
}
//}}}
/***
!!!! Data Plot
***/
//{{{
//dataPlot[0].setYTitle(
//	(config.browser.isIE ? 'Theta~~p~~ (&deg;)' : '\\(\\theta_p\\ (^\\circ)\\)')
//).setTitle('Theta vs Time');
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'a (m/s^^2^^)' : '\\(a (\\text{m/s}^2)\\)')
).setTitle('Acceleration vs Time');

//labelPlot[0].innerHTML = '\\(\\theta_p\\) (&deg;):';
labelPlot[0].innerHTML = '\\(v (\\text{m/s})\\):';
labelPlot[1].innerHTML = '\\(a (\\text{m/s}^2)\\):';

activateDAQChannels(2);
attachDAQBuffer(0,0);
attachDAQBuffer(1,1);
//}}}
/***
!! Camera view and others
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(Rring*2);
if(chkGravity !== 'undefined'){
	chkGravity.checked = true;
	chkGravity.disabled = true;
}
txtdT.value = 1e-5;
txtTrailInterval.value = 15;
//txtTrailLength.value = 32768;
txtTheta0.value = 30;
txtOmega.value = Math.PI*2;
txtYoungsModulus.value = 1;
chkShowVelocity.checked = chkShowAcceleration.checked = chkHertzianModel.checked = true;
//}}}
/***
!! Initialization
<<<
Variables {{{__r[], __v[], __a[]}}} are arrays to store the position, velocity, and acceleration, either linear or angular, of each of the moving objects in the bead-loop system..
<<<
***/
//{{{
let __r = [
		vector(),				// angular position of the loop
		vector(),				// position of the bead
	],
	__v = [
		vector(),				// angular velocity of the loop
		vector(),				// velocity of the bead
	],
	__a = [
		vector(),				// angular acceleration of the loop
		vector(),				// acceleration of the bead
	];
//}}}
/***
!!!! scene.init()
***/
//{{{
let tmpR = vector(), tmpV = vector();
scene.init = () => {
	if(loop.angle.value)
		loop.rotateWorld(loop.angle,-loop.angle.value);
	loop.angle.set(0,0,1);
	loop.angle.value = 0;
	loop.angular_velocity.set(
		0,0,
		(typeof txtOmega !== 'undefined' ? txtOmega.value*1 : Math.PI/2.5)
	);
	loop.angular_acceleration.set(0,0,0);
	for(let n=0,N=loop.arrFace.length; n<N; n++)
		loop.arrFace[n].hide();

	bead.YoungsModulus = txtYoungsModulus.value*1e9;
	bead.PoissonsRatio = txtPoissonsRatio.value*1;
	for(let n=0,N=loop.children.length; n<N; n++){
		loop.children[n].YoungsModulus = bead.YoungsModulus;
		loop.children[n].PoissonsRatio = bead.PoissonsRatio;
	}

	let R = Rring-Rbead,
		theta0 = txtTheta0.value*Math.PI/180;
	bead.position.copy(loop.position);
	//bead.position.y += R;
	bead.position.y += R*Math.sin(theta0);
	bead.position.z -= R*Math.cos(theta0);

	bead.velocity.set(0,0,0);
	/*
	tmpR.set(bead.position.x,bead.position.y,0);
	bead.velocity.copy(loop.angular_velocity).cross(tmpR);
	//*/
	bead.acceleration.copy(scene.g);
	bead.arrV.setAxis(bead.velocity).scaleLength(0.5);
	bead.arrA.setAxis(bead.acceleration).scaleLength(0.05);
	bead.arrdV.setAxis(bead.acceleration);
	bead.arrdA.setAxis(bead.acceleration);

	// angular position of the loop
	__r[0].copy(loop.angle).multiplyScalar(loop.angle.value);
	// position of the bead
	__r[1].copy(bead.position);

	// angular velocity of the loop
	__v[0].copy(loop.angular_velocity);
	// velocity of the bead
	__v[1].copy(bead.velocity);

	// angular acceleration of the loop
	__a[0].copy(loop.angular_acceleration);
	// acceleration of the bead
	__a[1].copy(bead.acceleration);
};
//}}}
/***
!!!! checkCameraView
***/
//{{{
/*
let CameraView = document.getElementsByName('CameraView');
CameraView.current = 'none';
for(let n=0,N=CameraView.length; n<N; n++){
	CameraView[n].checked = (CameraView[n].value === 'top');
}
let checkCameraView = () => {
	for(let n=0,N=CameraView.length; n<N; n++){
		if(CameraView[n].checked && CameraView[n].value !== CameraView.current){
			if(CameraView[n].value === 'top'){
				// Top view
				scene.camera.position.set(0,0,scene.camera.position.length());
				scene.setUp(0,1,0);
				scene.camera.lookAt(vector(0,0,-1));
			}else if(CameraView[n].value === 'side'){
				// Side view
				scene.camera.position.set(
					0,
					-scene.camera.position.length(),
					scene.camera.position.length()/40
				);
				scene.setUp(0,0,1);
				scene.camera.lookAt(vector(0,1,0));
			}else if(CameraView[n].value === 'end'){
				// End view
				scene.camera.position.set(scene.camera.position.length(),0,0);
				scene.setUp(0,0,1);
				scene.camera.lookAt(vector(-1,0,0));
			}
			CameraView.current = CameraView[n].value;
			break;
		}
	}
}
//*/
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	//cartesian.show(chkShowXYZ.checked);
	//checkCameraView();

	bead.arrdV.show(chkShowVelocity.checked);
	bead.arrV.show(chkShowVelocity.checked);
	bead.arrdA.show(chkShowAcceleration.checked);
	bead.arrA.show(chkShowAcceleration.checked);
	getTrailParam(bead);

	/*
	CM.visible = chkCM.checked;
	if(CM.visible){
		CM.arrV.visible = chkShowVelocity.checked;
		CM.arrA.visible = chkShowAcceleration.checked;
		CM.arrTau.visible = chkShowAcceleration.checked;
	}
	//*/
}
//}}}
/***
!! Acceleration calculations
***/
/***
!!! calcA(\(r[],v[],t,a[]\))
<<<
Calculate both linear and angular acceleration of the system. The argument \(r[0]\) shall store the linear position of the system, which can be used to calculate the positions of magnets at both ends of the battery, then calculate the magnetic forces exerting on the magnets. Those forces will determine both the linear and angular acceleration of the system.
<<<
***/
//{{{
let calcA = function(r,v,t,a){
//}}}
/***
!!!! force and torque on the loop
***/
//{{{
	//xx
//}}}
/***
!!!! force and torque on the bead
***/
//{{{
	if(a.isVector3)
		a.copy(scene.g);
	else
		a[1].copy(scene.g);
//}}}
/***
!!!! return the acceleration
***/
//{{{
	return a;
}
//}}}
/***
!! Update functions
***/
/***
!!! scene.update
***/
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		// calculate next position using 4th order Runge-Kutta mehtod
		delta_pos = $tw.numeric.ODE.nextValue(
			__r,__v,calcA,t_cur,dt,__a,adaptive,e0
		),
		delta_angle = $tw.physics.nextPosition(
			bead,calcA,t_cur,dt,adaptive,e0
		),
		angle = loop.angle.value;
	if(adaptive) setdT(Math.min(delta_pos[2],delta_angle[2]));

	loop.angle.copy(__r[0]).normalize();
	loop.angle.value = __r[0].length();
	let dangle = loop.angle.value - angle;
	loop.rotateWorld(loop.angle,dangle);
	loop.angular_velocity.copy(__v[0]);
	loop.angular_acceleration.copy(__a[0]);

	bead.position.copy(__r[1]);
	bead.velocity.copy(__v[1]);
	bead.acceleration.copy(__a[1]);

	let hertzian = chkHertzianModel.checked,
		collisions = $tw.physics.objCheckCollisions(bead,hertzian,loop);
	if(hertzian){
		__a[1].copy(bead.acceleration);
	}
	__v[1].copy(bead.velocity);

	for(let nc=0,NC=collisions.intersects_on_walls.length; nc<NC; nc++){
		let intersects_on_wall = collisions.intersects_on_walls[nc];
		for(let n=0,N=intersects_on_wall.length; n<N; n++){
			if(intersects_on_wall[n].collision){
				// collided with wall[n]
				//loop.children[n].setColor(0x00ff00);
				loop.arrFace[n].position.copy(intersects_on_wall[n][0].point);
				loop.arrFace[n].setAxis(
					intersects_on_wall[n][0].face.normal
				).scaleLength(0.2).show();
			}else{
				// not collided with wall[n]
				//loop.children[n].setColor(0xffffff);
				loop.arrFace[n].hide();
			}
		}
	}

	bead.arrV.position.copy(bead.position);
	bead.arrV.setAxis(__v[1]).scaleLength(0.5);
	bead.arrA.position.copy(bead.position);
	bead.arrA.setAxis(__a[1]).scaleLength(0.05);
	bead.arrdV.position.copy(bead.position);
	bead.arrdV.setAxis($tw.physics.__dv[0]).scaleLength(0.2);
	bead.arrdA.position.copy(bead.position);
	bead.arrdA.setAxis($tw.physics.__df[0]).scaleLength(0.05);

	recordData(t_cur,t_cur,[__v[1].length(),__a[1].length()]);
}
//}}}
|Bead Dynamics Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Initial Theta">> / \(\omega\) (1/s): <html><input type="number" title="omega" id="txtOmega" min="0" max="315" step="0.1" value="3.14" style="width:45px"></html>|<<tw3DCommonPanel "Contact Model">> / <<tw3DCommonPanel "Elastic Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Bead Dynamics Creation]] [[Bead Dynamics Initialization]] [[Bead Dynamics Iteration]]>>|
[[ndarray-fft|https://libraries.io/npm/ndarray-fft]]
!!! Speed
The [[FFTW benchmark|http://www.fftw.org/benchfft/]] [[speed page|http://www.fftw.org/speed/]]
!!! Accuracy
The [[FFTW benchmark|http://www.fftw.org/benchfft/]] [[accuracy page|http://www.fftw.org/accuracy/]]
對於長度不是 2 的次方數之陣列進行 FFT,尤其是長度為質數的情況,普遍採用的是 Bluestein 演算法,或稱為 [[Chirp Z 轉換|https://en.wikipedia.org/wiki/Chirp_Z-transform]]。Chirp Z 轉換可以比 FFT 更有彈性或者獲得更多訊息,這點可以接受,但我不能理解的是,假如還是得使用 FFT 函數,那就還是受限於 FFT 函數的長度要求,
!! The Chirp Z Transform
簡單來說,Bluestein 演算法是利用 convolution 來做 Chirp Z 轉換,而其中 convolution 的部分是利用 FFT 來完成的,也就是說,要用 Bluestein 演算法來計算長度為質數的陣列之 FFT,得先有一個可用的 FFT 才行。一般來講 FFT 的演算法多是針對長度為 2 的次方數所設計的,少數則是針對長度為質數所設計。
!!!! 簡單推導
按照[[維基百科 Chirp Z 轉換|https://en.wikipedia.org/wiki/Chirp_Z-transform]]的說明,DFT 的定義:\[X_k = \sum_{n=0}^{N-1}x_n e^{-{i2\pi \over N} nk} \qquad k = 0, \dots, N-1.\] 如果把 \(nk\) 改寫成 \[nk = {-(k-n)^2 \over 2}+{n^2 \over 2}+{k^2 \over 2},\] 的話,則 \[X_k = e^{-{i\pi \over N}k^2} \sum_{n=0}^{N-1}\left(x_n e^{-{i\pi \over N}n^2}\right)e^{-{i\pi \over N}(k-n)^2} \qquad k = 0,\dots,N-1.\] 這個結果正好就是 \[\begin{eqnarray*}a_n &=& x_ne^{-{i\pi \over N}(n-k)^2} \\ b_n &=& e^{-{i\pi \over N}n^2}\end{eqnarray*}\] 這兩個序列的 convolution(卷積)再乘上 \(b_k^*\):\[X_k = b_k^* \sum_{n=0}^{N-1}a_n b_{n-k} \qquad k = 0,\dots,N-1.\] 這個 convolution 的計算可以利用 FFT 來完成:\[FT(f\circ g) = FT(f) \cdot FT(g),\] 其中 \(f \circ g\) 表示 \(f\) 與 \(g\) 的 convolution。上面的式子告訴我們,\[f \circ g = iFT(FT(f) \cdot FT(g)).\]
/***
!! Creation and Definitions
<<<
Creation of battery and magnets.
<<<
!!!! Foil
***/
//{{{
let foil = box({
	width: 0.5,				// 50cm
	height: 0.5,			// 50cm
	depth: 1e-4,			// 0.1mm
	opacity: 0.3
});
foil.conductivity = 3.77e7;	// S/m
foil.resistivity = 2.65e-8;	// Ohm m at 20C for aluminum foil, value obtained from
							// http://hyperphysics.phy-astr.gsu.edu/hbase/Tables/rstiv.html
							// and
							// https://en.wikipedia.org/wiki/Electrical_resistivity_and_conductivity
foil.calculateResistance = function(iterator){
	let l = iterator.getDistance(0),
		A = iterator.getDistance(1)*iterator.getDistance(2);
	return (foil.R = foil.resistivity*l/A);
};
//}}}
/***
!!!! Battery
***/
//{{{
let system = group();
system.mu_s = 0;
system.mu_k = 0;
scene.add(system);
system.angle = vector();

let battery = cylinder({
	radius: 0.005,			// 5mm
	axis: vector(0.05,0,0),	// 5cm in x-dir
	color: 0xff0000,
	opacity: 0.7,
	make_trail: false
},'noadd');
battery.velocity = vector();
battery.V = 1.5;
battery.R = 0.15;	// Ohm for AA alkaline battery, obtained from
					// https://en.wikipedia.org/wiki/Internal_resistance
battery.getVoltage = function(){
	return battery.V;
};
battery.anode = cylinder({
	radius: battery.getRadius()/2,
	axis: vector(battery.getLength()/20,0,0),
	color:0xbbbbbb,
	opacity: 0.7
},'noadd');
system.add(battery).add(battery.anode);
//}}}
/***
!!!! Magnets
***/
//{{{
let magnet = [
	cylinder({
		radius: 0.007,				// 7mm
		axis: vector(0.005,0,0),	// 5mm
		color: 0xffff00,			// yellow
		opacity: 0.5,
		make_trail: false
	},'noadd'),
	cylinder({
		radius: 0.007,				// 7mm
		axis: vector(-0.005,0,0),	// 5mm
		color: 0x00ffff,			// cyan
		opacity: 0.5,
		make_trail: false
	},'noadd')
];
for(let n=0,N=magnet.length; n<N; n++){
	magnet[n].mu = $tw.physics.magneticDipoleMoment({
		center: magnet[n].position,
		axis: magnet[n].axis,						// 箭頭跟磁鐵同向,長度與磁鐵相同
		moment: magnet[n].axis,						// 磁偶極矩(大小在後面決定)
		volume: magnet[n].volume(),					// 磁鐵體積
		color: magnet[n].getColor()					// 顏色為磁鐵顏色
	});
console.log('magnet['+n+'].mu=',magnet[n].mu);
	// 積分參數
	///*
	magnet[n].mu.iterator = CylindricalIterator.create({
		//position: magnet[n].position,
		//axis: magnet[n].axis,
		rmin: 0,
		rmax: magnet[n].getRadius(),
		Nr: 20,
		phimin: 0,
		phimax: Math.PI*2,
		Nphi: 60,
		zmin: -magnet[n].getLength()/2,
		zmax: magnet[n].getLength()/2,
		Nz: 10
	});
	//*/

	magnet[n].resistivity = 1.4e-2; // Ohm m for neodymium magnets, obtained from
									// https://en.wikipedia.org/wiki/Neodymium_magnet
									// Note that in the source the value is given as a
									// range in Ohm cm, while we have calculated the mean
									// and converted it into Ohm m here.
	magnet[n].calculateResistance = function(iterator){
		let l = iterator.getDistance(2),
			A = iterator.getDistance(0)*iterator.getDistance(1);
		if(l < $tw.threeD.EPSILON) l = $tw.threeD.EPSILON*10;
		if(A < $tw.threeD.EPSILON) A = $tw.threeD.EPSILON*10; 
console.log('l=',l,'A=',A);
		return (magnet[n].R = magnet[n].resistivity*l/A);
	};

	// Calibrate the strength to 0.4T at the center of one end.
	magnet[n].mu.calibrate(magnet[n].mu.getAxis().add(magnet[n].mu.position),0.4);
console.log('magnet['+n+']=',magnet[n]);

	magnet[n].mu_s = 0.61;		// coefficient of static friction
	magnet[n].mu_k = 0.47;		// coefficient of kinetic friction
								// Values between Al and steel, obtained from
								// https://en.wikipedia.org/wiki/Friction
	magnet[n].mu_r = 0.02;		// coefficient of rolling drag
								// Estimated using the information in
								// https://en.wikipedia.org/wiki/Rolling_resistance

	system.mu_s += magnet[n].mu_s;
	system.mu_k += magnet[n].mu_k;

	magnet[n].pivot = vector();
	magnet[n].velocity = vector();
	magnet[n].angle = vector();
	magnet[n].angular_velocity = vector();

	magnet[n].fm = vector();
	magnet[n].fmfoil = vector();
	magnet[n].fmmagnet = vector();
	magnet[n].arrF = arrow({
		color: magnet[n].getColor()
	},'noadd');
	magnet[n].arrFfoil = arrow({
		//color: magnet[n].getColor()
	},'noadd');
	magnet[n].arrFmagnet = arrow({
		//color: magnet[n].getColor()
	},'noadd');

	magnet[n].taum = vector();
	magnet[n].taumH = vector();
	magnet[n].taumfoil = vector();
	magnet[n].taummagnet = vector();
	magnet[n].arrTau = arrow({
		color: magnet[n].getColor()
	},'noadd');
	magnet[n].arrTauH = arrow({
		color: 0xa52a2a
	},'noadd');
	magnet[n].arrTaufoil = arrow({
		color: 0xA52A2A
	},'noadd');
	magnet[n].arrTaumagnet = arrow({
		color: 0xA52A2A
	},'noadd');

	magnet[n].fdrag = vector();
	/*
	magnet[n].arrFdrag = arrow({
		//color: magnet[n].getColor()
	},'noadd');
	//*/

	magnet[n].CM = sphere({
		radius: magnet[n].getRadius()/20,
		color: magnet[n].getColor()
	},'noadd');
	magnet[n].add(magnet[n].CM);

	system.add(magnet[n]);
	system.add(magnet[n].mu);

	system.add(magnet[n].arrF);
	system.add(magnet[n].arrFfoil);
	system.add(magnet[n].arrFmagnet);

	system.add(magnet[n].arrTau);
	system.add(magnet[n].arrTauH);
	system.add(magnet[n].arrTaufoil);
	system.add(magnet[n].arrTaumagnet);

	//system.add(magnet[n].arrFdrag);
}
system.mu_s /= magnet.length;
system.mu_k /= magnet.length;
//}}}
/***
!!!! Center of Mass
***/
//{{{
let CM = sphere({
	radius: 0.001,
	opacity: 0.5,
	color: 0xffff00
});
CM.arrV = arrow({
	color: 0x0000ff
},'noadd');
CM.arrA = arrow({
	color: 0x00ff00
},'noadd');
CM.arrTau = arrow({
	color: 0xff0000
},'noadd');
CM.add(CM.arrV).add(CM.arrA).add(CM.arrTau);
let calculateCM = function(){
	let tmpV = vector();
	CM.position.copy(battery.position).multiplyScalar(battery.mass);
	CM.position.add(
		//battery.anode.localToWorld(tmpV.copy(
			battery.anode.position
		//))
	).multiplyScalar(battery.anode.mass);
	for(let n=0,N=magnet.length; n<N; n++){
		CM.position.add(
			tmpV.copy(magnet[n].position).multiplyScalar(magnet[n].mass)
		);
	}
	CM.position.multiplyScalar(1/CM.mass);
};

let Center = sphere({
	radius: 0.001,
	opacity: 0.5,
	color: 0xffffff
});
let calculateCenter = () => {
	Center.position.set(0,0,0);
	for(let n=0,N=magnet.length; n<N; n++){
		Center.position.add(magnet[n].position);
	}
	Center.position.multiplyScalar(1/magnet.length);
};
//}}}
/***
!!!! Magnet fields
***/
//{{{
let magneticFieldFoil = [], magneticFieldMagnet = [];
for(let n=0,N=magnet.length; n<N; n++){
	magneticFieldFoil[n] = vectorField.create(null,null,{
		color: magnet[n].getColor(),
		visible: false
	});
	system.add(magneticFieldFoil[n]);
	magneticFieldMagnet[n] = vectorField.create(null,null,{
		color: magnet[n].getColor(),
		visible: false
	});
	system.add(magneticFieldMagnet[n]);
}
let calculateMagneticField = (n,show) => {
	let t_M = performance.now();
	magneticFieldFoil[n].calculateField(
		currentIteratorFoil,
		function(dV,B){
			magnet[n].mu.fieldAt(dV.center,B);
			return B;
		}
	);
console.log('\tMagnetic field foil',n,'calculated in',Math.round(performance.now()-t_M),'ms.');

	t_M = performance.now();
	innerIteratorMagnet[n].position.copy(magnet[n].mu.center);
	innerIteratorMagnet[n].setAxis(magnet[n].mu.axis);
	let tmpB = vector();
	magneticFieldMagnet[n].calculateField(
		innerIteratorMagnet[n],
		function(dV,B,nf){
			magnet[n].mu.fieldAt(dV.center,B);
			B.multiplyScalar(-1);
			/*
			magnet[n].mu.fieldAt(dV.center,B,undefined,function(r,dV,dB,ns){
				if(n && nf===10) arrow({
					position: r,
					axis: tmpB.copy(dV.center).sub(r),
					opacity: 0.3,
					color: (dB.dot(magnet[n].mu.moment)<0 ? 0x777777 : 0xffff00)
				});
			});
			//*/
			return B;
		}
	);
console.log('\tMagnetic field magnet',n,'calculated in',Math.round(performance.now()-t_M),'ms.');

	t_M = performance.now();
	let max = 400*Math.max(magneticFieldFoil[n].max,magneticFieldMagnet[n].max);
	magneticFieldFoil[n].normalize(max);
	magneticFieldMagnet[n].normalize(max);

	if(show === true || show === false){
		magneticFieldFoil[n].show(show);
		magneticFieldMagnet[n].show(show);
	}
console.log('\tMagnetic fields',n,'normalized in',Math.round(performance.now()-t_M),'ms.');
};
//}}}
/***
!!!! Current field
***/
//{{{
let innerIteratorMagnet = [
	CartesianIterator.create({
		zstart: -magnet[0].getLength()/4,
		zend: -magnet[0].getLength()/4,
		ystart: -magnet[0].getRadius()/4,
		yend: magnet[0].getRadius()/4,
		xstart: 0,
		xend: magnet[0].getRadius(),
		Nx: 5,
		Ny: 5,
		Nz: 1
	}),
	CartesianIterator.create({
		zstart: -magnet[1].getLength()/4,
		zend: -magnet[1].getLength()/4,
		ystart: -magnet[1].getRadius()/4,
		yend: magnet[1].getRadius()/4,
		xstart: -magnet[1].getRadius(),
		xend: 0,
		Nx: 5,
		Ny: 5,
		Nz: 1
	})
];
let currentIteratorFoil = CartesianIterator.create({
	xstart: battery.position.x-(battery.getLength()/2+magnet[1].getLength()/4),
	xend: battery.position.x+battery.getLength()/2+
			battery.anode.getLength()+magnet[0].getLength()/4,
	ystart: -magnet[0].getRadius()/4,
	yend: magnet[0].getRadius()/4,
	zstart: -magnet[0].getRadius(),
	zend: -magnet[0].getRadius()-foil.getDepth(),
	Nx: 10,
	Ny: 5,
	Nz: 1
});

foil.calculateResistance(currentIteratorFoil);
console.log('foil.R=',foil.R,'foil.getDepth()=',foil.getDepth());
let R = foil.R+battery.R;
for(let n=0,N=magnet.length; n<N; n++){
	magnet[n].calculateResistance(innerIteratorMagnet[n]);
	R += magnet[n].R;
console.log('magnet['+n+'].R =',magnet[n].R,'total R =',R,magnet[n].mu.axis);
}
let I = battery.V/R/currentIteratorFoil.getSteps(1);
console.log('I =',I);

let t_I = performance.now(),
	currentFieldFoil = vectorField.create(
		currentIteratorFoil,
		function(dV,dI){
			return dI.copy(dV.dx[0]).multiplyScalar(-I);
		}
	);
currentFieldFoil.setColor(0x00ff00);
currentFieldFoil.normalize(currentFieldFoil.max*200);
system.add(currentFieldFoil);

let currentFieldMagnet = [];
for(let n=0,N=magnet.length; n<N; n++){
	innerIteratorMagnet[n].position.copy(magnet[n].mu.center);
	innerIteratorMagnet[n].setAxis(magnet[n].mu.axis);
	currentFieldMagnet[n] = vectorField.create(
		innerIteratorMagnet[n],
		function(dV,dI){
//console.log('dV.dx=',dV.dx);
			return dI.copy(dV.dx[0]).multiplyScalar(I);
		}
	)
	currentFieldMagnet[n].show(false);
	currentFieldMagnet[n].setColor(0x00ff00);
	currentFieldMagnet[n].normalize(currentFieldFoil.max*200);
	system.add(currentFieldMagnet[n]);
}
console.log('\tCurrent field calculated in',Math.round(performance.now()-t_I),'ms.');
//}}}
/***
!!!! Force field
***/
//{{{
let forceFieldFoil = [], forceFieldMagnet = [];
for(let n=0,N=magnet.length; n<N; n++){
	forceFieldFoil[n] = vectorField.create(null,null,{
		//color: magnet[n].getColor(),
		visible: false
	});
	system.add(forceFieldFoil[n]);
	forceFieldMagnet[n] = vectorField.create(null,null,{
		//color: magnet[n].getColor(),
		visible: false
	});
	system.add(forceFieldMagnet[n]);
}
let calculateForceField = (n,show) => {
	let tmpV = vector(), t_F = performance.now();
	magnet[n].pivot.copy(magnet[n].position);
	magnet[n].pivot.z -= magnet[n].getRadius();

	magnet[n].fm.set(0,0,0);
	magnet[n].fmfoil.set(0,0,0);
	magnet[n].fmmagnet.set(0,0,0);
	magnet[n].taum.set(0,0,0);
	magnet[n].taumfoil.set(0,0,0);
	magnet[n].taummagnet.set(0,0,0);

	forceFieldFoil[n].calculateField(
		currentIteratorFoil,
		function(dV,f,i){
			let I = currentFieldFoil.getField(i),
				B = magneticFieldFoil[n].getField(i);
			magnet[n].fmfoil.add(f.copy(I).cross(B));
			magnet[n].taumfoil.add(
				tmpV.copy(dV.center).sub(magnet[n].pivot).cross(f)
			);
			return f;
		}
	);
	magnet[n].fmfoil.multiplyScalar(-1);
	magnet[n].taumfoil.multiplyScalar(-1);

	innerIteratorMagnet[n].position.copy(magnet[n].mu.center);
	//innerIteratorMagnet[n].setAxis(magnet[n].mu.axis);
	forceFieldMagnet[n].calculateField(
		innerIteratorMagnet[n],
		function(dV,f,i){
			let I = currentFieldMagnet[n].getField(i),
				B = magneticFieldMagnet[n].getField(i);
			magnet[n].fmmagnet.add(f.copy(I).cross(B));
			magnet[n].taummagnet.add(
				tmpV.copy(dV.center).sub(magnet[n].pivot).cross(f)
			);
			return f;
		}
	);
	magnet[n].fmmagnet.multiplyScalar(-1);
	magnet[n].taummagnet.multiplyScalar(-1);

	magnet[n].fm.copy(magnet[n].fmfoil).add(magnet[n].fmmagnet);
console.log('magnet['+n+'].fmfoil('+magnet[n].fmfoil.length()+') / fmmagnet('+magnet[n].fmmagnet.length()+') = '+(magnet[n].fmfoil.length()/magnet[n].fmmagnet.length()));

	magnet[n].taum.copy(magnet[n].taumfoil).add(magnet[n].taummagnet);
console.log('magnet['+n+'].taumfoil('+magnet[n].taumfoil.length()+') / taummagnet('+magnet[n].taummagnet.length()+') = '+(magnet[n].taumfoil.length()/magnet[n].taummagnet.length()));
	tmpV.copy(magnet[n].position).sub(magnet[n].pivot).normalize();
	magnet[n].taumH.copy(magnet[n].taum).sub(tmpV.multiplyScalar(magnet[n].taum.dot(tmpV))).multiplyScalar(n?-1:1);

	let max = 400*Math.max(forceFieldFoil[n].max,forceFieldMagnet[n].max),
		invmax = 1/max;
	forceFieldFoil[n].normalize(max);
	forceFieldMagnet[n].normalize(max);

	magnet[n].fm.z = 0; 	// suppress the z-dir motion
	if(show === true || show === false)
		forceFieldFoil[n].show(show);
	magnet[n].arrF.position.copy(magnet[n].position);
	magnet[n].arrFfoil.position.copy(magnet[n].position);
	magnet[n].arrFmagnet.position.copy(magnet[n].position);
	magnet[n].arrF.setAxis(
		tmpV.copy(magnet[n].fm)
			.multiplyScalar(invmax)
	);
	magnet[n].arrFfoil.setAxis(
		tmpV.copy(magnet[n].fmfoil)
			.multiplyScalar(invmax)
	);
	magnet[n].arrFmagnet.setAxis(
		tmpV.copy(magnet[n].fmmagnet)
			.multiplyScalar(invmax)
	);

	magnet[n].arrTau.position.copy(magnet[n].position);
	magnet[n].arrTauH.position.copy(magnet[n].position);
	magnet[n].arrTaufoil.position.copy(magnet[n].position);
	magnet[n].arrTaumagnet.position.copy(magnet[n].position);
	invmax *= 10;
	magnet[n].arrTau.setAxis(
		tmpV.copy(magnet[n].taum)
			.multiplyScalar(invmax)
	);
	magnet[n].arrTauH.setAxis(
		tmpV.copy(magnet[n].taumH)
			.multiplyScalar(invmax)
	);
	magnet[n].arrTaufoil.setAxis(
		tmpV.copy(magnet[n].taumfoil)
			.multiplyScalar(invmax)
	);
	magnet[n].arrTaumagnet.setAxis(
		tmpV.copy(magnet[n].taummagnet)
			.multiplyScalar(invmax)
	);
	console.log('\tForce field',n,'calculated in',Math.round(performance.now()-t_F),'ms.');
};
//}}}
/***
!!!! UI for field status
***/
//{{{
let radioMagnet = [
	document.getElementsByName("mu1"),
	document.getElementsByName("mu0")
];
let checkMagnetOrientation = n => {
	if(! radioMagnet[n] || radioMagnet[n].length===0) return;
	if(magnet[n].mu.axis.dot(battery.axis) < 0){
		radioMagnet[n][0].checked = true;
		radioMagnet[n][1].checked = false;
	}else{
		radioMagnet[n][0].checked = false;
		radioMagnet[n][1].checked = true;
	}
}
let getRadioMagnetValue = n => {
	return ! radioMagnet[n]
		? 0
		: (radioMagnet[n][0].checked
			? radioMagnet[n][0].value
			: (radioMagnet[n][1].checked
				? radioMagnet[n][1].value
				: 0));
}
let magnetChanged = n => {
	let dir = getRadioMagnetValue(n);
	if (dir && magnet[n].mu.axis.dot(battery.axis)*dir < 0) {
		magnet[n].mu.flip(true);
		magnet[n].mu.iterator.setAxis(magnet[n].mu.axis);
		//magnet[n].mu.iterator.slicedPoints(true);
console.log('magnet['+n+'] flipped.');
		return true;
	}
	return false;
};
//}}}
/***
!!!! Opacity
***/
//{{{
txtOpacity.onchange = () => {
	let op = this.value;
	if(battery.getOpacity() !== op){
		battery.setOpacity(op);
		battery.anode.setOpacity(op);
		//for(let n=0,N=magnet.length; n<N; n++){
		//	magnet[n].setOpacity(op);
		//}
	}
}
txtOpacity.value = 0.7;
//}}}
/***
!!!! Data Plot
***/
//{{{
//dataPlot[0].setYTitle(
//	(config.browser.isIE ? 'Theta~~p~~ (&deg;)' : '\\(\\theta_p\\ (^\\circ)\\)')
//).setTitle('Theta vs Time');
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');

//labelPlot[0].innerHTML = '\\(\\theta_p\\) (&deg;):';
labelPlot[0].innerHTML = '\\(v (\\text{m/s})\\):';
labelPlot[1].innerHTML = '\\(v (\\text{m/s})\\):';

activateDAQChannels(2);
attachDAQBuffer(0,0);
attachDAQBuffer(1,1);
//}}}
/***
!! Camera view and others
***/
//{{{
scene.camera.position.set(0,0,50);
//scene.camera.lookAt(vector(0,0,-1));
chkGravity.checked = true;
chkGravity.disabled = true;
//}}}
/***
!! Initialization
<<<
Variables {{{__r[], __v[], __a[]}}} are arrays to store the position, velocity, and acceleration, either linear or angular, of each of the moving objects in the battery-magnets system. The first two elements are the corresponding information for the whole system, the next two elements are those for the magnets attached to the two ends of the battery, respectively, with the anode end preceding the cathode end.
<<<
***/
//{{{
let __r = [
		vector(), vector(),		// linear, angular position of CM
		vector(), vector(),		// linear, angular position of magnet[0]
		vector(), vector()		// linear, angular position of magnet[1]
	],
	__v = [
		vector(), vector(),		// linear, angular velocity of CM
		vector(), vector(),		// linear, angular velocity of magnet[0]
		vector(), vector()		// linear, angular velocity of magnet[1]
	],
	__a = [
		vector(), vector(),		// linear, angular acceleration of CM
		vector(), vector(),		// linear, angular acceleration of magnet[0]
		vector(), vector()		// linear, angular acceleration of magnet[1]
	];
//}}}
/***
!!!! scene.init()
***/
//{{{
scene.init = () => {
console.log('scene.init()');
	let tmpV = vector(1,0,0);
	system.angle.value = system.angle.length();
	system.angle.normalize();
	if(system.angle.value){
		rotateSystem(system.angle,-system.angle.value);
		/*
		sysetm.rotateWorld(system.angle,-system.angle.value);
		battery.rotateWorld(system.angle,-system.angle.value);
		battery.anode.rotateWorld(system.angle,-system.angle.value);
		currentFieldFoil.rotateWorld(system.angle,-system.angle.value);
		//*/
	}

	system.position.set(0,0,0);

	battery.mass = 0.01;		// 10g
	battery.position.set(0,0,0);
	battery.velocity.set(0,0,0);
	currentFieldFoil.position.set(0,0,0);

	battery.anode.mass = 0.0005	// 0.5g
	battery.anode.position.set(
		(battery.getLength()+battery.anode.getLength())/2,0,0
	);
	CM.mass = battery.mass+battery.anode.mass;
	let rho = 5.8*1e3			// kg/m^3, http://www.teslar-tech.com.tw/magnet-feature.html
	let h = 1000;
	for(let n=0,N=magnet.length; n<N; n++){
		magnet[n].position.set(0,0,0);
		magnet[n].velocity.set(0,0,0);
		magneticFieldFoil[n].position.set(0,0,0);
		forceFieldFoil[n].position.set(0,0,0);
		//magnet[n].angle.value = magnet[n].angle.length();
		//magnet[n].angle.normalize();
		/*
		if(system.angle.value){
			magnet[n].rotateWorld(system.angle,-system.angle.value);
			magnet[n].mu.rotateWorld(system.angle,-system.angle.value);
			magneticFieldFoil[n].rotateWorld(system.angle,-system.angle.value);
			magneticFieldMagnet[n].rotateWorld(system.angle,-system.angle.value);
			forceFieldFoil[n].rotateWorld(system.angle,-system.angle.value);
			forceFieldMagnet[n].rotateWorld(system.angle,-system.angle.value);
		}
		//*/
		magnet[n].angle.set(0,0,0);
		magnet[n].angle.value = 0;
		checkMagnetOrientation(n);
		if(n===0)
			magnet[n].position.x += (battery.getLength()+magnet[n].getLength())/2+
				battery.anode.getLength();
		else
			magnet[n].position.x -= (battery.getLength()+magnet[n].getLength())/2;
		magnet[n].setAxis(magnet[n].getLength()*(n?-1:1),0,0);
		magnet[n].mu.setAxis(magnet[n].axis);
		magnet[n].mu.setCenter(magnet[n].position);
		magnet[n].mu.iterator.position.copy(magnet[n].mu.center);
		magnet[n].mu.iterator.setAxis(magnet[n].axis);
		//magnet[n].mu.iterator.slicedPoints(true);

		let mh = magnet[n].position.y - magnet[n].getRadius();
		if(mh < h) h = mh;
		magnet[n].mass = Math.PI*Math.pow(magnet[n].getRadius(),2)*
			magnet[n].getLength()*rho;
		magnet[n].inertia = 0.5*magnet[n].mass*Math.pow(magnet[n].getRadius(),2);
		CM.mass += magnet[n].mass;

		currentFieldMagnet[n].position.copy(magnet[n].position);
		magneticFieldMagnet[n].position.copy(battery.position);
		forceFieldMagnet[n].position.copy(battery.position);
		magnet[n].arrF.position.copy(magnet[n].position);
		magnet[n].arrFfoil.position.copy(magnet[n].position);
		magnet[n].arrFmagnet.position.copy(magnet[n].position);
		magnet[n].arrTau.position.copy(magnet[n].position);
		magnet[n].arrTaufoil.position.copy(magnet[n].position);
		magnet[n].arrTaumagnet.position.copy(magnet[n].position);
	}
	foil.position.z = h-foil.getDepth()/2;
	system.angle.set(0,0,0);
	system.angle.value = 0;
	/*
	tmpV.set(
		magnet[0].position.z-magnet[1].position.z,
		(magnet[0].getRadius()-magnet[1].getRadius())/2,
		0
	);
	//*/
	calculateCM();
	calculateCenter();
	CM.arrV.setLength(0);
	CM.arrA.setLength(0);
	CM.arrTau.setLength(0);
	CM.inertia = battery.mass*Math.pow(battery.getLength(),2)/12+
			battery.mass*tmpV.copy(CM.position).sub(battery.position).lengthSq()+
		battery.anode.mass*Math.pow(battery.anode.getLength(),2)/12+
			battery.anode.mass*tmpV.copy(CM.position).sub(battery.anode.position).lengthSq();
	for(let n=0,N=magnet.length; n<N; n++){
		CM.inertia += magnet[n].mass*Math.pow(magnet[n].getLength(),2)/12+
			magnet[n].mass*tmpV.copy(CM.position).sub(magnet[n].position).lengthSq();
	}
console.log('CM.mass=',CM.mass,'CM.inertia=',CM.inertia);

	// linear, angular position of the system
	__r[0].copy(CM.position);
	__r[1].copy(system.angle);
	// linear, angular velocity of the system
	__v[0].set(0,0,0);
	__v[1].set(0,0,0);
	// linear, angular acceleration of the system
	__a[0].set(0,0,0);
	__a[1].set(0,0,0);

	// Angular position, velocity, acceleration of the magnets.
	for(let n=0,nx2=0,N=magnet.length; n<N; n++,nx2+=2){
		// linear, angular position of magnet[n]
		__r[2+nx2].copy(magnet[n].position);
		__r[2+nx2+1].copy(magnet[n].angle);
		// linear, angular velocity of magnet[n]
		__v[2+nx2].set(0,0,0);
		__v[2+nx2+1].set(0,0,0);
		// linear, angular acceleration of magnet[n]
		__a[2+nx2].set(0,0,0);
		__a[2+nx2+1].set(0,0,0);
	}
};
//}}}
/***
!!!! scene.starting
***/
//{{{
scene.starting = () => {
	if(!magneticFieldMagnet.calculated){
		for(let n=0,N=magnet.length; n<N; n++){
			calculateMagneticField(n,chkShowField.checked);
			calculateForceField(n,chkShowForce.checked);
		}
		magneticFieldMagnet.calculated = true;
	}
}
//}}}
/***
!!!! checkCameraView
***/
//{{{
let CameraView = document.getElementsByName('CameraView');
CameraView.current = 'none';
for(let n=0,N=CameraView.length; n<N; n++){
	CameraView[n].checked = (CameraView[n].value === 'top');
}
let checkCameraView = () => {
	for(let n=0,N=CameraView.length; n<N; n++){
		if(CameraView[n].checked && CameraView[n].value !== CameraView.current){
			if(CameraView[n].value === 'top'){
				// Top view
				scene.camera.position.set(0,0,scene.camera.position.length());
				scene.setUp(0,1,0);
				scene.camera.lookAt(vector(0,0,-1));
			}else if(CameraView[n].value === 'side'){
				// Side view
				scene.camera.position.set(
					0,
					-scene.camera.position.length(),
					scene.camera.position.length()/40
				);
				scene.setUp(0,0,1);
				scene.camera.lookAt(vector(0,1,0));
			}else if(CameraView[n].value === 'end'){
				// End view
				scene.camera.position.set(scene.camera.position.length(),0,0);
				scene.setUp(0,0,1);
				scene.camera.lookAt(vector(-1,0,0));
			}
			CameraView.current = CameraView[n].value;
			break;
		}
	}
}
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	cartesian.show(chkShowXYZ.checked);
	checkCameraView();
	CM.visible = Center.visible = chkCM.checked;
	if(CM.visible){
		CM.arrV.visible = chkShowVelocity.checked;
		CM.arrA.visible = chkShowAcceleration.checked;
		CM.arrTau.visible = chkShowAcceleration.checked;
	}
	getTrailParam(battery);
	let changed = false;
	for(let n=0,N=magnet.length; n<N; n++){
		if(magnetChanged(n)){
			magneticFieldFoil[n].flip();
			magneticFieldMagnet[n].flip();
			forceFieldFoil[n].flip();
			forceFieldMagnet[n].flip();
			magnet[n].fm.multiplyScalar(-1);
			magnet[n].fmfoil.multiplyScalar(-1);
			magnet[n].fmmagnet.multiplyScalar(-1);
			magnet[n].taum.multiplyScalar(-1);
			magnet[n].taumfoil.multiplyScalar(-1);
			magnet[n].taummagnet.multiplyScalar(-1);
			magnet[n].arrF.flip();
			magnet[n].arrF.position.copy(magnet[n].position);
			magnet[n].arrFfoil.flip();
			magnet[n].arrFfoil.position.copy(magnet[n].position);
			magnet[n].arrFmagnet.flip();
			magnet[n].arrFmagnet.position.copy(magnet[n].position);
			magnet[n].arrTau.flip();
			magnet[n].arrTau.position.copy(magnet[n].position);
			magnet[n].arrTaufoil.flip();
			magnet[n].arrTaufoil.position.copy(magnet[n].position);
			magnet[n].arrTaumagnet.flip();
			magnet[n].arrTaumagnet.position.copy(magnet[n].position);
			changed = true;
		}
		getTrailParam(magnet[n]);
		magneticFieldFoil[n].show(chkShowField.checked);
		magneticFieldMagnet[n].show(chkShowField.checked);
		magnet[n].CM.visible = chkCM.checked;
		forceFieldFoil[n].show(chkShowForce.checked);
		forceFieldMagnet[n].show(chkShowForce.checked);
		magnet[n].arrF.visible = magnet[n].arrFfoil.visible =
			magnet[n].arrFmagnet.visible = chkShowForce.checked;
		magnet[n].arrTau.visible = magnet[n].arrTauH.visible = magnet[n].arrTaufoil.visible =
			magnet[n].arrTaumagnet.visible = chkShowTorque.checked;
		currentFieldMagnet[n].show(chkShowCurrent.checked);
	}
	currentFieldFoil.show(chkShowCurrent.checked);
	return changed;
}
//}}}
/***
!! Acceleration calculations
***/
/***
!!! calcA(\(r[],v[],t,a[]\))
<<<
Calculate both linear and angular acceleration of the system. The argument \(r[0]\) shall store the linear position of the system, which can be used to calculate the positions of magnets at both ends of the battery, then calculate the magnetic forces exerting on the magnets. Those forces will determine both the linear and angular acceleration of the system.
<<<
***/
//{{{
let calcA = (r,v,t,a) => {
	// a[0] is the linear acceleration, while a[1] is the
	// angular acceleration of the system
	a[0].set(0,0,0);
	a[1].set(0,0,0);
	let fmagnet = vector(), torque = vector(), vnorm = vector(),
		// normal, static, kinetic friction, rolling drag and magnetic force
		fN = 0,	fs = 0, fk = 0, fr = 0, fm = 0;
//}}}
/***
!!!! force and torque of each magnet
***/
//{{{
	for(let n=0,nx2=0,N=magnet.length; n<N; n++,nx2+=2){
		// Find the direction of the total velocity: rolling+translation
		//let vn = v[2+nx2+1].length() || v[2+nx2].length();
		vnorm = magnet[n].fm.clone().normalize();
		//vnorm = vn
		//	?	vector(0,0,1).cross(v[2+nx2+1]).multiplyScalar(-1).add(v[2+nx2]).normalize()
		//	:	magnet[n].fm.clone().normalize();
		// --------------------------------------------------------------------
		// forces
		fN = CM.mass*scene.g.value/magnet.length; 		// normal force on magnet
		fs = magnet[n].mu_s*fN;					// max static friction
		fk = magnet[n].mu_k*fN;					// kinetic friction
		fr = magnet[n].mu_r*fN;					// rolling drag
		// --------------------------------------------------------------------
		//		To be correct, we need to re-calculate the magnetic force
		//		magnet[n].fm here, or at least find the slightly different
		//		orientation of it due to slightly different position.
		//		We are not doing that yet......
		//											Vincent 2020/09/06
		// --------------------------------------------------------------------
		// Find the slight angle to rotate the force
		/*
		let r1 = magnet[n].position.clone().sub(CM.position),
			r2 = r[2+nx2].clone().sub(r[0]),
			dr = r2.clone().sub(r1).sub(tmpV.copy(r[0]).sub(CM.position)),
			da = dr.length()/r1.length();
		// Find the axis of rotation
		tmpV.copy(r1).cross(r2).normalize();
		//*/
		fm = magnet[n].fm.length();
			//fm = vn
			//	?	Math.abs(magnet[n].fm.dot(vnorm))	// magnetic force component
			//											// parallel to the velocity
			//	:	magnet[n].fm.length();				// or the force itself
		magnet[n].fdrag.copy(vnorm);
		if(fm > fs+fr){
//console.log('kinetic');
			// magnetic force larger than maximum static friction+rolling drag,
			// magnet[n] shall slide and roll
			magnet[n].fdrag.multiplyScalar(-fr-fk);
			magnet[n].sliding = true;
			fmagnet.copy(magnet[n].fdrag).add(magnet[n].fm);
		}else{
//console.log('static');
			// magnetic force smaller than maximum static friction+rolling drag,
			// force shall be balanced by static friction and rolling drag.
			// magnet[n] shall not slide and just roll
			magnet[n].fdrag.multiplyScalar(-fm);
			magnet[n].sliding = false;
			fmagnet.copy(vnorm).multiplyScalar(-fr).add(magnet[n].fm);
		}
//}}}
/***
!!!! linear and angular acceleration of each magnet
***/
//{{{
		// linear acceleration of magnet[n]
		a[2+nx2].copy(magnet[n].fdrag).add(magnet[n].fm)
			.multiplyScalar(1/magnet[n].mass);
		// angular acceleraton of manget[n]
		a[2+nx2+1]
			.copy(torque.set(0,0,-magnet[n].getRadius()).cross(magnet[n].fdrag))
			//.add(torque.set(0,0,magnet[n].getRadius()).cross(magnet[n].fm))
			.multiplyScalar(1/magnet[n].inertia);
//}}}
/***
!!!! force and torque added to the whole system
***/
//{{{
		// translational force of CM
		a[0].add(fmagnet);
		// torque on CM due to that force
		//a[1].add(torque.copy(magnet[n].position).sub(CM.position).cross(fmagnet));
		a[1].add(torque.copy(r[2+nx2]).sub(r[0]).cross(fmagnet));
//console.log('torque['+n+']=',torque.length(),torque);
	}
//}}}
/***
!!!! linear and angular acceleration of the whole system
***/
//{{{
	// linear acceleration of CM
	a[0].multiplyScalar(1/CM.mass);
	// angular acceleration of CM
	a[1].multiplyScalar(1/CM.inertia);

	return a;
}
//}}}
/***
!! Update functions
***/
/***
!!!! moveSystem(dr)
***/
//{{{
let moveSystem = dr => {
	//system.position.add(dr);
	///*
	battery.position.add(dr);
	battery.anode.position.add(dr);
	currentFieldFoil.position.add(dr);
	for(let n=0,N=magnet.length; n<N; n++){
		currentFieldMagnet[n].position.add(dr);
		magnet[n].position.add(dr);
		magnet[n].arrF.position.add(dr);
		magnet[n].arrFfoil.position.add(dr);
		magnet[n].arrFmagnet.position.add(dr);
		magnet[n].arrTau.position.add(dr);
		magnet[n].arrTaufoil.position.add(dr);
		magnet[n].arrTaumagnet.position.add(dr);
		magnet[n].mu.position.add(dr);
		magneticFieldFoil[n].position.add(dr);
		magneticFieldMagnet[n].position.add(dr);
		forceFieldFoil[n].position.add(dr);
		forceFieldMagnet[n].position.add(dr);
	}
	//*/
}
//}}}
/***
!!!! rotateSystem()
***/
//{{{
let rotateSystem = (axis,angle) => {
	//system.rotateWorld(axis,angle,CM.position);
	///*
	battery.rotateWorld(axis,angle,CM.position);
	battery.anode.rotateWorld(axis,angle,CM.position);
	// Rotate the system by angle with respect to the axis.
	for(let n=0,N=magnet.length; n<N; n++){
		magnet[n].rotateWorld(axis,angle,CM.position);
		magnet[n].mu.rotateWorld(axis,angle,CM.position);
		magneticFieldFoil[n].rotateWorld(axis,angle,CM.position);
		magneticFieldMagnet[n].rotateWorld(axis,angle,CM.position);
		magnet[n].arrF.rotateWorld(axis,angle,CM.position);
		magnet[n].arrFfoil.rotateWorld(axis,angle,CM.position);
		magnet[n].arrFmagnet.rotateWorld(axis,angle,CM.position);
		magnet[n].arrTau.rotateWorld(axis,angle,CM.position);
		magnet[n].arrTaufoil.rotateWorld(axis,angle,CM.position);
		magnet[n].arrTaumagnet.rotateWorld(axis,angle,CM.position);
		magnet[n].fm.applyAxisAngle(axis,angle);
		magnet[n].fmfoil.applyAxisAngle(axis,angle);
		magnet[n].fmmagnet.applyAxisAngle(axis,angle);
		forceFieldFoil[n].rotateWorld(axis,angle,CM.position);
		forceFieldMagnet[n].rotateWorld(axis,angle,CM.position);
		currentFieldMagnet[n].rotateWorld(axis,angle,CM.position);
	}
	currentFieldFoil.rotateWorld(axis,angle,CM.position);
	//*/
}
//}}}
/***
!!!! updateWithoutRolling
***/
//{{{
let updateWithoutRolling = () => {
	// update linear status
	let tmpV = __r[0].clone().sub(CM.position);
	CM.position.copy(__r[0]);
	moveSystem(tmpV);

	// update angular status
	tmpV.copy(__r[1]).sub(system.angle);
	system.angle.copy(__r[1]);
	tmpV.value = tmpV.length();
	rotateSystem(tmpV.normalize(),tmpV.value);

	//update velocities, rotation+translation
	tmpV.copy(battery.position).sub(CM.position);		// r of rotation
	battery.velocity.copy(__v[1]).cross(tmpV).normalize().multiplyScalar(
		tmpV.length()*__v[1].length()					// r * omega
	).add(__v[0]);

	for(let n=0,nx2=0,N=magnet.length; n<N; n++,nx2+=2){
		tmpV.copy(magnet[n].position).sub(CM.position);		// r of rotation
		magnet[n].velocity.copy(__v[1]).cross(tmpV)			// omega x r
			.add(__v[0]);									// add v of CM
		// update magnet linear position and velocity for next calculations
		__r[2+nx2].copy(magnet[n].position);
		__v[2+nx2].copy(magnet[n].velocity);
	}
}
//}}}
/***
!!!! updateWithRolling
***/
//{{{
let updateWithRolling = () => {
	let tmpV = vector(), dr = vector(), dangle = vector();
	for(let n=0,nx2=0,N=magnet.length; n<N; n++,nx2+=2){
		dr.add(tmpV.copy(__r[2+nx2]).sub(magnet[n].position));
		// Calculate angular increment
		tmpV.copy(__r[2+nx2+1]).sub(magnet[n].angle);
		tmpV.value = tmpV.length();
		// Calculate the distance that magnet[n] has just rolled over.
		let ds = magnet[n].getRadius()*tmpV.value;
		// Calculate the angle of system rotation that matches this distance
		ds /= tmpV.copy(magnet[n].position).sub(CM.position).length();
		dangle.add(tmpV.cross(magnet[n].fm).normalize().multiplyScalar(ds));
		magnet[n].angle.copy(__r[2+nx2+1]);
		dr.add(tmpV.copy(magnet[n].fm).normalize().multiplyScalar(ds));
	}
	// Move the system by dr.
	CM.position.add(dr);
	__r[0].copy(CM.position);
	moveSystem(dr);
	// Rotate the system by dangle.
	system.angle.add(dangle);
	dangle.value = dangle.length();
	rotateSystem(dangle.normalize(),dangle.value);
	for(let n=0,nx2=0,N=magnet.length; n<N; n++,nx2+=2){
		magnet[n].velocity.copy(
			tmpV.set(0,0,-magnet[n].getRadius()).cross(__v[2+nx2+1])
		);
		__r[2+nx2].copy(magnet[n].position);
		__v[2+nx2].copy(magnet[n].velocity);
	}
}
//}}}
/***
!!! scene.update
***/
//{{{
scene.update = (t_cur,dt) => {
	// calculate next position using 4th order Runge-Kutta mehtod
	$tw.numeric.ODE.nextValue(
		__r,__v,calcA,t_cur,dt,__a
	);

	if(chkRolling.checked)
		updateWithRolling();
	else
		updateWithoutRolling();

	let tmpV = vector();
	CM.arrV.setAxis(tmpV.copy(__v[0]).multiplyScalar(0.1));
	CM.arrA.setAxis(tmpV.copy(__a[0]).multiplyScalar(0.1));
	CM.arrTau.setAxis(tmpV.copy(__a[1]).multiplyScalar(0.1));
	calculateCenter();
}
//}}}
|editable multilined spreadsheet|k
|Development Log|c
| 日期 | 描述 |h
| 2021/9/4<br>(=dow()) |* 圓柱磁鐵【內部】的磁場計算,似乎只有少數切割參數可以算出預期的結果,滿奇怪的!|
| 2020/9/4<br>(=dow()) |* bug<br>** 旋轉過後力的方向有錯<br>*** 猜測這是磁矩方向有改變,但是相對位置並沒有改變所造成的<br>**** 計算磁場的時後把磁矩轉回初始角度,結果還是不對<br>**** 這個還要再想想<br>***** @@color:red;暫時解決,就是「不」重算,只是把方向反過來而已。@@<br>** 用 group 把東西全部包起來,應該不會改變磁矩和和相對位置之間的關係了吧!<br>*** 力的方向還是一樣的錯誤<br>*** 運動過程看起來沒問題,但是<br>**** 磁鐵不會留下軌跡,因為它們沒有改變位置<br>**** reset 不會轉回初始方向(這點我不理解,為何運動過程看起來是正常的?)|
|Circling Magnets Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|[ =chkShowXYZ] ''XYZ'' / <<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<html><input type="radio" name="CameraView" value="top">Top view / <input type="radio" name="CameraView" value="side">Side view / Opacity <input type="number" title="Opacity 0 to 1" id="txtOpacity" min="0" max="1" step="0.05" value="1" style="width:40px"></html> / [ =chkRolling] Rolling|<<tiddler "Magnetic Train Panel##Magnet Control">> / [ =chkShowField] field / [ =chkShowCurrent] Current|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Circling Magnets Creation]] [[Circling Magnets Initialization]] [[Circling Magnets Iteration]]>>|
/***
!!!! calculateA( \(\vec r[],\vec v[],t\)[,\(\vec a[]\)] )
<<<
Calculates accelerations of the moving objects at positions \(r[]\) and with velocities \(v[],\) at time \(t.\)
<<<
***/
//{{{
let calculateA = (r,v,t,a) => {
	if(!a) a = [];
	let include_air_drag = chkAirDrag.checked && __d && __m;
	if(a.length===0){
		for(let n=0,N=r.length; n<N; n++){
			a[n] = vector(0,0,0).add(scene.g);
			if(include_air_drag)
				a[n].add(
					$tw.physics.airDragSphere(v[n],__d[n]).multiplyScalar(1/__m[n])
				);
		}
	}else{
		for(let n=0,N=r.length; n<N; n++){
			a[n].set(0,0,0).add(scene.g);
			if(include_air_drag)
				a[n].add(
					$tw.physics.airDragSphere(v[n],__d[n]).multiplyScalar(1/__m[n])
				);
		}
	}
	return a;
};
//}}}
/***
!!!! Initialize motion status
***/
//{{{
let __r = [], __v = [], __a = [], z_max = 0, d_mzx = 0;
let __d = null, __m = null, __e = [], __mu = [], __Y = [], __nu = [];
let d_max = 0;
scene.init = () => {
	prepareObjects();
	let elasticity = +txtElasticity.value;
	let speedloss = +txtSlidingSpeedLossRate.value;
	let Y = +txtYoungsModulus.value*1e9, nu = +txtPoissonsRatio.value;

	__r.length = __v.length = __a.length = particle_number;
	__e.length = __mu.length = __Y.length = __nu.length = particle_number;

	if(particle_size <= critical_size){
		for(let n=0; n<particle_number; n++){
			if(!__r[n]) __r[n] = vector();
			obj.set(n,{pos:__r[n].set(
				0.5+Math.random()*particle_size*10,
				0.5+Math.random()*particle_size*10,
				0.05+n*particle_size*1.5
			)});
			if(!__v[n]) __v[n] = vector();
			else __v[n].set(0,0,0);
			if(!__a[n]) __a[n] = vector();
			else __a[n].set(0,0,0);
			__e[n] = elasticity;
			__mu[n] = speedloss;
			__Y[n] = Y; __nu[n] = nu;
		}
	}else{
		let vmax = +document.getElementById('txtMaxSpeed').value;
		for(let n=0; n<particle_number; n++){
			if(!__r[n]) __r[n] = vector();
			obj[n].position.copy(__r[n].set(
				-0.5+Math.random(),-0.5+Math.random(),0.1+Math.random()*0.8
			));
			obj[n].arrV.position.copy(__r[n]);
			if(!__v[n]) __v[n] = vector();
			__v[n].set(Math.random(),Math.random(),Math.random())
					.multiplyScalar(vmax*(Math.random()>=0.5?1:-1));
			if(!__a[n]) __a[n] = vector();
			else __a[n].set(0,0,0);
			__e[n] = elasticity;
			__mu[n] = speedloss;
			__Y[n] = Y; __nu[n] = nu;
		}
	}
	__d = obj.__size__;
	__m = obj.__mass__;
	z_max = __r[__r.length-1].z;
	d_max = Math.max.apply(null,__d);
	adjust_dt(d_max,find_v_rms(__v));
}
let find_v_rms = v => {
	let sum = 0, count = 0;
	let vmaxSq = 2*9.8*z_max;
	for(let n=0,N=v.length; n<N; n++){
		let vsq = v[n].lengthSq();
		if(vsq <= vmaxSq){
			sum += vsq;
			count++;
		}
	}
	return count ? Math.sqrt(sum/count) : 0;
}
let adjust_dt = (d,v) => {
	if(v>0){
		let dt_max = d/v*0.5;
		if(scene.timeInterval()> dt_max) setdT(dt_max);
	}
}
//}}}
/***
!!!! Update Function
***/
//{{{
scene.update = (t_cur,dt) => {
	//let KE = 0, UE = 0; //P.set(0,0,0);
	//for(let n=0,N=__r.length; n<N; n++){
	//	KE += 0.5*__m[n]*__v[n].lengthSq();
	//	UE += __m[n]*9.8*__r[n].z;
		//P.add(p.copy(__v[n]).multiplyScalar(__m[n]));
	//}
	//arrP.setAxis(P);
	//recordData(scene.currentTime(),[scene.currentTime(),scene.currentTime()],[KE,KE+UE]);

	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.numeric.ODE.nextValue(
			__r,__v,calculateA,t_cur,dt,__a,adaptive,e0
		),
		 collisions = $tw.physics.checkCollisions(
			__r,__d,__m,__v,chkHertzianModel.checked,__e,__mu,wall,__a,__Y,__nu
		);
	if(adaptive) setdT(delta[2]);

	for(let n=0,N=collisions.colliding_pairs.length; n<N; n++){
		if(collisions.colliding_pairs[n].length){
			let n1 = collisions.colliding_pairs[n][0],
				n2 = collisions.colliding_pairs[n][1];
			if(obj[n1]) obj[n1].arrV.visible = obj[n2].arrV.visible = true;
		}
	}

	adjust_dt(d_max,find_v_rms(__v));
	if(obj.setStatus){
		obj.setStatus(__r,__v);
	}else
		obj.set(0,{pos:__r});
	//recordData(scene.currentTime(),[scene.currentTime(),scene.currentTime()],[0,__r[0].z]);
};
//}}}
/***
!! Initialization codes for conical pile simulations.
<<<
Conical pile simulation.
<<<
!!!! The preparation
***/
//{{{
let obj = null, critical_size = 1e-2;
let particle_mass = 0, particle_size = 0, particle_number = 0;
let prepareObjects = () => {
	let txtParticleMass = document.getElementById('txtParticleMass');
	particle_mass = +txtParticleMass.value;
	let txtParicleNumber = document.getElementById('txtParticleNumber');
	particle_number = +txtParticleNumber.value;
	let labelTotalMass = document.getElementById('labelTotalMass');
	labelTotalMass.innerText = $tw.ve.round(particle_mass*particle_number,3);
	let txtParicleSize = document.getElementById('txtParticleSize');
	particle_size = +txtParticleSize.value;

	if(obj){
		if(obj.length){
			for(let n=0; n<obj.length; n++){
				scene.remove(obj[n]);
				scene.remove(obj[n].arrV);
			}
		}else{
			scene.remove(obj);
		}
	}
	if(particle_size > critical_size){
		if(obj === null){
			obj = [];
			obj.__mass__ = [];
			obj.__size__ = [];
		}
		obj.length = particle_number;
		//obj.__mass__.length = particle_number;
		//obj.__size__.length = particle_number;
		for(let n=0; n<particle_number; n++){
			obj.__mass__[n] = particle_mass;
			obj.__size__[n] = particle_size/2;
			obj[n] = sphere({
				pos: vector(),
				radius: obj.__size__[n],
				color: 0xffff00,
				opacity: 0.5
			});
			obj[n].arrV = arrow({
				color: 0xffff00,
				visible: false
			});
		}
		obj.setStatus = function(r,v){
			for(let n=0,N=obj.length; n<N; n++){
				obj[n].position.copy(r[n]);
				obj[n].arrV.position.copy(r[n]);
				obj[n].arrV.setAxis(v[n]).scaleLength(3e-2);
			}
			return obj;
		};
		wall.length = 6;
		wall[0] = floor;
		wall[1] = ceiling;
		wall[2] = right;
		wall[3] = left;
		wall[4] = front;
		wall[5] = back;
		floor.visible = true;
		ceiling.visible = true;
		right.visible = true;
		left.visible = true;
		front.visible = true;
		back.visible = true;
	}else{
		obj = points({maxpoints: particle_number});
		for(let n=0; n<particle_number; n++){
			obj.add({
				pos: vector(),
				//color: new $tw.threeD.THREE.Color(Math.random(),Math.random(),Math.random()),
				size: particle_size,
				mass: particle_mass
			});
			//obj.geometry.getAttribute('position').setDynamic(true);
			obj.computeBoundings();
		}
		wall.length = 1;
		wall[0] = floor;
		floor.visible = true;
		ceiling.visible = false;
		right.visible = false;
		left.visible = false;
		front.visible = false;
		back.visible = false;
	}
	let Y = +txtYoungsModulus.value*1e9, nu = +txtPoissonsRatio.value;
	floor.YoungsModulus = Y;
	floor.PoissonsRatio = nu;
	ceiling.YoungsModulus = Y;
	ceiling.PoissonsRatio = nu;
	right.YoungsModulus = Y;
	right.PoissonsRatio = nu;
	left.YoungsModulus = Y;
	left.PoissonsRatio = nu;
	front.YoungsModulus = Y;
	front.PoissonsRatio = nu;
	back.YoungsModulus = Y;
	back.PoissonsRatio = nu;
};
//}}}
/***
!!!! The walls
***/
//{{{
let floor = plane({
	width:2,
	height:2,
	widthSegments: 1,
	heightSegments: 1,
	opacity:0.3
});
let ceiling = plane({
	pos:vector(0,0,1),
	width:2,
	height:2,
	widthSegments: 1,
	heightSegments: 1,
	opacity:0.3
});
let right = box({
	pos:vector(0,1,0.5),
	width:2,
	height:0.01,
	widthSegments: 1,
	heightSegments: 1,
	depth:1,
	opacity:0.3
});
let left = box({
	pos:vector(0,-1,0.5),
	width:2,
	height:0.01,
	widthSegments: 1,
	heightSegments: 1,
	depth:1,
	opacity:0.3
});
let front = box({
	pos:vector(1,0,0.5),
	width:0.01,
	height:2,
	widthSegments: 1,
	heightSegments: 1,
	depth:1,
	opacity:0.3
});
let back = box({
	pos:vector(-1,0,0.5),
	width:0.01,
	height:2,
	widthSegments: 1,
	heightSegments: 1,
	depth:1,
	opacity:0.3
});
let wall = [];
//}}}
/***
!!! Data Plotters
***/
//{{{
dataPlot[0].setYTitle("K.E. (J)").setTitle('Kinetic Energy vs Time');
dataPlot[1].setYTitle("E (J)").setTitle('Total Energy vs Time');

labelPlot[0].innerText = 'K.E. (J)';
labelPlot[1].innerText = 'E (J)';
//}}}
/***
!!!! Scene control, Cartesian coordinate system, etc.
***/
//{{{
txtdT.min = 1e-6;
txtdT.value = 2e-4;
txtYoungsModulus.value = 5e-5;
chkGravity.checked = true;
//scene.camera.position.multiplyScalar(3);
//}}}
!! Quantity Control
Particle number: <html><input type="number" title="size of particles" id="txtParticleNumber" min="0" max="1e6" step="10" value="200" style="width:75px"></html> / Total \(M\) (kg): <html><label id="labelTotalMass" style="font-family:'Courier New'"></label></html>
!! Particle Control
Mass (kg): <html><input type="number" title="Total mass of the particles" id="txtParticleMass" min="1e-31" max="1e32" step="1e-15" value="1e-6" style="width:100px"></html> / Size (m): <html><input type="number" title="size of particles" id="txtParticleSize" min="1e-10" max="1e9" step="1e-10" value="5e-4" style="width:90px"></html>
!! Motion Control
\(v_\text{max} (m/s)\): <html><input type="number" title="Maximum speed of particles" id="txtMaxSpeed" min="1e-6" max="100" step="1e-6" value="10" style="width:45px"></html>
!! Property Control
//e//~~ restore~~: <html><input type="number" title="Elasticity of particles" id="txtElasticity" min="0" max="1" step="0.001" value="0.7" style="width:55px"></html> / \(\mu_\text{ sliding}\): <html><input type="number" title="Sliding speed loss rate" id="txtSlidingSpeedLossRate" min="0" max="1" step="0.001" value="0.3" style="width:55px"></html>
<<tw3DCommonPanel "Start">>
|Conical Pile Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tiddler "Conical Pile Panel##Particle Control">>|<<tiddler "Conical Pile Panel##Quantity Control">>|
|<<tiddler "Conical Pile Panel##Property Control">> / <<tiddler "Conical Pile Panel##Motion Control">>|<<tw3DCommonPanel "Contact Model">> / <<tw3DCommonPanel "Elastic Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel labelCPS "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Conical Pile Initial]] [[Conical Pile Codes]]>>|
[[模擬教室]]
/***
!!! General Functions
***/
//{{{
let same = function(a,b,n){
	return $tw.ve.round(a,n)===$tw.ve.round(b,n);
};

let compare = function(signal,transformed,dlen){
	let diff = 1, L2_0 = 0, n, err = 0, d;
	for(n=0; n<dlen; n++){
		L2_0 += signal[n]*signal[n];
		d = transformed[n] - signal[n];
		err += d*d;
	}
	L2_0 = Math.sqrt(L2_0);
	err = Math.sqrt(err)/(L2_0 == 0 ? 1 : L2_0);
	showMessage('err: '+err);
};

let ftspeed = function(t,N,real){
	let spd = 5*N*Math.log2(N)/(t*1000);
	return spd / (real === true || real === 'real' ? 2 : 1);
};

let timingMessagen = function(N,t,shape,isign=1){
	let msg = N+' time'+(N>1?'s ':' ') + (shape[0]/(real?1:2));
	let timemsg = ' '+(real?'real':'complex')+(isign>0?' forward ':' inverse ')+'FFT in '+$tw.ve.round(t,3)+' ms';
	let dlen = shape[0];
	if(shape.length === 1){
		msg += ' x 1';
	}else{
		for(let n=1; n<shape.length; n++){
			msg += ' x ' + shape[n];
			dlen *= shape[n];
		}
	}
	msg += ' ('+dlen+')';
	return msg+timemsg+', speed (mflops): '+$tw.ve.round(ftspeed(t/N,dlen,real),3);
};
//}}}
/***
!!! Power spectrum
***/
//{{{
let powerSpectrum = function(signal,transformed){
	let power = $tw.data.dataArray();
	let shape = []; //$tw.data.dataArray();
	shape.length = transformed.shape ? transformed.shape.length : 1;
	if(shape.length === 1){
		shape[0] = power.length = transformed.length;
		if(signal.dtype==='real') shape[0] >>= 1;
	}else{
		for(let n=0; n<shape.length; n++)
			shape[n] = transformed.shape[n];
		if(signal.dtype==='real') shape[signal._fastest] >>= 1;
		init_data(power,shape,'complex',signal.order);
	}

	for(let n=0,pn=0,nlen=power.length; pn<nlen; n+=2, pn++){
		power[pn] = transformed[n]*transformed[n]+transformed[n+1]*transformed[n+1];
	}

	let len = power.shape, index = power._index;

	if(len.length === 1){
		power.coord = [[]];
		power.kx = power.coord[0];
	}else if(len.length === 2){
		power.coord = [[],[]];
		power.kx = power.coord[0];
		power.ky = power.coord[1];
	}else{
		power.coord = [[],[],[]];
		power.kx = power.coord[0];
		power.ky = power.coord[1];
		power.kz = power.coord[1];
	}

	for(let n=0; n<len.length; n++){
		let r0;
		if(index[n]===power._fastest && power.dtype==='real')
			r0 = len[index[n]]/2;
		else
			r0 = 0;
		for(let i=0,r=r0; i<len[index[n]]; i++,r=(r+1)%len[index[n]]){
			power.coord[n][i] = r;
		}
	}

	return power;
};
//}}}
/***
!!! Data visualization
***/
//{{{
let coloredSurface = function(sceneElem,data,x,y,z,surface,cmin,cmax){
	cmax = cmax || 1;
	cmin = cmin || 0;
	let vmax = data.max(), vmin = data.min(), full = vmax-vmin;
	vmax = vmin + full*cmax;
	vmin = vmin + full*cmin;
	full = vmax - vmin;

	if (!surface){
		surface = points(null,'noadd');
		sceneElem.scene.add(surface);
	}else{
		surface = surface.recreate(sceneElem.scene);
	}

	let len = data.shape, index = data._index;

	let cn;
	if(len.length === 2){
		for(let i=0, t=0; i<len[index[0]]; i++){
			for(let j=0; j<len[index[1]]; j++, t++){
				if(data[t] < vmin || data[t] > vmax) continue;
				cn = (data[t]-vmin)/full;
//console.log('['+i+','+j+'] = '+data[t]+' at ('+x[i]+','+y[j]+')');
				surface.add({
					pos: vector(x[i], y[j], data[t]),
					color: new $tw.threeD.THREE.Color(cn,cn,cn)
				},'nocalc');
			}
		}
	}else if(len.length === 3){
		for(let i=0, t=0; i<len[index[0]]; i++){
			for(let j=0; j<len[index[1]]; j++){
				for(let k=0; k<len[index[2]]; k++, t++){
					if(data[t] < vmin || data[t] > vmax) continue;
					cn = (data[t]-vmin)/full;
//console.log('['+i+','+j+','+k+'] = '+data[t]+' at ('+x[i]+','+y[j]+','+z[k]+')');
					surface.add({
						pos: vector(x[i], y[j], z[k]),
						color: new $tw.threeD.THREE.Color(cn,cn,cn)
					},'nocalc');
				}
			}
		}
	}
	surface.calculateBoundings();
	return surface;
}
let surface = null;
let spectrum = null;
let showData = function(signal,transformed){
	let dim = signal.shape.length;
	let real = document.getElementById('sceneReal');
	surface = coloredSurface(
		real,signal,signal.x,signal.y,
		(dim===2?signal:signal.z),surface,0.2,1
	);
	surface.setSize(0.1);
	//surface.setTexture('textures/sprites/ball.png');
	//surface.scale.set(2,2,2);

	let reciprocal = document.getElementById('sceneReciprocal');
	let power = powerSpectrum(signal,transformed);
	spectrum = coloredSurface(
		reciprocal,power,power.kx,power.ky,
		(dim===2?power:power.kz),spectrum,0.3,1
	);
	spectrum.setSize(0.2);
	//spectrum.setTexture('textures/sprites/ball.png');
	//spectrum.scale.set(1,1,1);
	real.scene.objectChanged(true);
	reciprocal.scene.objectChanged(true);
};
//}}}
/***
!!! Plot a cubic frame
***/
//{{{
/*
let cubeFrame = function(x,y,z,ox=0,oy=0,oz=0,ax=1,ay=1,az=1,frame,color){
	color = color || new $tw.threeD.THREE.Color(0.5,0.5,0.5);
	c = curve(
		frame=frame,
        color=color,
        pos=[
            (x[ox],y[oy],z[oz]),(x[ox+ax],y[oy],z[oz]),
            (x[ox+ax],y[oy+ay],z[oz]),(x[ox],y[oy+ay],z[oz]),
            (x[ox],y[oy],z[oz]),(x[ox],y[oy],z[oz+az]),
            (x[ox],y[oy],z[oz+az]),(x[ox+ax],y[oy],z[oz+az]),
            (x[ox+ax],y[oy+ay],z[oz+az]),(x[ox],y[oy+ay],z[oz+az]),
            (x[ox],y[oy],z[oz+az]),(x[ox],y[oy+ay],z[oz+az]),
            (x[ox],y[oy+ay],z[oz]),(x[ox+ax],y[oy+ay],z[oz]),
            (x[ox+ax],y[oy+ay],z[oz+az]),(x[ox+ax],y[oy],z[oz+az]),
            (x[ox+ax],y[oy],z[oz])
        ]
    )
    return c

def latticeFrameCube(x,y,z,px,py,pz,ax,ay,az,frame=None):
	nx = 0
	while nx < px-ax:
		ny = 0
		while ny < py-ay:
			nz = 0
			while nz < pz-az:
				#halfCubeFace(
				cubeFrame(
					x,y,z,
					ox=nx,oy=ny,oz=nz,
					ax=ax,ay=ay,az=az,
					frame=frame
				)
				nz = nz + az
			ny = ny + ay
		nx = nx + ax
*/
//}}}
/***
!!! Test routine
***/
//{{{
let fshow = 0, fshowmax = 0.5;
let FTtest = function(signal,transformed,log2N,real){
	let t0 = performance.now();
	let tprep, tfft, tshow, tifft, terr;
	clearMessage();
	tprep = performance.now();
	prepareData(signal,log2N,real);
	tprep = performance.now() - tprep;

	prepareData(transformed,log2N,real);
	let N = txttimes.value;
	tfft = performance.now();
	FFT(transformed,N);
	tfft = performance.now() - tfft;
	tshow = performance.now();
	if(fshow < fshowmax) {
		showData(signal,transformed);
	}
	tshow = performance.now() - tshow;

	if(typeof iFFT === 'function'){
		tifft = performance.now();
		iFFT(transformed,N);
		tifft = performance.now() - tifft;
		terr = performance.now();
		compare(signal,transformed,signal.length);
		terr = performance.now() - terr;
	}
	t0 = performance.now() - t0;
	if(fshow < fshowmax) fshow = tshow/t0;
	let tmisc = t0-tprep-tfft-tshow-tifft-terr;
	showMessage(
		'Total test time: '+t0+' ms.\n'+
		'\tData prepared in '+tprep+' ms ('+($tw.ve.round(tprep/t0*100,2))+'%).\n'+
		'\tFFT done in '+tfft+' ms ('+($tw.ve.round(tfft/t0*100,2))+'%).\n'+
		(fshow<fshowmax?'':'(Skipped)')+'\tData visualized in '+tshow+' ms ('+($tw.ve.round(tshow/t0*100,2))+'%).\n'+
		'\tiFFT done in '+tifft+' ms ('+($tw.ve.round(tifft/t0*100,2))+'%).\n'+
		'\tErr calculated in '+terr+' ms ('+($tw.ve.round(terr/t0*100,2))+'%).\n'+
		'\tUncounted: '+tmisc+' ms ('+($tw.ve.round(tmisc/t0*100,2))+'%).\n'
	);
};
//}}}
/***
!!! Initialization
***/
//{{{
let txtlog2N = document.getElementById('txtLengthPower');
let txttimes = document.getElementById('txtTestTimes');
let txtRepeat = document.getElementById('txtRepeat');
let chkReal = document.getElementById('chkReal');
let transformed = $tw.data.dataArray(), signal = $tw.data.dataArray();
let log2N = 0, real = true, repeat = 1;

if(txtlog2N){
	log2N = txtlog2N.value;
	real = chkReal.checked;
//}}}
/***
!!! showMessage()
***/
//{{{
	let msgbrd = document.getElementById('spanMsg');
	let clearMessage = function(){
		if (msgbrd){
			msgbrd.msg = "";
			msgbrd.innerHTML = "";
		}
	};

	let showMessage = function(msg){
		if (msgbrd){
			if (msgbrd.msg === undefined)
				msgbrd.msg = msg;
			else
				msgbrd.msg += '\n'+msg;
			$tw.ve.node.wikify(msgbrd.msg,msgbrd);
		}
	};
//}}}
/***
!!! onchange
***/
//{{{
	let checkAndRecreate = function(){
		let newlog2N = txtlog2N.value;
		if(newlog2N < txtlog2N.min) newlog2N = txtlog2N.min;
		let newreal = chkReal.checked;
		let newrepeat = txtRepeat.value;
		if(newrepeat < txtRepeat.min) newrepeat = txtRepeat.min;
		if (newlog2N !== log2N || newreal !== real || newrepeat !== repeat){
			log2N = newlog2N;
			real = newreal;
			repeat = newrepeat;
			fshow = 0;
			FTtest(signal,transformed,log2N,real);
		}
	};

	txtlog2N.onchange = chkReal.onchange =
		chkComplex.onchange = txtRepeat.onchange = checkAndRecreate;
}
//}}}
/***
!!! data initialization
***/
//{{{
let init_data = function(data, shape, real, order){
	data.shape = []; //$tw.data.dataArray();
	for(let n=0,N=shape.length; n<N; n++)
		data.shape[n] = shape[n];

	if(order && order === 'col'){
		data.order = 'col';
	}else{
		data.order = 'row';
	}
	data.getProcessOrder();
	if(real){
		data.dtype = 'real';
	}else{
		data.dtype = 'complex';
		data.shape[data._index[data._index.length-1]] *= 2;
	}
	data.setMaxLength($tw.data.product(data.shape));
	data.fill(0);
};
//}}}
/***
!!! Data preperation -- 1D
***/
//{{{
let prepareData = function(data,log2N,real){
	// 1D data.
	let ppx = Math.pow(2,log2N);
	let npx = txtRepeat.value, dlen = ppx * npx;
	let phase = 2*Math.PI/ppx;
	init_data(data,[dlen],real);
	for(let i=0,i2=0; i < dlen; i++,i2+=2){
		if (real){
			data[i] = Math.sin(i*phase);
		} else {
			//data[i2] = 0;
			data[i2] = Math.sin(i*phase);
			data[i2+1] = 0;
		}
	}
	data.setLength(dlen);

	return data;
};
//}}}
/***
!!! Fast Fourier Transform -- 1D
***/
//{{{
let FFT = function(data,N,isign=1){
	let tfft = $tw.data.dataArray(), dt = 0;
	for(let i=0; i < N; i++){
		if(isign > 0){
			dt = performance.now();
			(real ? $tw.numeric.fft.rfft(data) : $tw.numeric.fft.fft1(data));
			tfft.addPoint(performance.now()-dt);
		}else{
			dt = performance.now();
			(real ? $tw.numeric.fft.irfft(data) : $tw.numeric.fft.ifft(data));
			tfft.addPoint(performance.now()-dt);
		}
	}

	let msg = timingMessagen(N,tfft.sum(),[data.getLength()],isign);
	showMessage(msg);
	//console.log(msg);
};
let iFFT = function(data,N){
	FFT(data,N,-1);
};
//}}}
/***
!!! Data visualization -- 1D
***/
//{{{
let showData = function(signal,transformed){
	realPlot.clearPlot().setYData(signal).update();
	let power = $tw.data.dataArray();
	for(let n=0,pn=0,N=transformed.getLength(); n<N; n+=2,pn++)
		power[pn] = transformed[n]*transformed[n]+transformed[n+1]*transformed[n+1];
	power.setLength(pn);
	reciprocalPlot.clearPlot().setYData(power).update();
};
//}}}
/***
!!! Initialization
***/
//{{{
let realPlot = $tw.data.linearPlot({
	id: 'dataPlot',
	xTitle: 'Position/Time',
	yTitle: 'Signal',
	Title: 'Real Space/Time Domain Data'
});
let reciprocalPlot = $tw.data.logYPlot({
	id: 'powerPlot',
	xTitle: '\\(k\\) or \\(f\\)',
	yTitle: 'Power',
	Title: 'Reciprocal Space/Frequency Domain Results'
});
if(log2N){
	FTtest(signal,transformed,log2N,real);
}
//}}}
/***
!!! Data preperation -- 2D
***/
//{{{
let prepareData = function(data,log2N,real,order){
	// 2D data.
	init_data(data,[Math.pow(2,log2N), Math.pow(2,log2N)],real,order);

	let index = data._index, shape = data.shape;

	let repeat = +txtRepeat.value;
	let np = [repeat, repeat];
	let pp = [shape[0]/np[0], shape[1]/np[1]];
	let xo2 = [np[0]/2, np[1]/2];

	data.coord = [[],[]];
	data.x = data.coord[0];
	data.y = data.coord[1];
	let xs;
	if(data.order === 'row'){
		xs = data._fastest;
	}else{
		xs = data._slowest;
	}
	for(let i=0; i<shape[xs]; i++) data.coord[1][i] = i/pp[xs]-xo2[xs];
	for(let j=0; j<shape[1]; j++) data.coord[0][j] = j/pp[1]-xo2[1];

	let phase = [Math.PI*2*2/pp[index[0]],Math.PI*2/pp[index[1]]];
	let ndx = [0,0];
	for(; ndx[0]<shape[index[0]]; ndx[0]++){
		for(ndx[1] = 0; ndx[1]<shape[index[1]]; ndx[1]++){
			data.setNDPoint(ndx, Math.sin(ndx[0]*phase[0])+Math.cos(ndx[1]*phase[1]),0);
		}
	}
	data.getExtents();
	return data;
};
//}}}
/***
!!! Fast Fourier Transform -- 2D
***/
//{{{
let FFT = function(data,N,isign){
	if(typeof isign !== 'number') isign = 1;
	let tfft = $tw.data.dataArray(), dt = 0;
	for(let i=0; i < N; i++){
		if(isign >= 0){
			dt = performance.now();
			(data.dtype === 'real'
				? $tw.numeric.fft.rfft2(data,data.shape)
				: $tw.numeric.fft.fft2(data,data.shape)
			);
			tfft.addPoint(performance.now()-dt);
		}else{
			dt = performance.now();
			(data.dtype === 'real'
				? $tw.numeric.fft.irfft2(data,data.shape)
				: $tw.numeric.fft.ifft2(data,data.shape)
			);
			tfft.addPoint(performance.now()-dt);
		}
	}

	let msg = timingMessagen(N,tfft.sum(),data.shape,isign);
	showMessage(msg);
};
let iFFT = function(data,N){
	FFT(data,N,-1);
};
//}}}
/***
!!! scene.dispose
***/
//{{{
/*
scene.dispose = function(){
	document.getElementById('sceneReal').scene.__dispose();
	//document.getElementById('sceneReciprocal').scene.__dispose();
}
*/
//}}}
/***
!!! Initialization
***/
//{{{
if(log2N){
	document.getElementById('sceneReal').scene.createTrackballControl();
	document.getElementById('sceneReciprocal').scene.createTrackballControl();
	FTtest(signal,transformed,log2N,real);
}
//}}}
/***
!!! sine/cosine data
***/
//{{{
let csdata = function(data,np,pp,real){
	let index = data._index, dN = data._dN;
	let phase = [2*Math.PI/pp[index[0]], 2*Math.PI/pp[index[1]], 2*Math.PI/pp[index[2]]];
	for(let i=0,t0=0; i<data.shape[index[0]]; i++, t0+=dN[0]){
		for(let j=0, t1=t0; j<data.shape[index[1]]; j++, t1+=dN[1]){
			for(let k=0, t2=t1; k<data.shape[index[2]]; k++,t2+=dN[2]){
				data[t2] = Math.sin(i*phase[0])+Math.cos(j*phase[1])+Math.sin(k*phase[2]);
					data[t2] = Math.sin(i*phase[0])+Math.cos(j*phase[1])+Math.sin(k*phase[2]);
			}
		}
	}
	return data;
};
//}}}
/***
!!! Simple Cubic Lattice
***/
//{{{
let scdata = function(data,np,pp){
	let index = data._index;
	let n=[0,0,0], ndx=[0,0,0];
	for(; n[0]<np[index[0]]; n[0]++){
		ndx[0] = n[0]*pp[0];
		for(n[1]=0; n[1]<np[index[1]]; n[1]++){
			ndx[1] = n[1]*pp[1];
			for(n[2]=0; n[2]<np[index[2]]; n[2]++){
				ndx[2] = n[2]*pp[2];
console.log('n='+n+' ndx='+ndx);
				data.setNDPoint(ndx,1);
			}
		}
	}
	return data;
};
//}}}
/***
!!! Body Centered Cubic Lattice
***/
//{{{
let bccdata = function(data,np,pp){
	let index = data._index;
	let ndx = [0,0,0], bc = [0,0,0];
	for(; ndx[0]<np[index[0]]; ndx[0]++){
		bc[0] = ndx[0] + 0.5;
		for(ndx[1]=0; ndx[1]<np[index[1]]; ndx[1]++){
			bc[1] = ndx[1] + 0.5;
			for(ndx[2]=0; ndx[2]<np[index[2]]; ndx[2]++){
				bc[2] = ndx[2] + 0.5;
				// corners
				data.setNDPoint(ndx,1);
				// body centers
				data.setNDPoint(bc,1);
			}
		}
	}
	return data;
};
//}}}
/***
!!! Data preperation -- 3D
***/
//{{{
let prepareData = function(data,log2N,real,order){
	// 3D data.
	let N = Math.pow(2,log2N);
	init_data(data,[N,N,N],real,order);

	let index = data._index, dN = data._dN;
	let shape = data.shape;

	let repeat = txtRepeat.value;
	let np = [repeat, repeat, repeat];
	let pp = [shape[0]/np[0], shape[1]/np[1], shape[2]/np[2]];
	let xo2 = [np[0]/2, np[1]/2, np[2]/2];
	data.coord = [[],[],[]];
	data.x = data.coord[2]; data.y = data.coord[1]; data.z = data.coord[0];
	for(let i=0; i<shape[0]; i++) data.x[i] = i/pp[0]; //-xo2[0];
	for(let j=0; j<shape[1]; j++) data.y[j] = j/pp[1]; //-xo2[1];
	for(let k=0; k<shape[2]; k++) data.z[k] = k/pp[2]; //-xo2[2];

	if(!real)
		csdata(data,np,pp,real);
	else
		scdata(data,np,pp);

	data.getExtents();
	return data;
};
//}}}
/***
!!! Fast Fourier Transform -- 3D
***/
//{{{
let FFT = function(data,N,isign=1){
	let tfft = $tw.data.dataArray(), dt = 0;
	for(let i=0; i < N; i++){
		if(isign > 0){
			dt = performance.now();
			(real
				? $tw.numeric.fft.rfft3(data,data.shape)
				: $tw.numeric.fft.fft3(data,data.shape)
			);
			tfft.addPoint(performance.now()-dt);
		}else{
			dt = performance.now();
			(real
				? $tw.numeric.fft.irfft3(data,data.shape)
				: $tw.numeric.fft.ifft3(data,data.shape)
			);
			tfft.addPoint(performance.now()-dt);
		}
	}

	let msg = timingMessagen(N,tfft.sum(),data.shape,isign);
	showMessage(msg);
};
let iFFT = function(data,N){
	FFT(data,N,-1);
};
//}}}
/***
!!! Initialization
***/
//{{{
if(log2N){
	FTtest(signal,transformed,log2N,real);
}
//}}}
!! Data Length
Data points in one period: \(2^p, p = \) <html><input type="number" id="txtLengthPower" min="1" max="20" step="1" value="4" style="width:40px"></html>
!! Data Type
Data Type: [ =chkReal{}{}{if(chkReal.checked && chkComplex.checked) chkComplex.click();}] Real / [X=chkComplex{}{}{if(chkComplex.checked && chkReal.checked) chkReal.click();}] Complex
!! Repeat Control
Repeat structure <html><input type="number" id="txtRepeat" min="1" max="20" step="1" value="1" style="width:40px"></html> time(s).
!! Test Control
Test for <html><input type="number" id="txtTestTimes" min="1" max="100" step="1" value="1" style="width:40px"></html> time(s).
!! labelReal
''Real space signal:''
!! labelReciprocal
''Reciprocal space''
!! FFT test
* \(err \equiv {\sqrt{|iFT(FT(X))|^2 - |X|^2} \over |X|^2}\)
|FFT Test|c
|>|width:45%;<<tw3DCommonPanel "Message">>|
|<<tiddler "FFT Panel##Data Length">>|<<tiddler "FFT Panel##Test Control">>|
|<<tiddler "FFT Panel##Data Type">>|<<tiddler "FFT Panel##Repeat Control">>|
<<tabs ''
'1D' '' 'FFT test 1D'
'2D' '' 'FFT test 2D'
'3D' '' 'FFT test 3D'
>>
<<twDataPlot id:dataPlot width:48% height:20em>> <<twDataPlot id:powerPlot width:48% height:20em [[FFT Code]] [[FFT Code 1D]]>>
<<tw3DScene id:sceneReal width:48% height:50%>>  <<tw3DScene id:sceneReciprocal width:48% height:50%  [[FFT Code]] [[FFT Code 2D]]>>
<<tw3DScene id:sceneReal width:48% height:50%>> <<tw3DScene id:sceneReciprocal width:48% height:50% [[FFT Code]] [[FFT Code 3D]]>>
{{MOSTTitle{
探究式學習怎麼做?
How to practice inquiry-based learning?
}}}
{{MOSTSection{
!!! 觀察 To observe
}}}
{{MOSTParagraph{
探究式學習的第一件事通常是【觀察】。Usually the first thing in inquiry-based-learning is "to observe".
}}}
{{MOSTSubParagraph{
觀察什麼?觀察【特徵行為】。Observe what? Find the characteristic behaviors.
}}}
{{MOSTSubParagraph{
例:For example:
|||
|||
}}}
{{MOSTSection{
!!! 找參數 Find parameters
}}}
{{MOSTParagraph{
找出特徵行為之後,再來要找出影響這些行為的【參數】。After finding the characteristic behaviors, we shall find those "parameters" that would affect those behaviors.
}}}
{{MOSTSubParagraph{
怎麼確認參數?How to identify a parameter?
# 找出可改變的因素;Find those factors that can be changed;
# 改變其中一個因素並保持其它因素不變,看看特徵行為是否受到影響。Change one of those factors while keep others unchanged, see if the characteristic behavior is affected.
# 如果有,再改變同一個因素一兩次,如果特徵行為確實受到影響,那麼這個改變的因素就是一個參數。If it is, then change the same factor one or two more times. If the characteristic behavior is affected for sure, then this factor being changed is a parameter of the system.
}}}
{{MOSTSection{
!!! 觀察參數的影響 Observe the effect of the parameters
}}}
{{MOSTParagraph{
找出參數之後,再來要找出這些參數【如何影響】特徵行為。After finding the parameters, we shall find out how the parameters affect the characteristic behaviors.
}}}
{{MOSTSubParagraph{
多次改變參數,橫跨較大範圍。Change the parameters multiple times, span over a larger range.
# 
}}}
/***
!!!Constants
***/
//{{{
// 假設木頭棒子,楊氏係數約為 11 GPa(參考 https://en.wikipedia.org/wiki/Young%27s_modulus)
// radius = 0.005 (diameter 1 cm)
const __kmax__ = 1.1e10*(Math.PI*Math.pow(0.005,2))*0.1;
console.log('__kmax__='+$tw.ve.round(__kmax__,3));
__trail_len__ = +txtTrailLength.value;
__trail_interval__ = +txtTrailInterval.value;
//}}}
/***
!!!General Controls
***/
//{{{
txtTmax.value = 50;
chkGravity.checked = true;
chkGravity.disabled = true;
txtdT.value = 1e-4;
txtTolerance.value = '1e-3';
chkAutoCPF.checked = true;
chkMakeTrail.checked = true;
//txtTrailInterval.value = 10;
//}}}
/***
!!!Problem specific properties
***/
//{{{
txtPivot1Mass.value = txtPivot2Mass.value = 0.01;
txtKpivot1.value = txtKpivot2.value = 500;
chkPivot1Movable.checked = chkPivot2Movable.checked = true;
txtString1Radius.value = txtString2Radius.value = 0.002;
//chkHertzianModel.checked = true;
//}}}
/***
!!!Creation
***/
//{{{
// Arrange the scene
let pendulum = [null,null],
	colliding_obj = [],
	pivotMass = [txtPivot1Mass,txtPivot2Mass],
	pivotMovable = [chkPivot1Movable,chkPivot2Movable],
	Kpivot = [txtKpivot1,txtKpivot2],
	initialTheta = [txtInitialTheta1,txtInitialTheta2],
	initialPhi = [txtInitialPhi1,txtInitialPhi2],
	stringLength = [txtString1Length,txtString2Length],
	stringMass = [txtString1Mass,txtString2Mass],
	stringRadius = [txtString1Radius,txtString2Radius],
	bobMass = [txtBob1Mass,txtBob2Mass],
	bobRadius = [txtBob1Radius,txtBob2Radius];
//}}}
/***
!!!!Pendulum creation
***/
//{{{
for(let n=0,N=pendulum.length; n<N; n++){
	pendulum[n] = $tw.physics.Pendulum.create({
		pivot: {
			radius:0.01,
			opacity:0.3,
			mass: +pivotMass[n].value,
			make_trail: false,
			retain: __trail_len__,
			interval: __trail_interval__
		},
		string: {
			axis:vector(0,0,-1),
			radius:+stringRadius[n].value,
			thickness:0.005,
			color:0xFED162,
			//opacity:0.3,
			soft:true,
			coils:60
		},
		bob: {
			radius: +bobRadius[n].value,
			mass: +bobMass[n].value,
			make_trail: false,
			retain: __trail_len__,
			interval: __trail_interval__,
			opacity: 0.5
		}
	});
}
colliding_obj[0] = pendulum[0].bob;
colliding_obj[1] = pendulum[1].bob;
scene.add(pendulum[0]).add(pendulum[1]);
//}}}
/***
!!!!Intrinsic Properties
***/
//{{{
for(let n=0,N=pendulum.length; n<N; n++){
	pendulum[n].pivot.r0 = pendulum[n].pivot.position.clone();
	pendulum[n].pivot.attachSprings(0.05,__kmax__);
	pendulum[n].pivot.addToCSSScene(cssscene,scene.scale);
	pendulum[n].string.k = __kmax__;
}
//}}}
/***
!!!!Add a spring to connect the two pivots
***/
//{{{
pendulum.spring_pivots = $tw.physics.Pendulum.String.create({
	radius: 0.0005,
	k: __kmax__*10,
	axis: $tw.threeD.vector3(0,0,1),
	color: 0xffff00,
	coils: 60
});
pendulum.spring_pivots.L0 = 2e-3;
//}}}
/***
!!!Camera setting
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(txtString1Length.value*2);
//}}}
/***
!!!Center of Mass
***/
//{{{
const CM = sphere({
	radius: 0.005, opacity: 0.5,
	color: 0xffff00
}),
//}}}
/***
!!!calculateInitialTheta
***/
//{{{
calculateInitialTheta = () => {
	txtInitialTheta2.value = -(
		txtInitialTheta1.value = Math.asin(txtBob1Radius.value/txtString1Length.value)/Math.PI*180
	);
},
//}}}
/***
!!!calculateCM
***/
//{{{
calculateCM = () => {
	CM.position.set(0,0,0);
	let totalM = 0, tmp = vector();
	for(let n=0,N=pendulum.length; n<N; n++){
		CM.position.add(
			tmp.copy(pendulum[n].calculateCM())
				.multiplyScalar(pendulum[n].totalMass())
		);
		totalM += pendulum[n].totalMass();
	}
	CM.position.multiplyScalar(1/totalM);
	return CM;
},
//}}}
/***
!!!checkKpivot
***/
//{{{
checkKpivot = (p,k) => {
	for(let n=0,N=p.pivot.spring.length; n<N; n++){
		let ks = (n === 2 ? k : __kmax__);
		if(ks !== p.pivot.spring[n].k){
			p.pivot.spring[n].k = ks;
			p.pivot.spring[n].label.setText('k='+$tw.ve.round(ks,2));
			p.pivot.spring[n].setRadius(0.002);
		}
	}
	return k;
},
//}}}
/***
!!!storage for calculations
***/
//{{{
__r = [], __v = [], __a = [];
//}}}
/***
!!!scene.init
***/
//{{{
scene.init = () => {
	calculateInitialTheta();
	for(let n=0,N=pendulum.length; n<N; n++){
//}}}
/***
!!!!pivot properties
***/
//{{{
		pendulum[n].pivot.setMass(+pivotMass[n].value);
		pendulum[n].pivot.velocity.set(0,0,0);
		pendulum[n].pivot.acceleration.set(0,0,0);
		if(pendulum[n].pivot.spring)
			checkKpivot(pendulum[n],+Kpivot[n].value);
//}}}
/***
!!!!string properties
***/
//{{{
		pendulum[n].string.setRadius(+stringRadius[n].value);
		pendulum[n].string.setLength((pendulum[n].string.L0=
			+stringLength[n].value
			//n===0
			//	?	(stringLength[n].value-bobRadius[n].value)*2
			//	:	(bobRadius[n].value*2)
		));
		pendulum[n].string.theta0 = initialTheta[n].value*Math.PI/180.0;
		pendulum[n].string.phi0 = initialPhi[n].value*Math.PI/180.0;
		pendulum[n].string.mass = +stringMass[n].value;
//}}}
/***
!!!!bob properties
***/
//{{{
		pendulum[n].bob.setRadius(+bobRadius[n].value);
		pendulum[n].bob.setMass(+bobMass[n].value);		// kg
		if(n===N-1) pendulum[n].bob.setColor(0xffff00);
		pendulum[n].bob.YoungsModulus = +txtYoungsModulus.value*1e9;
		pendulum[n].bob.PoissonsRatio = +txtPoissonsRatio.value;
		showForceIndicator(pendulum[n].bob,chkShowForce.checked);
		showVelocityIndicator(pendulum[n].bob,chkShowVelocity.checked);
		showAccelerationIndicator(pendulum[n].bob,chkShowAcceleration.checked);
//}}}
/***
!!!!pivot and bob positions
***/
//{{{
		let r_pivot = vector(), r_bob = vector(), L = 0,
			theta = Math.PI-pendulum[n].string.theta0,
			sin_theta = Math.sin(theta);
		if(pendulum[n].pivot.spring){
			// Pivot is displaced initially.
			L = 3.6e-3;
			r_pivot.set(
				(pendulum[n].pivot.spring[0].k===__kmax__?0:L),
				(pendulum[n].pivot.spring[1].k===__kmax__?0:L),
				(pendulum[n].pivot.spring[2].k===__kmax__?0:L)
			).add(pendulum[n].pivot.r0);
		}else
			r_pivot.copy(pendulum[n].pivot.r0);
		r_pivot.x += pendulum.spring_pivots.L0/2*(n?-1:1);

		// Bob is displaced accordingly.
		L = pendulum[n].string.L0;
		r_bob.set(
			L*sin_theta*Math.cos(pendulum[n].string.phi0),
			L*sin_theta*Math.sin(pendulum[n].string.phi0),
			L*Math.cos(theta)
		).add(r_pivot);
		pendulum[n].setPosition(r_pivot,r_bob);
	}
}
//}}}
/***
!!!Adjust the spring connecting the two pivots
***/
//{{{
pendulum.spring_pivots.setPosition(
	pendulum[0].pivot.position
).setAxis(
	pendulum[1].pivot.position.clone().sub(pendulum[0].pivot.position)
);
//}}}
/***
!!!scene.initialized
***/
//{{{
scene.initialized = () => {
	for(let n=0,n2=0,N=pendulum.length; n<N; n++,n2+=2)
		pendulum[n].getPositions(__r,n2).getVelocities(__v,n2).getAccelerations(__a,n2);
	calculateCM();
}
//}}}
/***
!!!calculateA
***/
//{{{
const calculateA = (r,v,t,a) => {
	for(let n=0,n2=0,N=pendulum.length; n<N; n++,n2+=2)
		pendulum[n].calculateAcceleration(r,v,a,n2);

	// Calculate the force connecting the two pivots
	let T0 = pendulum.spring_pivots.calculateTension(r[2],r[0]),
		T1 = T0.clone().multiplyScalar(-1);
	a[0].add(T0.multiplyScalar(pendulum[0].pivot.inv_mass));
	a[2].add(T1.multiplyScalar(pendulum[1].pivot.inv_mass));

	// Calculate damping on the pivots
	T0.copy(v[0]).multiplyScalar(-0.3);
	T1.copy(v[2]).multiplyScalar(-0.3);
	a[0].add(T0.multiplyScalar(pendulum[0].pivot.inv_mass));
	a[2].add(T1.multiplyScalar(pendulum[1].pivot.inv_mass));
},
//}}}
/***
!!!updatePendulumMotion
***/
//{{{
updatePendulumMotion = (r,v,a) => {
	for(let n=0,n2=0,N=pendulum.length; n<N; n++,n2+=2)
		pendulum[n].setPositions(r,n2).setVelocities(v,n2).setAccelerations(a,n2);
},
//}}}
/***
!!!afterCollision
***/
//{{{
afterCollision = () => {
	if(chkHertzianModel.checked)
		for(let n=0,n2=0,N=pendulum.length; n<N; n++,n2+=2)
			pendulum[n].getAccelerations(__a,n2);
	else
		for(let n=0,n2=0,N=pendulum.length; n<N; n++,n2+=2)
			pendulum[n].getVelocities(__v,n2);
}
//}}}
/***
!!!scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	for(let p of pendulum){
		p.showForce(chkShowForce.checked);
		p.showVelocity(chkShowVelocity.checked);
		p.showAcceleration(chkShowAcceleration.checked);
	}
}
//}}}
/***
!!!scene.update
***/
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		// calculate next positions/velocities using 4th order Runge-Kutta mehtod
		delta_pos = $tw.numeric.ODE.nextValue(
			__r,__v,calculateA,t_cur,dt,__a,adaptive,e0
		);
	if(adaptive) setdT(delta_pos[2]);

	updatePendulumMotion(__r,__v,__a);
	$tw.physics.objCheckCollisions(colliding_obj,chkHertzianModel.checked);
	afterCollision();
}
//}}}
!!Pivot1 Control
Pivot~~1~~: \(m\) (kg) <html><input type="number" title="Mass of pivot 1." id="txtPivot1Mass" min="0.01" max="1" step="0.001" value="0.1" style="width:55px"></html> / [ =chkPivot1Movable] Movable / \(k_\text{pivot,1}\): <html><input type="number" title="Spring constant of pivot 1." id="txtKpivot1" min="0" max="200" step="0.1" value="81.7" style="width:55px"></html>
!!Initial Position1
\(\theta_{1,0}\) (&deg;): <html><input type="number" title="Initial theta 1." id="txtInitialTheta1" min="0" max="180" step="0.1" value="5" style="width:45px"></html> / \(\phi_{1,0}\) (&deg;): <html><input type="number" title="Initial phi 1." id="txtInitialPhi1" min="0" max="360" step="0.1" value="0" style="width:45px"></html>
!!Pivot2 Control
Pivot~~2~~: \(m\) (kg) <html><input type="number" title="Mass of pivot 2." id="txtPivot2Mass" min="0.01" max="1" step="0.001" value="0.1" style="width:55px"></html> / [ =chkPivot2Movable] Movable / \(k_\text{pivot,2}\): <html><input type="number" title="Spring constant of pivot 2." id="txtKpivot2" min="0" max="200" step="0.1" value="81.7" style="width:55px"></html>
!!Initial Position2
\(\theta_{2,0}\) (&deg;): <html><input type="number" title="Initial theta 2." id="txtInitialTheta2" min="0" max="180" step="0.1" value="-5" style="width:45px"></html> / \(\phi_{2,0}\) (&deg;): <html><input type="number" title="Initial phi 2." id="txtInitialPhi2" min="0" max="360" step="0.1" value="0" style="width:45px"></html>
!!String1 Control
String~~1~~: //L//~~1,0~~ (m) <html><input type="number" title="Length of string 1." id="txtString1Length" min="0.1" max="1" step="0.01" value="0.3" style="width:50px"></html> / \(m\) (kg) <html><input type="number" title="Mass of string 1." id="txtString1Mass" min="0.001" max="1" step="0.001" value="0.005" style="width:60px"></html> / \(r\) (m) <html><input type="number" title="Radius of string 1." id="txtString1Radius" min="0.0005" max="0.05" step="0.001" value="0.0005" style="width:55px"></html>
!!String2 Control
String~~2~~: //L//~~2,0~~ (m) <html><input type="number" title="Length of string 2." id="txtString2Length" min="0.1" max="1" step="0.01" value="0.3" style="width:50px"></html> / \(m\) (kg) <html><input type="number" title="Mass of string 2." id="txtString2Mass" min="0.001" max="1" step="0.001" value="0.005" style="width:60px"></html> / \(r\) (m) <html><input type="number" title="Radius of string 2." id="txtString2Radius" min="0.0005" max="0.05" step="0.001" value="0.0005" style="width:55px"></html>
!!Bob1 Control
Bob~~1~~: \(m\) (kg) <html><input type="number" title="Mass of bob 1." id="txtBob1Mass" min="0.001" max="1" step="0.01" value="0.029" style="width:55px"></html> / \(r\) (m) <html><input type="number" title="Radius of bob 1." id="txtBob1Radius" min="1e-2" max="1" step="0.01" value="0.025" style="width:50px"></html>
!!Bob2 Control
Bob~~2~~: \(m\) (kg) <html><input type="number" title="Mass of bob 2." id="txtBob2Mass" min="0.001" max="1" step="0.01" value="0.029" style="width:55px"></html> / \(r\) (m) <html><input type="number" title="Radius of bob 2." id="txtBob2Radius" min="1e-2" max="1" step="0.01" value="0.025" style="width:50px"></html>
|Lato Lato Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "View Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Lato Lato Panel##Pivot1 Control">>|<<tiddler "Lato Lato Panel##Pivot2 Control">>|
|<<tiddler "Lato Lato Panel##Initial Position1">>|<<tiddler "Lato Lato Panel##Initial Position2">>|
|<<tiddler "Lato Lato Panel##String1 Control">>|<<tiddler "Lato Lato Panel##String2 Control">>|
|<<tiddler "Lato Lato Panel##Bob1 Control">>|<<tiddler "Lato Lato Panel##Bob2 Control">>|
|Show vector: <<tw3DCommonPanel "LinearMotionStatus">>|<<tw3DCommonPanel "Contact Model">> / <<tw3DCommonPanel "Elastic Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel LabelCPS>> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Lato Lato Creation]] [[Lato Lato Initialization]] [[Lato Lato Iteration]]>>|
/***
!! checkStatus
***/
//{{{
scene.checkStatus = () => {
	//pivot[0].angle_indicator.visible = chkIndicator.checked;
	//pivot[1].angle_indicator.visible = chkIndicator.checked;
	bob[0].angle_indicator.visible = chkIndicator.checked;
	bob[0].angle_indicator.label.show(chkIndicator.checked);
	CM.visible = chkCM.checked;
	for(let n=0,N=bob.length; n<N; n++){
		getTrailParam(bob[n]);
		rod[n].arrT.show(chkShowForce.checked);
		rod[n].labelL.show(chkIndicator.checked);
		bob[n].arrV.show(chkShowVelocity.checked);
		bob[n].arrA.show(chkShowAcceleration.checked);
		bob[n].arrFg.show(chkShowForce.checked);
	}
}
//}}}
/***
!! {{{__init_acc__}}}() 
<<<
Initialize acceleration vectors
<<<
***/
//{{{
let __init_acc__ = a => {
	if(!a) a = [];
	if(!a[0] || !a[0].isVector3){
		//if(typeof a.init === 'function'){
		//	a.init();
		//}else{
			a[0] = vector();
			a[1] = vector();
		//}
	}
	return a;
};
//}}}
/***
!! calcAVector(\(\vec r[], \vec v[], t, \vec a[]\)) 
<<<
Calculate the accelerations of the heavy and light objects at positions \(\vec r[]\) and velocities \(\vec v[].\) The argument \(t\) specifies the current time, while \(\vec a[]\) serves as the place holder for the calculated acceleration. All arrays contain the corresponding properties of the pivots in the first half, and that of the bobs in the second, with lighter object before the heavier.
<<<
***/
//{{{
let calcAVector = (r,v,t,a) => {
	a = __init_acc__(a);

	tmpV.copy(r[0]).sub(pivot[0].position);
	let dtheta = findThetaChange(tmpV,false);
	let theta = pivot[0].theta + dtheta;
	let r0 = Hrod.getRadius();
	let rl = tmpV.length()-r0*dtheta;
	let rh = tmpV.copy(r[1]).sub(pivot[1].position).length();
	let dL = r0*(theta-pivot[1].theta);
	let stretch = 0, T = 0, load = 0, hold = 1;
	v[1].value = v[1].length();
	let delta_theta = theta-pivot[1].theta;
	// Calculate string tension.
	stretch = rl+rh+dL-rod.L0;
	T = stretch > 0 ? (stretch*rod.k) : 0;
	//T = stretch*rod.k;

	// Initialize the direction of acceleration
	// [0] is the light, [1] is the heavy object.
	a[0].set(0,r0*Math.sin(theta),r0*Math.cos(theta))
		.sub(r[0]).normalize();
	a[1].copy(pivot[1].position).sub(r[1]).normalize();
	let fc = __m[0]*v[0].lengthSq()/rl;

	if(chkFriction.checked){
		a[0].multiplyScalar(T);
		/*
		let exp = 0;
		if(chkModifiedCapstan.checked){
			if(v[1].value > 1e-4){
				exp = Math.exp(txtMuk.value*delta_theta);
				a[1].multiplyScalar(T*exp+rod.linearDensity*r0*__a[1].z/txtMuk.value*(exp-1));
			}else{
				exp = Math.exp(txtMus.value*delta_theta);
				a[1].multiplyScalar(T*exp);
			}
		}else{
			exp = Math.exp(txtMus.value*delta_theta);
			if(exp>2){
				// static friction is larger than the tension, heavier obj will stop sharply
				a[1].multiplyScalar(v[1].value*__m[1]/(1.5e-1)+scene.g.value*__m[1]);
			}else{
				// static friction is smaller than tension, heavier obj is still possible to slide
				if(v[1].value > 1e-4)
					exp = Math.exp(txtMuk.value*delta_theta);
				a[1].multiplyScalar(T*exp);
			}
		}
		*/
		let exp = Math.exp(txtMus.value*delta_theta);
		if(exp>2){
			// static friction is larger than the tension, heavier obj will stop sharply
			a[1].multiplyScalar(v[1].value*__m[1]/(1.5e-1)+scene.g.value*__m[1]);
		}else{
			// static friction is smaller than tension, heavier obj is still possible to slide
			if(v[1].value > 1e-4){
				exp = Math.exp(txtMuk.value*delta_theta);
				if(chkModifiedCapstan.checked){
					a[1].multiplyScalar(T*exp+rod.linearDensity*r0*__a[1].z/txtMuk.value*(exp-1));
				}else{
					a[1].multiplyScalar(T*exp);
				}
			}else{
				a[1].multiplyScalar(T*exp);
			}
		}
	}else{
		a[0].multiplyScalar(T);
		a[1].multiplyScalar(T);
	}

	// Add drag force if required, then divide by their corresponding
	// mass and add the gravitational acceleration.
	let dragging = chkAirDrag.checked;
	for(let n=0; n<2; n++){
		if(dragging){
			a[n].add($tw.physics.airDragSphere(v[n],bob[n].getRadius(),tmpV));
		}
		a[n].multiplyScalar(1/__m[n]).add(scene.g);
	}

	return a;
}
//}}}
/***
!! calcAScalar(\(q[], \dot q[], t, \ddot q[]\)) 
<<<
Calculate the generalized acceleration of the generalized coordinates of the objects. The argument \(t\) specifies the current time, while \(\ddot q[]\) serves as the place holder for the calculated generalized acceleration. The first argument \(q[]\) shall be an array storing the position, \(r,\) followed by the //angles//, \(\theta,\) of the light and heavy objects, in that order. The second argument \(\dot q[]\) shall store the first derivatives of \(q[].\) The returned array shall contain the acceleration of the distances followed by that of the angles.
<<<
***/
//{{{
let calc_pos_light = (q,pos) => {
	return pos.set(
		0,
		q[0]*Math.cos(q[2]),
		-q[0]*Math.sin(q[2])
	);
};
let calc_pos_heavy = function(q,pos){
	return pos.set(0,0,q[1]);
};
//}}}
//{{{
let calcAScalar = (q,qdot,t,qddot) => {
	if(!qddot) qddot = [0,0,0,0];
	else if(typeof qddot[0]!=='number')
		qddot[0] = qddot[1] = qddot[2] = qddot[3] = 0;

	let qddot_0 = qddot[0];
	let r0 = Hrod.getRadius();
	qddot[0] = -r0*qddot[2]+(
		-__m[1]*scene.g.value
		+__m[0]*(q[0]*qdot[2]*qdot[2]+scene.g.value*Math.sin(q[2]))
	)/__m.total;
	qddot[2] = (
		-__m[1]*scene.g.value*r0+__m[0]*scene.g.value*(
			r0*Math.sin(q[2])+q[0]*Math.cos(q[2])
		)-2*__m[0]*q[0]*qdot[0]*qdot[2]-__m.total*r0*qddot_0
	);
	if(chkFriction.checked){
		calc_pos_light(q,tmpV);
		let dtheta = findThetaChange(tmpV,false);
		let theta = pivot[0].theta + dtheta;
		let r0 = Hrod.getRadius();
		let rl = tmpV.length()-r0*dtheta;
		let rh = calc_pos_heavy(q,tmpV).sub(pivot[1].position).length();
		let dL = r0*(theta-pivot[1].theta);
		let stretch = 0, T = 0;
		let delta_theta = theta-pivot[1].theta;
		// Calculate string tension.
		stretch = rl+rh+dL-rod.L0;
		T = stretch > 0 ? (stretch*rod.k) : 0;
		//T = stretch*rod.k;
		//let exp = Math.exp((v[1].value<1e-6?txtMus.value:txtMuk.value)*delta_theta);
		let exp = Math.exp(txtMus.value*delta_theta);
		if(exp>2){
		}else{
		}
		let f = T*(exp-1);
		qddot[0] -= f/__m.total;
		qddot[2] -= r0*f;
	}
	qddot[2] /= (__m.total*r0*r0+__m[0]*q[0]*q[0]);
	qddot[1] = qddot[0] + r0*qddot[2];
	qddot[3] = 0;
	return qddot;
}
//}}}
/***
!! Update
update_vector() solves the Newton's equation of motion using vectors, while update_scalar applies the Lagragian's method.
!!!! update_pivot
***/
//{{{
let update_pivot = () => {
	tmpV.copy(bob[0].position).sub(pivot[0].position);
	bob[0].theta += findThetaChange(tmpV,true);

	pivotPosition(pivot[0],tmpV,bob[0].theta,bob[0].phi);
}
//}}}
/***
!!!! show_message
***/
//{{{
let show_message = () => {
	let Etot = bob.totalEnergy()+rod.totalEnergy();
	rod[0].T.value = rod[0].T.length();
	rod[1].T.value = rod[1].T.length();
	spanMsg.innerText = 'E='+$tw.ve.round(Etot,4)+' / '
		+$tw.ve.round(bob.E0,4)+' = '+$tw.ve.round(bob.E/bob.E0*100,2)+'%'
		//+'\nT_l='+$tw.ve.round(rod[0].T.value,4)+' T_h='+$tw.ve.round(rod[1].T.value,4)
		//+' T_h/g='+$tw.ve.round(rod[1].T.value/scene.g.value,2)
		+'\nrl('+$tw.ve.round(rod.rl,4)+')+rh('+$tw.ve.round(rod.rh,4)
		+')+dL('+$tw.ve.round(rod.dL,4)+')='+$tw.ve.round(rod.L,4)+' / '+rod.L0+' = '
		+$tw.ve.round(rod.L/rod.L0*100,2)+'%'
		;
	recordData(
		scene.currentTime(),
		null,
		[
			//bob[1].acceleration.length(),
			bob[0].velocity.length(),
			bob[1].velocity.length(),
			//bob.E
		]
	);
}
//}}}
/***
!!!! update_vector
***/
//{{{
let update_vector = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.numeric.ODE.nextValue(
			__r,__v,calcAVector,t_cur,dt,__a,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	bob[0].position.copy(__r[0]);
	bob[1].position.copy(__r[1]);

	bob[0].velocity.copy(__v[0]);
	bob[1].velocity.copy(__v[1]);
	if(scene.currentTime()> 1e-1 && __v[1].length()<1e-2){
		if(!switch_point.visible){
			switch_point.position.copy(bob[0].position);
			switch_point.visible = true;
		}
	}

	bob[0].acceleration.copy(__a[0]);
	bob[1].acceleration.copy(__a[1]);

	update_pivot();
	update_string();
	show_message();
}
//}}}
/***
!!!! update_scalar
***/
//{{{
let update_scalar = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.numeric.ODE.nextValue(
			__q,__qdot,calcAScalar,t_cur,dt,__qddot,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	// Calculate the positions and velocities of the objects.

	let r0 = Hrod.getRadius();
	pivot[0].theta = __q[2];
	pivot[0].position.set(
		0,r0*Math.sin(__q[2]),r0*Math.cos(__q[2])
	);
	/*
	bob[0].position.set(
		0,
		__q[0]*Math.cos(__q[2]),
		-__q[0]*Math.sin(__q[2])
	).add(pivot[0].position);
	bob[1].position.z = __q[1];
	*/
	calc_pos_light(__q,bob[0].position);
	calc_pos_heavy(__q,bob[1].position);

	let cosq2 = Math.cos(__q[2]), sinq2 = Math.sin(__q[2]);
	bob[0].velocity.set(
		0,
		r0*cosq2*__qdot[2]+__qdot[0]*cosq2-__q[0]*sinq2*__qdot[2],
		-r0*sinq2*__qdot[2]-__qdot[0]*sinq2-__q[0]*cosq2*__qdot[2]
	);
	bob[1].velocity.z = __qdot[1];

	bob[0].acceleration.set(
		0,
		__qddot[0]*cosq2+__qddot[2]*(r0*cosq2-__q[0]*sinq2)
			-__qdot[2]*__qdot[2]*(r0*sinq2+__q[0]*cosq2)
			-2*__qdot[2]*__qdot[0]*sinq2,
		-__qddot[0]*sinq2-__qddot[2]*(r0*sinq2+__q[0]*cosq2)
			-__qdot[2]*__qdot[2]*(r0*cosq2-__q[0]*sinq2)
			-2*__qdot[2]*__qdot[0]*cosq2
	);
	bob[1].acceleration.z = __qddot[1];

	update_string();
	show_message();
}
//}}}
/***
!!!! update
***/
//{{{
scene.update = (t_cur,dt) => {
	if(tmpV.copy(bob[0].position).sub(Hrod.position).length()
		<= bob[0].getRadius()+Hrod.getRadius()) return false;

	if(chkVectorEquation.checked)
		update_vector(t_cur,dt);
	else
		update_scalar(t_cur,dt);
}
//}}}
/***
!! Creation
***/
//{{{
let __calculate_k = (r,Y) => {
	// 預設木頭棒子,楊氏係數約為 11 GPa(參考 https://en.wikipedia.org/wiki/Young%27s_modulus)
	// 預設半徑為 0.005m (diameter 1cm)
	r = r || 0.005;
	return (Y || 1.1e10)*(Math.PI*r*r);
}
txtTrailInterval.value = __trail_interval__ = 20;
let arrsw = 0.04
txtTheta0.value = 0;
txtPhi0.value = 90;
let txtStringMass = document.getElementById("txtStringMass");
let txtStringLength = document.getElementById("txtStringLength");
txtStringLength.value = 2;
//txtStringRadius.value = 0.001;
let txtMassLighter = document.getElementById("txtBobMass");
txtMassRatio.value = 10;
txtRodRadius.value = 0.015;
txtStringLengthRatioLight.value = 0.7;

chkGravity.checked = true;
chkGravity.disabled = true;
txtdT.value = 2e-4;
chkVectorEquation.disabled = false;
chkFriction.checked = true;
txtMuk.value = 0.3;
txtMus.value = 0.35;
txtDAQRate.value = 100;
txtTolerance.value = '1e-4';
chkTlLarger.onclick = function(){
	rod.Thlarger = !this.checked;
}
//}}}
/***
!!!! Pivot Creation
***/
//{{{
// Arrange the scene
let pivot = [
	sphere({		// create a sphere to represent the pivot for light object
		radius:0.001,
		//make_trail: false,
		color:0xffff00,
		opacity:0.5
	}),
	sphere({		// create a sphere to represent the pivot for heavy object
		radius:0.001,
		//make_trail: false,
		color:0xff0000,
		opacity:0.5
	})
];

/*
for(let n=0,N=pivot.length; n<N; n++){
	pivot[n].velocity = vector();
	pivot[n].acceleration = vector();
};
*/
//}}}
/***
!!!! Rod Creation
***/
//{{{
let rod = [
	helix({
			pos:pivot[0].position,
			axis:vector(0,0,-1),
			radius:0.0005,
			thickness:0.001,
			color:0xFED162,
			//opacity:0.3,
			coils:60
	}),
	helix({
			pos:pivot[1].position,
			axis:vector(0,0,-1),
			radius:0.0005,
			thickness:0.001,
			color:0xFED162,
			//opacity:0.3,
			coils:60
	})
];

for(let n=0,N=rod.length; n<N; n++){
	rod[n].T = vector();
	rod[n].arrT = arrow({
		shaftwidth:arrsw,
		color:0xffff00,
		label:{
			text: '\\(\\vec T_'+(n?'h':'l')+'\\)',
			size: scene.getDefaultFontSize()*0.5,
			lookAt: vector(1,0,0),
			color: 0xffff00
		}
	});
	rod[n].labelL = label({
		text: '\\(L_'+(n?'h':'l')+'\\)',
		size: scene.getDefaultFontSize()*0.5,
		lookAt: vector(1,0,0),
		color: 0xffff00
	});
}

rod.totalEnergy = () => {
	if(rod.rl === undefined){
		rod.rl = rod[0].L0;
		rod.rh = rod[1].L0;
		rod.dL = contactLength(pivot[0].theta,pivot[1].theta);
		rod.L = rod.L0;
		rod.stretch = 0;
		return 0;
	}
	rod.L = rod.rl+rod.rh+rod.dL;
	rod.stretch = rod.L - rod.L0;
	return rod.stretch > 0
		? (0.5*rod.k*rod.stretch*rod.stretch)
		: 0;
}

let Hrod = cylinder({
	axis: vector(1,0,0),
	radius: 0.01,
	opacity: 0.5
});
Hrod.arrR = arrow({
	axis: vector(0,-Hrod.getRadius(),0),
	shaftwidth:arrsw,
	color:0xffffff
});

let switch_point = sphere({
	visible: false,
	color: 0xff0000
});

pivot[0].angle_indicator = circle({
	radius: Hrod.getRadius(),
	thetaStart: Math.PI/2,
	thetaLength: Math.PI/2,
	segments: 60,
	color: 0xffff00,
	opacity: 0.3
});
pivot[0].angle_indicator.lookAt(vector(1,0,0));

pivot[1].angle_indicator = circle({
	radius: Hrod.getRadius(),
	thetaStart: Math.PI/2,
	thetaLength: Math.PI/2,
	segments: 60,
	color: 0xff0000,
	opacity: 0.3
});
pivot[1].angle_indicator.lookAt(vector(1,0,0));

rod.wrapped = ring({
	Rin: Hrod.getRadius(),
	Rout: Hrod.getRadius()+rod[0].getRadius(),
	segments: 60,
	thetaStart: Math.PI,
	thetaLength: Math.PI/2,
	opacity: 0.3
});
rod.wrapped.lookAt(vector(1,0,0));
//}}}
/***
!!!! Bob Creation
***/
//{{{
let bob = [
	sphere({
		pos: vector(),
		radius: 0.025,
		make_trail: false,
		retain: __trail_len__,
		interval: __trail_interval__,
		opacity: 0.5
	}),
	sphere({
		pos: vector(),
		radius: 0.05,
		make_trail: false,
		retain: __trail_len__,
		interval: __trail_interval__,
		opacity: 0.5
	})
];

bob.totalEnergy = () => {
	bob.KE = 0;
	bob.UE = 0;
	bob.E = 0;
	bob.forEach(function(b){
		b.KE = b.mass*0.5*b.velocity.lengthSq();
		b.UE = b.mass*scene.g.value*b.position.z;
		b.E = b.KE + b.UE;
		bob.KE += b.KE;
		bob.UE += b.UE;
		bob.E += b.E;
	});
	return bob.E;
}

bob[0].angle_indicator = circle({
	radius: Hrod.getRadius()*10,
	thetaStart: 0,
	thetaLength: Math.PI/2,
	segments: 60,
	color: bob[0].getColorHex(),
	opacity: 0.5
});
bob[0].angle_indicator.label = label({
	text: '\\(\\theta_p\\)',
	size: scene.getDefaultFontSize()*0.5,
	lookAt: vector(1,0,0),
	color: bob[0].getColorHex()
});
bob[0].angle_indicator.lookAt(vector(1,0,0));

for(let n=0,N=bob.length; n<N; n++){
	bob[n].velocity = vector();
	bob[n].acceleration = vector();
	bob[n].arrV = arrow({
		shaftwidth:arrsw,
		color:0x00ffff,
		label: {
			text: '\\(\\vec v_'+(n?'h':'l')+'\\)',
			size: scene.getDefaultFontSize()*0.5,
			lookAt: vector(1,0,0),
			color: 0x00ffff
		}
	});
	bob[n].arrA = arrow({
		shaftwidth:arrsw,
		color:0xff00ff,
		label: {
			text: '\\(\\vec a_'+(n?'h':'l')+'\\)',
			size: scene.getDefaultFontSize()*0.5,
			lookAt: vector(1,0,0),
			color: 0xff00ff
		}
	});
	bob[n].arrFg = arrow({
		shaftwidth:arrsw,
		color:0xffffff,
		label: {
			text: '\\('+(n?'M':'m')+'\\vec g\\)',
			size: scene.getDefaultFontSize()*0.5,
			lookAt: vector(1,0,0),
			color: 0xffffff
		}
	});
}
//}}}
/***
!!!! Center of Mass
***/
//{{{
let tmpV = vector();
let TotM = 0, __type = '';
let T = [],  __m = [];
let __r = [], __v = [], __a = [];
let __q = [], __qdot = [], __qddot = [];
let CM = sphere({
	radius: 0.01,
	opacity: 0.5,
	color: 0xffff00
});
let calculateCM = () => {
	CM.position.set(0,0,0);
	for(let n=0,N=bob.length; n<N; n++){
		CM.position.add(
			tmpV.copy(bob[n].position).multiplyScalar(bob[n].mass)
		);
		CM.position.add(
			tmpV.copy(pivot[n].position).multiplyScalar(pivot[n].mass)
		);
		CM.position.add(
			tmpV.copy(rod[n].position).multiplyScalar(rod[n].mass)
		);
	}
	CM.position.multiplyScalar(1/TotM);
};
//}}}
/***
!! calculateTheta\((\vec r)\)
***/
//{{{
let calculateTheta = r => {
	//let theta = Math.acos(r.z/r.length());
	//if(theta < 0) theta += Math.PI*2;
	let theta = Math.atan2(r.y,r.z);
	if(theta < 0) theta += Math.PI*2;
	return theta;
}
let theta_last = 0;
let findThetaChange = (r,tokeep) => {
	let theta = calculateTheta(r);
	let dtheta = theta - theta_last;
	if(Math.abs(dtheta)>Math.PI){
		dtheta += Math.PI*2*(dtheta<0?1:-1);
	}
	if(tokeep) theta_last = theta;
	return dtheta;
}
//}}}
/***
!! pivotPosition
***/
//{{{
let pivotPosition = (pivot,r,theta,phi) => {
	if(typeof theta !== 'number'){
		theta = calculateTheta(r);
	}
	phi = phi || bob[0].phi;
	pivot.theta = theta-Math.PI/2;
	pivot.phi = phi;
	let r0 = Hrod.getRadius();
	let r0sine = r0*Math.sin(pivot.theta);
	pivot.position.set(
		r0sine*Math.cos(pivot.phi),
		r0sine*Math.sin(pivot.phi),
		r0*Math.cos(pivot.theta)
	);
};
//}}}
/***
!! contactLength
***/
//{{{
let contactLength = (theta0, theta1) => {
	// Calculate the length of the rope segment that is in contact with the rod.
	return Hrod.getRadius()*(theta0-theta1);
};
//}}}
/***
!!!! update_indicators
***/
//{{{
let update_indicators = () => {
	let f_T = 1, f_V = 2e-1, f_A = 2e-2;
	for(let n=0,N=bob.length; n<N; n++){
		rod[n].arrT.position.copy(bob[n].position);
		bob[n].arrV.position.copy(bob[n].position);
		bob[n].arrV.setAxis(
			bob[n].velocity.clone().multiplyScalar(f_V)
		);
		bob[n].arrA.position.copy(bob[n].position);
		bob[n].arrA.setAxis(
			tmpV.copy(bob[n].acceleration).multiplyScalar(f_A)
		);
		bob[n].arrFg.position.copy(bob[n].position);
		bob[n].arrFg.setAxis(
			tmpV.copy(scene.g).multiplyScalar(bob[n].mass)
		);
	}
	rod[1].arrT.setAxis(tmpV.copy(rod[1].T).multiplyScalar(f_T));
	rod[0].arrT.setAxis(tmpV.copy(rod[0].T).multiplyScalar(f_T));

	let r0 = Hrod.getRadius();
	pivot[0].angle_indicator.setRadius(r0)
		.setThetaLength(-pivot[0].theta);
	pivot[1].angle_indicator.setRadius(r0)
		.setThetaLength(-pivot[1].theta);
	bob[0].angle_indicator.position.copy(pivot[0].position);
	r0 *= 20;
	bob[0].angle_indicator.setRadius(r0)
		.setThetaLength(-pivot[0].theta);
	let theta = (pivot[0].theta+Math.PI)/2;
	r0 *= 1.75;
	bob[0].angle_indicator.label.setPosition(
		tmpV.set(
			0,r0*Math.sin(theta),r0*Math.cos(theta)
		).add(pivot[0].position)
	);
}
//}}}
/***
!!!! update_string
***/
//{{{
let update_string = () => {
	rod[0].position.copy(pivot[0].position);
	rod[1].position.copy(pivot[1].position);

	tmpV.copy(bob[0].position).sub(pivot[0].position);
	rod[0].setAxis(tmpV);
	rod.rl = tmpV.length();
	rod[0].labelL.setPosition(rod[0].position)
		.shiftPosition(tmpV.multiplyScalar(0.5));

	rod[1].setAxis(tmpV.copy(bob[1].position).sub(pivot[1].position));
	rod.rh = tmpV.length();
	rod.dL = contactLength(pivot[0].theta,pivot[1].theta);
	rod[1].labelL.setPosition(rod[1].position)
		.shiftPosition(tmpV.multiplyScalar(0.5));

	let r0 = Hrod.getRadius();
	rod.wrapped.setInnerRadius(r0).setOuterRadius(r0+rod[0].getRadius())
		.setThetaLength(pivot[1].theta-pivot[0].theta);

	rod[1].T.copy(bob[1].acceleration).sub(scene.g)
		.multiplyScalar(bob[1].mass);
	rod[0].T.copy(bob[0].acceleration).sub(scene.g)
		.multiplyScalar(bob[0].mass);

	update_indicators();
}
//}}}
/***
!! Initialization
***/
//{{{
scene.init = () => {
	TotM = 0;
	let R = +txtRodRadius.value, Rsine = 0;
	Hrod.setRadius(R);
	Hrod.arrR.setLength(R);
	rod.L0 = +txtStringLength.value;		// original length of the rod
	rod.mass = +txtStringMass.value;
	rod.linearDensity = rod.mass / rod.L0;
	for(let n=0,N=pivot.length; n<N; n++){
		pivot[n].theta = (n===0?txtTheta0.value*Math.PI/180.0:-Math.PI/2);
		Rsine = R*Math.sin(pivot[n].theta);
		pivot[n].phi = +txtPhi0.value*Math.PI/180;
		pivot[n].position.set(
			Rsine*Math.cos(pivot[n].phi),
			Rsine*Math.sin(pivot[n].phi),
			R*Math.cos(pivot[n].theta)
		);
		//pivot[n].mass = +txtPivotMass.value;
		//pivot[n].velocity.set(0,0,0);
		//pivot[n].acceleration.set(0,0,0);

		rod[n].setRadius(+txtStringRadius.value);
		let rlratio = +txtStringLengthRatioLight.value;
		if(n===0){
			rod[n].L0 = rod.L0*rlratio;
		}else{
			rod[n].L0 = rod.L0*(1-rlratio);
		}
		rod[n].L0 -= R*Math.abs(pivot[n].theta);
		rod[n].mass = rod.mass*rod[n].L0/rod.L0;
		rod[n].T.set(0,0,0);
		rod[n].T.value = 0;

		//bob[n].clearTrail();
		bob[n].mass = +txtMassLighter.value*(n===0?1:+txtMassRatio.value);		// kg
		bob[n].setRadius(+txtBobRadius.value*
			(n?Math.pow(+txtMassRatio.value,1/3):1)
		);
		bob[n].theta = pivot[n].theta;
		bob[n].phi = pivot[n].phi;
		if(bob[n].theta < 0) bob[n].theta -= Math.PI/2;
		else bob[n].theta += Math.PI/2;
		Rsine = rod[n].L0*Math.sin(bob[n].theta);
		bob[n].position.set(
			Rsine*Math.cos(bob[n].phi),
			Rsine*Math.sin(bob[n].phi),
			rod[n].L0*Math.cos(bob[n].theta)
		).add(pivot[n].position);
		bob[n].clearTrail();
		//bob[n].Fg.set(0,0,-scene.g.value*bob[n].mass);

		//rod[n].position.copy(pivot[n].position);
		//rod[n].setAxis(
		//	tmpV.copy(bob[n].position).sub(pivot[n].position)
		//);
		bob[n].velocity.set(0,0,0);
		bob[n].acceleration.set(0,0,0);

		TotM += bob[n].mass+rod[n].mass+pivot[n].mass;
	}
	rod.k = __calculate_k(+txtStringRadius.value);
	console.log('rod.k=',$tw.ve.round(rod.k,2));
	rod.Thlarger = !chkTlLarger.checked;
	switch_point.position.copy(bob[0].position);
	switch_point.setRadius(bob[0].getRadius());
	switch_point.visible = false;
	calculateCM();
	update_string();

	theta_last = bob[0].theta;

	T.length = 0;
	if(__r[0]) __r[0].copy(bob[0].position);
	else __r[0] = bob[0].position.clone();
	if(__r[1]) __r[1].copy(bob[1].position);
	else __r[1] = bob[1].position.clone();
	if(__v[0]) __v[0].copy(bob[0].velocity);
	else __v[0] = bob[0].velocity.clone();
	if(__v[1]) __v[1].copy(bob[1].velocity);
	else __v[1] = bob[1].velocity.clone();
	if(__a[0] && __a[0].copy)
		__a[0].copy(bob[0].acceleration);
	else
		__a[0] = bob[0].acceleration.clone();
	if(__a[1] && __a[1].copy)
		__a[1].copy(bob[1].acceleration);
	else
		__a[1] = bob[1].acceleration.clone();
	__m[0] = bob[0].mass; __m[1] = bob[1].mass;
	__m.total = __m.reduce(function(sum,m){return sum+m;});
	calcAVector(__r,__v,0,__a);
	__q[0] = tmpV.copy(bob[0].position).sub(pivot[0].position).length();
	__q[1] = tmpV.copy(bob[1].position).sub(pivot[1].position).z;
	__q[2] = pivot[0].theta;
	__q[3] = 0;
	__qdot[0] = bob[0].velocity.length();
	__qdot[1] = bob[1].velocity.length();
	__qdot[2] = 0;
	__qdot[3] = 0;
	calcAScalar(__q,__qdot,0,__qddot);
	__type = '';

	bob.E0 = bob.totalEnergy();
};
//}}}
/***
!!!! Data Plot
***/
//{{{
//dataPlot[0].setYTitle(
//	(config.browser.isIE ? 'Theta~~p~~ (&deg;)' : '\\(\\theta_p\\ (^\\circ)\\)')
//).setTitle('Theta vs Time');
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');

//labelPlot[0].innerHTML = '\\(\\theta_p\\) (&deg;):';
labelPlot[0].innerHTML = '\\(v (\\text{m/s})\\):';
labelPlot[1].innerHTML = '\\(v (\\text{m/s})\\):';

activateDAQChannels(2);
attachDAQBuffer(0,0);
attachDAQBuffer(1,1);
//}}}
/***
!! Camera view angle
***/
//{{{
scene.camera.position.set(txtStringLength.value*400,0,0);
scene.camera.up.set(0,0,1);
//}}}
!! Pivot Control
<<tiddler "Pendulum Panel##Pivot Control">>
!! Rod Control
\(R_\text{rod}\): <html><input type="number" title="Radius of the horizontal rod." id="txtRodRadius" min="0.001" max="0.05" step="0.001" value="0.005" style="width:55px"></html>
!! String Length Control
\(r_l / L_0\): <html><input type="number" title="Ratio of the string in the light object part." id="txtStringLengthRatioLight" min="0.1" max="1" step="0.05" value="0.5" style="width:45px"></html>
!! String Control
<<tiddler "Pendulum Panel##String Control">>
!! Bob Control
<<tiddler "Pendulum Panel##Bob Control">> / \(M_\text{H} / M_\text{L}\): <html><input type="number" title="Mass ratio of heavier to lighter." id="txtMassRatio" min="1" max="50" step="0.1" value="3" style="width:45px"></html>
!! Misc Control
[ =chkIndicator] Indicators
!! Tension Control
[ =chkTlLarger] \(T_l > T_h\) if checked, \(T_l < T_h\) if not.
!! Model Control
[ =chkModifiedCapstan] Modified Capstan
|Looping Pendulum Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Solver Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Looping Pendulum Panel##Misc Control">> / <<tiddler "Looping Pendulum Panel##Tension Control">>|<<tw3DCommonPanel "Friction Control">> / <<tiddler "Looping Pendulum Panel##Model Control">>|
|<<tiddler "Looping Pendulum Panel##Pivot Control">> / <<tiddler "Looping Pendulum Panel##Rod Control">> / <<tiddler "Looping Pendulum Panel##String Length Control">>|<<tiddler "Looping Pendulum Panel##String Control">>|
|<<tw3DCommonPanel "Initial Theta">> (CW from Horizontal) / <<tw3DCommonPanel "Initial Phi">>|<<tiddler "Looping Pendulum Panel##Bob Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Looping Pendulum Initial]] [[Looping Pendulum Codes]]>>|
|Magnetic Train Simulation -- Magnetic Field from Magnets|c
|width:100%;<<tiddler "Magnetic Train Panel##Coil Control">>|width:100%;<<tiddler "Magnetic Train Panel##Magnet Control">>|
|<<tiddler "Magnetic Train Panel##labelTime">>|<<tiddler "Magnetic Train Panel##Field Display Control">>|
|>|<<tw3DScene [[Magnetic Train Code 01]] [[Magnetic Train Code 01-1]]>>|
|Magnetic Train Simulation -- Magnetic Field on Wire|c
|width:100%;<<tiddler "Magnetic Train Panel##Coil Control">>|width:100%;<<tiddler "Magnetic Train Panel##Magnet Control">>|
|<<tiddler "Magnetic Train Panel##labelTime">>|<<tiddler "Magnetic Train Panel##Field Display Control">>|
|>|<<tw3DScene [[Magnetic Train Code 01]] [[Magnetic Train Code 02]] [[Magnetic Train Code 02-1]]>>|
|Magnetic Train Simulation -- Magnetic Force on Wire|c
|width:100%;<<tiddler "Magnetic Train Panel##Coil Control">>|width:100%;<<tiddler "Magnetic Train Panel##labelBattery">>|width:100%;<<tiddler "Magnetic Train Panel##Magnet Control">>|
|<<tiddler "Magnetic Train Panel##labelTime">>|<<tiddler "Magnetic Train Panel##labelForce">>|<<tiddler "Magnetic Train Panel##Force Display Control">>|
|>|>|<<tw3DScene [[Magnetic Train Code 01]] [[Magnetic Train Code 02]] [[Magnetic Train Code 03]]>>|
/***
!!!! Magnets
***/
//{{{
let spinner = [
	cylinder({
		radius: 0.01,				// 10mm
		axis: vector(0,0,0.003),	// 3mm
		color: 0xffff00,			// yellow
		opacity: 0.5,
		make_trail: false
	},'noadd'),
	cylinder({
		radius: 0.01,				// 10mm
		axis: vector(0,0,0.003),	// 3mm
		color: 0xff00ff,			// cyan
		opacity: 0.5,
		make_trail: false
	},'noadd'),
	cylinder({
		radius: 0.01,				// 10mm
		axis: vector(0,0,0.003),	// 3mm
		color: 0xffff00,			// cyan
		opacity: 0.5,
		make_trail: false
	},'noadd')
];
//}}}
!!Problem No. 10 "Magnetic gear"
*Take several identical fidget spinners and attach neodymium magnets to their ends. If you place them side by side on a plane and rotate one of them, the remaining ones start to rotate only due to the magnetic field. Investigate and explain the phenomenon.
!!!Background reading
*Videos
**@@color:red;''This one is interesting!''@@@@"Free Energy" Magnetic Fidget Spinner Motor Real? (youtube, electronicsNmore, 22.07.2017), https://youtu.be/BSdSDfOWbNs@@
**@@Магнитный множитель скорости | Magnetic Games (youtube, Magnetic Games, 12.06.2021), https://youtu.be/1w5Ol05blLE@@
**@@Magnetic Gears (youtube, K&J Magnetics, 03.08.2021), https://youtu.be/HBgjueoZ58Q@@
***@@Magnetic Gears (kjmagnetics.com), https://www.kjmagnetics.com/blog.asp?p=magnetic-gear@@
**https://www.youtube.com/watch?v=76yRObMIwa0
*Articles
**Wikipedia: Magnetic gear, https://en.wikipedia.org/wiki/Magnetic_gear
**P. M. Tlali, R-J. Wang, and S. Gerber. Magnetic gear technologies: A review. Proc. 2014 International Conference on Electrical Machines (ICEM) (Berlin Sept 2-5, 2014)
**K. Atallah and D. Howe. A novel high-performance magnetic gear. IEEE Trans. on Magnetics 37, 4, 2844-2846 (2001)
**C. G. Armstrong. Power Transmitting Device. U.S. Pat. No. 0,687,292 (1901), https://patents.google.com/patent/US687292
**R. Bassani. Dynamic stability of passive magnetic bearings. Nonlinear Dynamics 50,161-168 (2007)
**A. Ya. Krasil'nikov and A. Ya. Krasil'nikov. Calculation of the shear force of highly coercive permanent magnets in magnetic systems With consideration of affiliation to a certain group based on residual induction. Chem. Petroleum Engineering 44, 7-8, 362-365 (2008)
**E. P. Furlani. Permanent magnet and electromechanical devices (Academic Press, San Diego, 2001)
|Circling Magnets Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|[ =chkShowXYZ] ''XYZ'' / <<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<html><input type="radio" name="CameraView" value="top">Top view / <input type="radio" name="CameraView" value="side">Side view / Opacity <input type="number" title="Opacity 0 to 1" id="txtOpacity" min="0" max="1" step="0.05" value="1" style="width:40px"></html> / [ =chkRolling] Rolling|<<tiddler "Magnetic Train Panel##Magnet Control">> / [ =chkShowField] field / [ =chkShowCurrent] Current|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Magnetic Gear Creation]] [[Magnetic Gear Initialization]] [[Magnetic Gear Iteration]]>>|
/***
!!! 共用常數 Global Constants
***/
//{{{
	let solL = 1.0,					// 線圈長度
		solR = 0.009,				// 線圈半徑
		solN = 300,					// 線圈密度
		magL = 0.01,				// 磁鐵長度
		magR = 0.006,				// 磁鐵半徑
		batL = 0.045,				// 電池長度
		batR = 0.0045,				// 電池半徑
		batX = -batL*0.5,			// 電池位置

	// 線圈尺寸
		wireD = 6e-4,				// 銅線直徑 0.6mm(廠商報價單)
		wireA = Math.PI*wireD/4,	// 銅線截面積
//}}}
/***
!!! 座標系 The Coordinate System
***/
//{{{
		axisL = solR * 2,
//}}}
/***
!!! 螺線圈 The Solenoid
***/
//{{{
		solenoid = helix({
			pos: vector(-solL/2,0,0),
			axis: vector(1,0,0),
			color: 0xFED162,
			coils: solL*solN,
			radius: solR,
			radiusSegments: 20,
			length: solL,
			lineWidth: wireD,
			opacity: 0.5
		});
//}}}
/***
!!! 電池 The Battery
***/
//{{{
	const createBattery = () => {
		let bat = group(),
			aLen = batL * 0.05;		// 正極的長度
		bat.add(cylinder({			// 電池本身
			pos: vector(aLen/2,0,(magR-solR)),
			axis: vector((batL-aLen),0,0),
			radius: batR,
			color: 0xff0000,
			opacity: 0.5
		},'noadd')).add(cylinder({	// 正極突出
			pos: vector(-(batL-aLen)/2,0,(magR-solR)),
			axis: vector(aLen,0,0),
			radius: batR*0.6,
			opacity: 0.5
		},'noadd'));
		bat.getPosition = function(r){
			if (r===undefined) r = vector();
			return r.copy(bat.position).add(bat.children[0].position);
		};
		return bat;
	}
	let bat = createBattery();
	scene.add(bat);
//}}}
/***
!!! 磁鐵 The Magnets
***/
//{{{
	let mag = [
		cylinder({						// 用圓柱體來代表磁鐵 0
			pos: vector(),				// 沿 Z 軸擺放,讓磁鐵 0 貼著電池的左端
			axis: vector(magL,0,0),		// 圓柱方向
			radius: magR,				// 半徑為 magR
			color: 0xffff00,			// 顏色為黃色
			opacity: 0.3				// 不透明度為 0.3(半透明)
		}),
		cylinder({						// 用圓柱體來代表磁鐵 1
			pos: vector(),				// 沿 Z 軸擺放,讓磁鐵 1 貼著電池的右端
			axis: vector(magL,0,0),		// 圓柱方向
			radius: magR,				// 半徑為 magR
			color: 0xffff00,			// 顏色為黃色
			opacity: 0.3				// 不透明度為 0.3(半透明)
		})
	];
//}}}
/***
!!! 磁矩 The Magnetic Moment
***/
//{{{
	const magneticDipoleMoment = (param,how) => {
		let mu = arrow(param,how);
		mu.factor = 1;
		mu.BfieldAt = function(r,B){
			// Returns the magnetic field generated by this moment at position r.
			if ( B === undefined ) B = r.clone().normalize();
			else B.copy(r).normalize();
			return B.multiplyScalar(3*mu.axis.dot(B)).sub(mu.axis)
		                .multiplyScalar(mu.factor*1e-7/Math.pow(r.length(),3));
		}
		mu.dxToCenter = function(){
			return (mu.axis.x > 0 ? muL : -muL)/2;
		};
		mu.flip = function(){
			mu.axis.x *= -1;
			mu.setDirection(mu.axis.clone().normalize());
			mu.position.x -= mu.dxToCenter()*2;
			return mu;
		};
		mu.calibrate = function(r,B){
			// Calibrate the magnetic field of this moment to B at position r.
			mu.factor = B/mu.BfieldAt(r).length();
			return mu;
		}
		return mu;
	};
	let muL = magL,							// 磁矩長度
		mu = [
			magneticDipoleMoment({				// 產生磁矩 0
				pos: mag[0].position,			// 放在磁鐵 0 的位置
				dir: vector(1,0,0),				// 箭頭朝向 -Z
				length: muL,					// 長度為 muL
				color: 0xff00ff					// 顏色為 magenta
			}),
			magneticDipoleMoment({				// 產生磁矩 1
				pos: mag[1].position,			// 放在磁鐵 1 的位置
				dir: vector(-1,0,0),			// 箭頭朝向 Z
				length: muL,					// 長度為 muL
				color: 0x00ffff					// 顏色為 cyan
			})
		];

	// 根據實測,磁鐵最大磁場(貼著其中一個極測量)在 4000 高斯(0.4 T)左右,
	// 我們將計算結果校正到這個數值。
	mu[0].calibrate(vector(magL/2,0,0),0.4);
	mu[1].calibrate(vector(magL/2,0,0),0.4);
//}}}
/***
!!! 使用者介面 User Interface
***/
//{{{
	let radioMagnet = [
		document.getElementsByName("mu0"),
		document.getElementsByName("mu1")
	];
	const checkMagnetOrientation = (mu,n) => {
		if ( ! radioMagnet[n] || radioMagnet[n].length===0 ) return;
		if (mu[n].axis.z < 0){
			radioMagnet[n][0].checked = true;
			radioMagnet[n][1].checked = false;
		} else {
			radioMagnet[n][0].checked = false;
			radioMagnet[n][1].checked = true;
		}
	}
	checkMagnetOrientation(mu,0);
	checkMagnetOrientation(mu,1);

	const getRadioMagnetValue = n => {
		return ! radioMagnet[n]
			? 0
			: (radioMagnet[n][0].checked
				? radioMagnet[n][0].value
				: (radioMagnet[n][1].checked
					? radioMagnet[n][1].value
					: 0));
	},
	magnetChanged = which => {
		let dir = getRadioMagnetValue(which);
		if (dir && mu[which].axis.x*dir < 0) {
			mu[which].flip();
			return true;
		}
		return false;
	},
	magnetsChanged = () => {
		let result = false;
		for(let n=0,len=mu.length; n<len; n++)
			result = result || magnetChanged(n);
		return result;
	}

	let txtCD = document.getElementById('txtCD');
	const coilDensityChanged = () => {
		if (solenoid.getCoilDensity() != txtCD.value && txtCD.value >= txtCD.min){
			solenoid = solenoid.setCoilDensity(txtCD.value);
			return true;
		}
		return false;
	},
//}}}
/***
!!! 調整攝影機與場景設定 Adjust Camera and Scene Settings
***/
//{{{
	initBatteryPosition = () => {
		bat.position.set(0,0,0);
		mag[0].position.set(batX-magL/2,0,magR-solR);
		mag[1].position.set(batX+batL+magL/2,0,magR-solR);
		mu[0].position.copy(mag[0].position);
		mu[1].position.copy(mag[1].position);
		mu[0].position.x -= mu[0].dxToCenter ();
		mu[1].position.x -= mu[1].dxToCenter ();
	}
	initBatteryPosition();

	// 設置相機視角
	scene.camera.position.set(0,-50,0);
	scene.camera.up.set(0,0,1);
	scene.camera.lookAt(vector(0,0,0));
//}}}
/***
!!! calculateBFields(\(\mu,\) B[], colorB)
<<<
算出磁鐵周圍的磁場分布
Calculate the B fields around the magnets.
* 作法:在想像中將空間切割成許多小區間,一個一個地對小區間做計算
* 這裡我們使用【球座標 (r, theta, phi)】來將空間分割成小區間
** 球座標可參考 https://zh.wikipedia.org/wiki/%E7%90%83%E5%BA%A7%E6%A8%99%E7%B3%BB
** 磁偶極磁場分布可參考 https://en.wikipedia.org/wiki/Magnetic_dipole
** 圓柱座標可參考 https://zh.wikipedia.org/wiki/%E5%9C%93%E6%9F%B1%E5%9D%90%E6%A8%99%E7%B3%
<<<
***/
//{{{
	const calculateBFields = (mu,B,colorB) => {
		let r1 = vector(1, 0, 0),
			r = vector(magL/2,0,0),
			dB = vector(0, 0, 0);
		if ( ! B ) B = vectorField.create();
		B.max = 0;					// 磁場最大值

		let i = 0,
			BL = magR*3,			// 計算磁場的距離
			drp = BL/3,
			ntheta = 20,			// theta 方向角度切成多少等分
			dtheta = Math.PI / ntheta,
			//nz = 15,
			//dz = BL / nz,
			nphi = 20,				// phi 方向角度切成多少等分
			dphi = Math.PI * 2 / nphi;

		for (let rp=BL; rp <= BL; rp += drp){
			// 第一個迴圈是針對 r 做切割
			for (let theta=dtheta; theta <= Math.PI; theta += dtheta){
				 // 第二個迴圈是針對 theta 角做切割(ntheta 等分)
				for (let phi=dphi; phi <= Math.PI*2; phi+=dphi){
					// 第三個迴圈是針對 phi 角做切割(nphi 等分)
				//for (let z=dz; z <= BL; z += dz){
					// 第三個迴圈是針對 z 軸做切割(nz 等分)

					// 算出小區間位置的位置
					let sintheta = Math.sin(theta);
			                r.set(
						rp*sintheta*Math.cos(phi),		// 小區間位置的 x 座標
						rp*sintheta*Math.sin(phi),		// 小區間位置的 y 座標
						rp*Math.cos(theta)				// 小區間位置的 z 座標
					);
					/*
			                r.set(
						rp*Math.cos(phi),		// 小區間位置的 x 座標
						rp*Math.sin(phi),		// 小區間位置的 y 座標
						z					// 小區間位置的 z 座標
					);
					*/
					r1.copy(r).add(mu.position);		// 對應到磁矩 mu 周圍的這個小區間
					r1.x += mu.dxToCenter();		// 調整到以磁矩中心來計算

					// 計算磁鐵 1 在其相對應的小區間所產生的磁場
					mu.BfieldAt(r,dB);

					let magB = dB.length();			// 磁場的大小
					if (magB > B.max)				// 留住最大的磁場(僅為繪圖用)
						B.max = magB;

					// 產生一個【箭頭】來代表這個磁場
					B.setField(i,{
						pos: r1,				// 箭頭的位置在小區間的位置
						length: magB,			// 箭頭的長度等於磁場大小
						dir: dB.normalize(),		// 箭頭方向等於磁場方向
						color: colorB
					});
					i += 1;
				}
			}
		}
		return B;
	}
//}}}
/***
!!! 計算兩個磁鐵的磁場 Calculate the B field from two magnets.
***/
//{{{
	let B = [
		vectorField.create(),		// 紀錄磁鐵 1 產生的磁場
		vectorField.create()		// 紀錄磁鐵 2 產生的磁場
	];

	const calculateFields = () => {
		let t0 = new Date();
		// 呼叫 calculateBFields() 函數計算磁鐵的磁場
		calculateBFields(mu[0],B[0],mu[0].getColorHex());
		let t1 = new Date();
		calculateBFields(mu[1],B[1],mu[1].getColorHex());

		// Normalize the B field so they look reasonable.
		// 將磁場【規一化(以最大磁場為 1)】讓繪圖結果看起來合理
		let factor = 1/(B[0].max >= B[1].max ? B[0].max : B[1].max)/200;
		B[0].scaleAndShiftToCenter(factor);
		B[1].scaleAndShiftToCenter(factor);
		document.getElementById('labelCalcTime').innerText =
			'B0:'+$tw.ve.round(t1-t0,1)+' / B1:'+$tw.ve.round(new Date()-t1,1);
	}
	calculateFields();
	scene.add(B[0]).add(B[1]);
//}}}
/***
!!! 畫面更新函數 Update function
***/
//{{{
	const update = () => {
		coilDensityChanged();
		if ( magnetsChanged() )
			calculateFields();
		B[0].showField(chkShowB0.checked);
		B[1].showField(chkShowB1.checked);
	}
//}}}
/***
!!! Module Objective
<<<
計算磁鐵在導線上各點所產生的磁力,假設電流走最短路徑,也就是電流只在最靠近電池的磁鐵-線圈接觸點之間流動。
Calculate the magnetic force at each locations of the wire, assuming the shortest path for the current, i.e., current flows only between the magnet-wire contact points that are closest to the battery.

要計算導線上的磁力,我們需要
To calculate magnetic force at the wire, we need to
# 決定導線與磁鐵 1 的接觸點 z1,以及和磁鐵 2 的接觸點 z2(假設磁鐵 1 位於正極而磁鐵 2 位於負極);<br>determine the contact points z1 at which magent 1 touches the wire, and z2 where magnet 2 first touches the wire (assuming magnet 1 at the anode and magnet 2 at the cathod);
# 從 z1 到 z2 沿著導線,以每圈多個點做切割,計算兩個磁鐵在各切割位置所產生的磁力。<br>go around the wire with certain points pur turn, from z1 to z2 and calculate the magnetic fields from both magnets.
<<<
!!! contactZCenter(mag, where)
<<<
計算磁鐵-線圈接觸點。
Calculates the magnet-wire contact point.
參數 Arguments
# mag: 磁鐵。The magnet.
# where: 'right' or 'left'.
<<<
***/
//{{{
	const contactXCenter = (mag,where) => {
		// 計算磁鐵若在線圈正中間時與線圈的接觸點之 x 位置

		if (where === undefined) where = 'right';
		// 從線圈的 +x 端看過來(也可以從 -x 端看,只是算式要做修正)
		let xR = solenoid.position.x + solenoid.getLength();
		let nf = 0, ni = 0;
		let N = solenoid.getCoilDensity();

		if (where === 'right'){
			nf = (xR-mag.position.x-magL/2)*N;		// 磁鐵右端位置對應的圈數(實數)
			ni = Math.ceil(nf);						// 磁鐵右端位置對應的圈數(整數)
			return xR-ni/N;
		}else{
			nf = (xR-mag.position.x+magL/2)*N;		// 磁鐵左端位置對應的圈數(實數)
			ni = Math.ceil(nf);						// 磁鐵左端位置對應的圈數(整數)
			// 往回 1 圈便是磁鐵底部與線圈接觸的點(如果磁鐵在線圈正中間的話)
			return xR-(ni-1)/N;
		}
	},
//}}}
/***
!!! contactAngle (mag)
<<<
計算磁鐵-線圈接觸點對應的角度(與鉛直線之間)。
Calculates the corresponding angle (from the vertical line) of the magnet-wire contact point.
參數 Arguments
# mag: 磁鐵。The magnet.
<<<
***/
//{{{
	contactAngle = mag => Math.atan2(mag.position.z,mag.position.y),
//}}}
/***
!!! contactXShifted (\(x,\theta,\)mag)
<<<
計算磁鐵偏離軸心角度 \(\theta\) 時相較於線圈-桌面接觸點的 //x// 偏移。
Calculate the //x//-shift relative to the wire-table contact point when the magnet is off the axis.
參數 Arguments
# x: 線圈-桌面接觸點位置的 x 分量。The x-component of the wire-table contact point.
# \(\theta\): 偏離軸心的程度(也就是 {{{contactAngle()}}} 所算出的接觸角)。How much the magnet is off the axis (the angle returned from {{{contactAngle()}}}).
# mag: 磁鐵 The magnet.
<<<
***/
//{{{
	contactXShifted = (x,theta,mag) => {
		let dx = 1/solenoid.getCoilDensity();
		x -= (0.25+theta/Math.PI/2)*dx;
		return x >= mag.position.x ? x : (x+dx);
	},
//}}}
/***
!!! contactPoint (mag,where,\(\vec{cp},\)coneCP)
<<<
計算單一磁鐵 mag 跟線圈的 左或右 接觸點(由 {{{where}}} 指示),並調整標示標誌。
Calculate the left or right (specified in {{{where}}}) contact point of a single magnet with the wire, and adjust the marker.
參數 Arguments
# mag: 磁鐵 The magnet.
# where: 'left' or 'right'.
# \(\vec{cp}\): 接收接觸點位置的向量。A vector to receive the contact point.
# coneCP: 標示接觸點位置的物體,比如說一個圓錐體。The marker object, such as a cone, to show where the contact point is.
<<<
***/
//{{{
	contactPoint = (mag,where,cp,coneCP) => {
		// 先計算磁鐵在軸心時與導線的接觸點 x1 之 x 分量,放在 cp.x
		cp.x = contactXCenter(mag, where);
		coneCP.position.x = cp.x;

		// 計算接觸點的 y 及 z 分量,如果磁鐵的位置偏離中心軸的話
		if (mag.position.y != 0){
			// 算出偏離中心軸的轉角
			let R = solenoid.getRadius();
			let theta = contactAngle(mag);
			cp.y = R*Math.cos(theta);	// 此轉角對應到線圈的 y 位置
			cp.z = R*Math.sin(theta);	// 此轉角對應到線圈的 z 位置

			// 計算對應的 x 偏移
			cp.x = contactXShifted(cp.x,theta,mag);

			// 把指示接觸點的圓點也移到相應的位置
			coneCP.position.copy(cp);
		}
		return { point: cp, pointer: coneCP };
	}
//}}}
/***
!!! 變數
<<<
表示接觸點的變數。Variables related to the contact points.
<<<
***/
//{{{
	let cp = [vector(0,0,0), vector(0,0,0)],				// 兩個接觸點
		coneL = axisL / 25,									// 標示接觸點的圓錐長度
		coneCP = [											// 使用兩個圓點標示接觸點
			sphere({										// 圓點 1(標示 z1)
				pos: vector(0,0,-solenoid.getRadius()),		// 放在原點正下方
				radius: coneL,								// 半徑
				color: mu[0].getColorHex(),					// 顏色跟 mu[0] 一樣
				opacity: 0.6
			}),
			sphere({										// 圓點 2(標示 z2)
				pos: vector(0,0,-solenoid.getRadius()),		// 放在原點正下方
				radius: coneL,								// 半徑
				color: mu[1].getColorHex(),					// 顏色跟 mu[1] 一樣
				opacity: 0.6
			})
		];
//}}}
/***
!!! contactPoints ()
<<<
計算兩個磁鐵跟線圈的接觸點。
Calculate the contact points with the wire for both magnets.
<<<
***/
//{{{
	const calculateContactPoints = () => {
		// 呼叫 contactPoint() 函數計算接觸點並顯示出來
		return [
			contactPoint(mag[0],'right',cp[0],coneCP[0]),
			contactPoint(mag[1],'left',cp[1],coneCP[1])
		];
	}
//}}}
/***
!!! 電阻估算 Resistance estimation
<<<
* 一般鹼性電池室溫的內電阻約 0.15 歐姆。<be>In general an alkaline battery has an internal resistance of about 0.15 ohms.
** 參考 (Ref):https://en.wikipedia.org/wiki/Internal_resistance
* 銅線電阻的計算方式如下:<br>The copper wire's resistance is calculated according to: \[R = \rho L/A,\] 其中 \(\rho\) 為銅線的電阻率,\(L\) 為其全長,而 \(A\) 為其截面積。<br>where \(\rho\) is the resistivity, \(L\) is the total length, while \(A\) is the cross sectional area of the wire.
** 參考 (Ref):
*** 銅電阻率 (Copper resistivity):https://en.wikipedia.org/wiki/Electrical_resistivity_and_conductivity
*** 螺線圈全長 (Total length of a coil):https://en.wikipedia.org/wiki/Helix
* 磁鐵電阻由同樣的公式計算。<br>The resistance of magnets is calculated by the same formula.
** 磁鐵的電阻率由下列網址中提到的範圍(\(110 \sim 170 \times 10^{-6} \Omega\cdot\text{m}\))選取中間值 \(140 \times 10^{-6} \Omega\cdot\text{m}\)。<br>The resistance of the magnets is assumed a mid value \(140 \times 10^{-6} \Omega\cdot\text{m}\) of the range \(110 \sim 170 \times 10^{-6} \Omega\cdot\text{m},\) as described in the following page:
** https://en.wikipedia.org/wiki/Neodymium_magnet
<<<
***/
//{{{
	let Rbat = 0.15;
	// 銅線電阻 = 銅電阻率(=1.68e-8 歐姆米) x 長度 / 截面積
	//	參考:https://en.wikipedia.org/wiki/Electrical_resistivity_and_conductivity
	// 繞線長度
	const Lwire = cp => {
		// 計算接觸點之間的銅線長度
		// cp[] 為接觸點,由 contactPoint() 所計算g得到
		// 螺線長度計算參考:https://en.wikipedia.org/wiki/Helix
		let R = solenoid.getRadius();
		let N = solenoid.getCoilDensity();
		let H = 1.0/N/Math.PI/2;										// 螺線管【單圈】的高度 / 2pi
		let L = Math.sqrt(R*R+H*H)*Math.PI*2;			// 單圈的長度
		N *= (cp[1].point.z-cp[0].point.z);						// 接觸點之間的圈數
		return N * L;														// 圈數 x 單圈長度
	},

	// 銅線電阻
	Rwire = cp => 1.68e-8 * Lwire(cp) / wireA;

	// 磁鐵電阻 = 電阻率(110~170e-6 歐姆米)x 長度 / 截面積
	//	參考:https://en.wikipedia.org/wiki/Neodymium_magnet
	let Rmag = 140e-6 * magL / (Math.PI*magR*magR),
		labelR = document.getElementById('labelR');
	const Rtotal = cp => {
		// 總電阻 = 電池內電阻 + 磁鐵電阻x2 + 導線電阻
		let R = Rbat + Rmag*2 + Rwire(cp);
		if (labelR) labelR.innerText = $tw.ve.round(R,3);
		return R;
	}
//}}}
/***
!!! 使用者介面 UI
***/
//{{{
	let txtV = document.getElementById("labelVoltage"),
		batV = (txtV ? +txtV.value : 1.5);
	const voltageChanged = () => {
		let newV = (txtV ? +txtV.value : 1.5);
		if ( newV != batV ) {
			batV = newV;
			return true;
		}
		return false;
	}

	let txtI = document.getElementById("labelCurrent");
	const calculateCurrent = cp => {
		let I = batV/Rtotal(cp);
		if (txtI) txtI.innerText = $tw.ve.round(I,3);
		return I;
	}
//}}}
/***
!!! ~BfieldOnWire(\(\vec \mu, \vec{cp},\) B[], color)
<<<
計算磁鐵在導線上各處產生的磁場。
Calculates the magnetic field on the wire generated by the magnets.
參數 Parameters
# \(\vec \mu\): 磁矩。Magnetic moment.
# \(\vec{cp}\): 兩個磁鐵與線圈的接觸點位置,cp[0] 為靠近電池正極的位置,cp[1] 為靠近負極的位置。Contact points of the two magnets with the coil, while cp[0] is the point close to the anode of the battery, cp[1] is close to the cathode.
# B: 接收線圈各處磁場的陣列。An array to receive the magnet field on the wire.
# color: 表示磁場的箭頭顏色陣列,color[0] 為表示 B[0] 的箭頭顏色,餘依此類推。Array of colors of the arrows representing the magnetic fields, with color[0] being that of the arrows representing B[0], etc.
<<<
***/
//{{{
	const BfieldOnWire = (mu,cp,B,color) => {
		let r = vector(0,0,0);
		let dr = vector();
		let dB = vector();
		let ndx = [
			solenoid.vertexIndex(cp[0].point.x-solenoid.position.x),
			solenoid.vertexIndex(cp[1].point.x-solenoid.position.x)
		];

		for (let i=0, ilen=B.length; i<ilen; i++ ) B[i].max = 0;

		for (let n=ndx[0],nB=0; n<=ndx[1]; n++,nB++){
			// 小段導線位置
			solenoid.localToWorld(solenoid.getVertex(n,r)).multiplyScalar(1/scene.scale.x);

			for (let i=0, ilen=mu.length; i<ilen; i++){
				// 磁鐵 i 在這段小導線處所產生的磁場
				dr.copy(r).sub(mu[i].position);	// 該小段導線從磁鐵 0 測量的相對位置
				dr.x -= mu[i].dxToCenter();		// 調整到以中心點開始測量
				mu[i].BfieldAt(dr,dB);			// 計算磁場
				let magB = dB.length();
				if (magB > B[i].max) B[i].max = magB;

				B[i].setField(nB,{				// 設定磁場
					pos: r,						// 就放在這個小段導線的位置
					dir: dB.normalize(),		// 方向與磁場相同
					length: magB,				// 長度等於磁場大小
					color: color[i]				// 顏色
				});
			}
		}
		return B;
	}
//}}}
/***
!!! calculateFields()
<<<
計算兩磁鐵產生的磁場。
Calculates the magnetic field from both magnets.
<<<
***/
//{{{
	let B = [
		vectorField.create(),			// 紀錄磁鐵 0 在導線上各處產生的磁場
		vectorField.create()			// 紀錄磁鐵 1 在導線上各處產生的磁場
	];
	const calculateFields = () => {
		let t0 = Date.now();
		// 計算接觸點
		let cp = calculateContactPoints();
		let t1 = Date.now();

		// 呼叫 BfieldOnWire() 函數來計算並顯示兩個磁鐵在導線各處的磁場
		BfieldOnWire(
			mu,cp,B,[mu[0].getColorHex(),mu[1].getColorHex()]
		);

		let t2 = Date.now();
		// 將代表磁場的箭頭長度規一化(最大磁場為 1),使其看起來較為合理
		let max = B[0].max > B[1].max ? B[0].max : B[1].max;
		B[0].normalize(max*100);
		B[1].normalize(max*100);
		document.getElementById('labelCalcTime').innerText = 'B: '+$tw.ve.round(t2-t1,1);
		document.getElementById('labelElapsedTime').innerText =
			'CP: '+$tw.ve.round(t1-t0,1)+' / Norm: '+$tw.ve.round(Date.now()-t2,1);
	}
//}}}
/***
!!! update()
<<<
The update function.
<<<
***/
//{{{
	//calculateFields();
	scene.add(B[0]).add(B[1]);

	const update = () => {
		if (coilDensityChanged() || magnetsChanged()){
			calculateFields();
		}
		B[0].showField(chkShowB0.checked);
		B[1].showField(chkShowB1.checked);
	}
//}}}
/***
!!! forceOnFire(\(\vec \mu, \vec{cp},\) //I//, \(\vec F,\) Lseg,color)
<<<
計算磁鐵對線圈產生的磁力。<br>Calculate the magnetic force exerted on the coil by the magnets.
<<<
***/
//{{{
	// 磁鐵
	let visTauW = [ true, true ];		// 磁鐵在線圈上各處所產生的磁【力矩】是否可見

	// 計算磁鐵在導線上各處產生的磁力,其反作用力即為導線對磁鐵所施的力
	const forceOnWire = (mu,cp,I,F,Lseg,color) => {
		let r = vector(),
			dr = vector(),
			dl = vector(),
			dB = vector(),
			dF = vector(),
			magF = 0,
			nF = 0,
			ndx = [
				solenoid.vertexIndex(cp[0].point.x-solenoid.position.x,'right'),
				solenoid.vertexIndex(cp[1].point.x-solenoid.position.x,'left')
			];

		for (let i=0, ilen=F.length; i<ilen; i++ ) {
			if ( ! F[i].total )
				F[i].total = vector();		// 總力
			else
				F[i].total.set(0,0,0);
			F[i].max = 0;					// 最大力
		}

		for (let n=ndx[0]; n<=ndx[1]; n++,nF++){
			// 小段導線位置(內部座標)
			solenoid.getVertex(n,r);
			// 小段導線切線方向
			solenoid.getSegment(n,dl.copy(r));
			// 導線位置轉換為外部座標
			scene.localToWorld(solenoid,r);
			scene.localToWorld(solenoid,dl).sub(solenoid.position);
			Lseg.setField(nF,{
				pos: r,					// 小段導線的位置
				dir: dr.copy(dl).normalize(),
				length: dl.length(),
				color: 0x00ff00			// 顏色為綠
			});

			for (let i=0,ilen=mu.length; i<ilen; i++){
				// 磁鐵 i 在這段小導線處所產生的磁場
				dr.copy(r).sub(mu[i].position);		// 該小段導線從磁鐵 0 測量的相對位置
				dr.x -= mu[i].dxToCenter();			// 調整成從磁矩中心開始測量
				mu[i].BfieldAt(dr,dB);

				// 磁鐵 i 在這段小導線處產生的磁力
				dF.copy(dl).cross(dB).multiplyScalar(I);
				F[i].total.sub(dF);					// 加到【磁鐵受力】
				magF = dF.length();
				if (magF > F[i].max) F[i].max = magF;

				F[i].setField(nF,{
					pos: r,							// 小段導線的位置
					dir: dF.normalize(),			// 長度與方向等於磁力
					length: magF,
					color: color[i]					// 顏色
				});
			}
		}

		// 計算結束,隱藏剩餘的箭頭
		Lseg.clearField(nF,false);
		for (let i=0, ilen=F.length; i<ilen; i++ )
			F[i].clearField(nF);
			//F[i].showField(false,nF);

		for (let i=0, ilen=F.length; i<ilen; i++ ) {
			// 設定代表磁鐵 i 所受總力的箭頭
			if ( ! F[i].arrTotal ) F[i].arrTotal = arrow();
			F[i].arrTotal.position.copy(mu[i].position);		// 放在磁鐵 i 的位置
			F[i].arrTotal.position.x += mu[i].dxToCenter();		// 移到中心位置
			F[i].arrTotal.setAxis(		// 箭頭方向與總力相同,長度以重力為基準
				dF.copy(F[i].total).normalize()
					.multiplyScalar(F[i].total.length()/(-Fg.z)*FgL)
			);
			F[i].arrTotal.setColor(arrFg.getColorHex());		// 顏色
		}
		return F;
	}
//}}}
/***
!!! Variables and Indicators
<<<
變數與
<<<
***/
//{{{

	let mass = 0.03,				// 小火車質量(AAA電池約10g,磁鐵約10gx2)
		Fg = vector(0,0,-mass*9.8),
		FgL = 0.01,
		arrFg = arrow({
			pos: bat.getPosition(),
			dir: vector(0,0,-1),
			length: FgL,
			color: 0xffff00
		}),
		FB = [
			vectorField.create(),			// 紀錄磁鐵 0 在導線上各處產生的磁力
			vectorField.create()			// 紀錄磁鐵 1 在導線上各處產生的磁力
		];
	FB.normalize = max => {
		FB.max = FB[0].max > FB[1].max ? FB[0].max : FB[1].max;
		if ( ! FB.total ) FB.total = vector();
		FB.total.copy(FB[0].total).add(FB[1].total);
		if ( ! FB.arrTotal ) FB.arrTotal = arrow();
		// 設定代表總力的箭頭
		FB.arrTotal.position.copy(bat.getPosition());			// 放在電池中心位置
		FB.arrTotal.setAxis(		// 方向與總力相同,長度以重力為基準
			FB.total.clone().normalize()
				.multiplyScalar(FB.total.length()/(-Fg.y)*FgL)
		);
		FB.arrTotal.setColor(arrFg.getColorHex());		// 顏色

		// 將磁力規一化(最大力為 1)
		FB[0].normalize((max || FB.max)*100);
		FB[1].normalize((max || FB.max)*100);
	}
	let Lseg = vectorField.create();			// 紀錄個小段導線的位置與電流方向

	const calculateForce = () => {
		let t0 = Date.now();
		// 計算接觸點
		let cp = calculateContactPoints();
		// 呼叫 forceAndTorqueWire() 函數來計算並顯示兩個磁鐵在導線各處的磁場
		forceOnWire(
			mu, cp, calculateCurrent(cp), FB, Lseg,
			[mu[0].getColorHex(), mu[1].getColorHex()]
		).normalize();
		FB.calcTime = Date.now() - t0;
	}

	calculateForce();
	document.getElementById('labelCalcTime').innerText = $tw.ve.round(FB.calcTime,1);
	document.getElementById("labelForce").innerText=FB.total.toString(4);
	scene.add(FB[0]).add(FB[1]).add(Lseg);

	//let ni = 0, NI = Lseg.length(), dni = Math.round(NI / 5);
	const showCurrentAndForce = () => {
		FB[0].showField(chkShowFW.checked);
		FB[1].showField(chkShowFW.checked);
		Lseg.showField(chkShowI.checked);
		//if ( ni > NI ) ni = 0;
		//Lseg.showField(chkShowI.checked,ni,ni+dni);
		//ni += dni;
	},

	update = () => {
		if (coilDensityChanged() || magnetsChanged() || voltageChanged()){
			calculateForce();
			document.getElementById('labelCalcTime').innerText=$tw.ve.round(FB.calcTime,1);
			document.getElementById("labelForce").innerText=FB.total.toString(4);
		}
		showCurrentAndForce();
	}
//}}}
/***
!! 變數與初始條件
***/
//{{{
	cartesian.show(false);

	let xmax = solenoid.getLength()/2;			// 磁鐵的最大 x 位置
	let rmax = (solenoid.getRadius()-magR);		// 磁鐵距離軸心的最大值

	let v = vector();		// 初始速度(靜止)
	let dv = vector();		// 速度的變化
	let a = vector();		// 初始加速度(無)
	let dr = vector();		// 位置變化
//}}}
/***
!! 圖形標示物體
***/
//{{{
	let Fn = vector(0,0,-Fg.z);	// 線圈給小火車的正向力
	let arrFn = arrow({			// 代表正向力的箭頭
		pos: bat.getPosition(),
		axis: vector(0,0,FgL),
		color: 0x00ffff
	});

	let friction = vector();
	let arrFriction = arrow({
		pos: bat.getPosition(),
		axis: vector(1e-8,0,0),			// 給一個很小的長度避免 three.js 一直丟錯誤
		visible: false,
		color: 0x00ffff
	});

	let Ftotal = vector().copy(FB.total);	// 小火車所受的總力
	let arrFtotal = arrow({				// 代表總力的箭頭
		pos: bat.getPosition(),
		axis: vector(1e-8,0,0),			// 給一個很小的長度避免 three.js 一直丟錯誤
		color: 0xffff00
	});
//}}}
/***
!! UI
***/
//{{{
	let labelA = document.getElementById("labelA");
	let labelV = document.getElementById("labelV");
	let txtFriction = [
		document.getElementById("txtFriction0"),
		document.getElementById("txtFriction1")
	];

	let ndec = 3;
	labelA.innerText = a.toString(ndec);
	labelV.innerText = v.toString(ndec);
//}}}
/***
!! Data plotters
***/
//{{{
	dataPlot[0].setTitle(
		'Acceleration vs Time'
	).setYTitle(
		(config.browser.isIE ? 'Acceleration (m/s^2)' : '\\(\\vec{a}\\ (\\text{m/s}^2)\\)')
	);

	dataPlot[1].setTitle(
		'Speed vs Time'
	).setYTitle(
		(config.browser.isIE ? 'Velocity (m/s)' : '\\(\\vec{v}\\ (\\text{m/s})\\)')
	);

	labelPlot[0].innerHTML = '\\(\\vec a \\text{ (m/s^2)}\\):';
	labelPlot[1].innerHTML = '\\(\\vec v\\) (m/s):';
//}}}
/***
!! Data recording
***/
//{{{
	const clearData = () => {
		dataPlot[0].clear([0,0.05],[0,0.05]);
		dataPlot[1].clear([0,0.05],[0,0.05]);
	},
//}}}
/***
!! 初始設定
***/
//{{{
	updateForcePositions = () => {
		arrFg.position.copy(bat.position);			// 移動代表重力的箭頭
		arrFtotal.position.copy(bat.position);		// 移動代表總力的箭頭
		FB.arrTotal.position.copy(bat.position);	// 移動代表磁力的箭頭
		arrFn.position.copy(bat.position);			// 移動代表正向力的箭頭
		arrFriction.position.copy(bat.position);	// 移動代表摩擦力的箭頭
		//arrTotalTau.position.copy(bat.position);	// 移動代表總力矩的箭頭
	},

	init = () => {
		initBatteryPosition();
		cp[0].copy(cp[1].set(0,0,0));
		coneCP[0].position.copy(
			coneCP[1].position.set(0,0,-solenoid.getRadius())
		);

		a.set(0,0,0);		// 初始加速度(無)
		v.set(0,0,0);		// 初始速度(靜止)
		labelA.innerText = a.toString(ndec);
		labelV.innerText = v.toString(ndec);

		calculateForce();
		Ftotal.copy(FB.total);
		updateForcePositions();

		clearData();
	}
//}}}
/***
!! Status checking
***/
//{{{
let suspended = false;
scene.checkStatus = () => {
	if (coilDensityChanged() || magnetsChanged() || voltageChanged()){
		calculateForce();
	}
	showCurrentAndForce();
	if(chkCyclic.checked) suspended = false;
}
//}}}
/***
!! The Update function
***/
//{{{
	// 開始計算,先前已經先計算過初始磁力,這裡就從計算
	// 加速度、速度、位移開始
	bat.calcA = () => {
//}}}
/***
!!!! Magnetic forces
***/
//{{{
		Ftotal.copy(FB.total);
//}}}
/***
!!!! Frictional force
***/
//{{{
		friction.set (
			txtFriction[0].value*(v.x>0?1:-1)/10,
			txtFriction[0].value*(v.y>0?1:-1)/10,
			txtFriction[0].value*(v.z>0?1:-1) + txtFriction[1].value*v.z
		);
		if (chkFriction.checked){
			// 加上摩擦力 = consant-bv(正比於速度,符合軟線圈實驗有終端速度的結果)
			//					(花中與彰女用硬線圈/粗線圈似乎沒有終端速度?)
			arrFriction.visible = true;
			Ftotal.add(friction);
		} else
			arrFriction.visible = false;
//}}}
/***
!!!! Gravitational force
***/
//{{{
		Ftotal.z += Fg.z;					// 把重力加入總力
//}}}
/***
!!!! Normal force
***/
//{{{
		// 計算正向力
		Fn = cp[0];						// 先找出磁鐵壓迫線圈的方向
		Fn.x = 0;
		let magFn = Fn.length();
		if (magFn == 0){
			magFn = Fn.z = -Fg.z;			// 沒有橫向的力,正向力抵銷重力
		}else{
			Fn.multiplyScalar(1/magFn);		// 算出單位向量
			magFn = Ftotal.dot(Fn);			// 將合力投影到此方向,
			Fn.multiplyScalar(-magFn);		// 其反向即為正向力
			//labelFn.text = 'Fn = '+format(magFn,'0.5f')+' N'
		}
//}}}
/***
!!!! Total force
***/
//{{{
		Ftotal.add(Fn);						// 把正向力加入總力
		let magFtotal = Ftotal.length();

		//labelTotTau.text = 'Tau = '+Math.round(totalTau.length(),5)+' N*m';
//}}}
/***
!!!! Adjust force arrows
***/
//{{{
		// 調整代表正向力、摩擦力、總力與總力矩的箭頭長度與方向
		arrFn.setAxis(
			Fn.normalize()
				.multiplyScalar(magFn/(-Fg.z)*FgL)
		);
		arrFriction.setAxis(
			Fn.copy(friction).normalize()
				.multiplyScalar(friction.length()/(-Fg.z)*FgL)
		);
		arrFtotal.setAxis(
			Fn.copy(Ftotal).normalize()
				.multiplyScalar(magFtotal/(-Fg.z)*FgL)
		);

		//arrTotalTau.axis.copy(totalTau).multiplyScalar(1/totalTau.length()/100);

		// 計算小火車的加速度、速度、位移
		a.copy(Ftotal).multiplyScalar(1/mass);				// 加速度 = 力 / 質量
	}
//}}}
/***
!!!! next position
***/
//{{{
	bat.nextPosition = () => {
		bat.calcA();
		dv.copy(a).multiplyScalar(scene.timeInterval());	// 速度的變化 dv = a dt
		dr.copy(dv).multiplyScalar(0.5)						// 位置變化(從上一時刻到現在為止)
			.add(v).multiplyScalar(scene.timeInterval());	// dr = v dt + 1/2 dv dt
		v.add(dv);											// 更新速度
	}
//}}}
/***
!!!! update function
***/
//{{{
	bat.update = () => {
		if(suspended) return;
//}}}
/***
!!!! Calculate acceleration, velocity, position.
***/
//{{{
		bat.nextPosition();
//}}}
/***
!!!! Check position validity
***/
//{{{
		mag[0].position.add(dr);						// 移動磁鐵 1
		mag[1].position.add(dr);						// 移動磁鐵 2

		while (mag[0].position.x < solenoid.position.x){
			// 如果跑出線圈的左邊,
			if (chkCyclic.checked){
				// 且在循環模式下,繞回右邊繼續
				let dx = solenoid.position.x+solL-mag[1].position.x;
				mag[0].position.x += dx;
				mag[1].position.x += dx;
				dr.x += dx;
			}else{
				// 不在循環模式下,暫停計算
				suspended = true;
				return;
			}
		}
		while (mag[1].position.x > solenoid.position.x+solL){
			// 如果跑出線圈的右邊,
			if (chkCyclic.checked){
				// 且在循環模式下,繞回左邊繼續
				let dx = mag[0].position.x-solenoid.position.x;
				mag[0].position.x -= dx;
				mag[1].position.x -= dx;
				dr.x -= dx;
			}else{
				// 不在循環模式下,停止計算
				suspended = true;
				return;
			}
		}

		let ca = contactAngle(mag[0]);
		let dmax = Math.abs(rmax * Math.cos(ca));
		let d = mag[0].position.y - dmax;		// 檢查 y 方向是否超出線圈範圍
		if (d > 0){								// 如果磁鐵 1 的 X 位置超過 xmax
												// 表示小火車有側向移動,不能超出線圈
			mag[0].position.y -= d;				// 磁鐵 1 挪回線圈內
			mag[1].position.y -= d;				// 磁鐵 2 挪回線圈內
			dr.y -= d;							// 確保其它部位都在線圈內
		}else{									// 磁鐵 1 並未超出 xmax,檢查另一邊
			d = mag[0].position.y + dmax;
			if (d < 0){							// 磁鐵 1 超出 -xmax
				mag[0].position.y -= d;			// 挪回線圈內
				mag[1].position.y -= d;
				dr.y -= d;						// 確保其它部位都在線圈內
			}
		}

		dmax = Math.abs(rmax * Math.sin(ca));
		d = mag[0].position.z - dmax;			// 檢查 z 方向是否超出線圈範圍
		if (d > 0){								// 同上述
			mag[0].position.z -= d;
			mag[1].position.z -= d;
			dr.z -= d;
		}else{
			d = mag[0].position.z + dmax;
			if (d < 0){
				mag[0].position.z -= d;
				mag[1].position.z -= d;
				dr.z -= d;
			}
		}
//}}}
/***
!!!! Update screen
***/
//{{{
		bat.position.add(dr);						// 移動電池
		mu[0].position.add(dr);						// 移動磁矩 0
		mu[1].position.add(dr);						// 移動磁矩 1
		updateForcePositions();

		//scene.camera.position.x =
		//	bat.position.x;					// 移動攝影機(跟著小火車)
		//scene.camera.lookAt(bat.position);
//}}}
/***
!!!! Calculate magnetic force at the new position
***/
//{{{
		calculateForce();
	}
//}}}
!! Force Display Control
[ =chkShowFW] \(d\vec F_{B}\)  /  [ =chkShowI] \(Id\vec l\)
!! Field Display Control
[X=chkShowB0] Show B[0] / [X=chkShowB1] Show B[1]
!! Flow Control
[ =chkCyclic] Cyclic Coil
!! Friction Control
[X=chkFriction] Friction:<html><input type="number" id="txtFriction0" max="0" step="0.001" value="-0.01" style="width:55px"> + <input type="number" id="txtFriction1" max="0" step="0.01" value="-0.2" style="width:55px"></html> \(\vec{v}\)
!! Magnet Control
Magnets: <html>[&larr;<input type="radio" name="mu0" value="-1"><input type="radio" name="mu0" value="1">&rarr;] / [&larr;<input type="radio" name="mu1" value="-1"><input type="radio" name="mu1" value="1">&rarr;]</html>
!! Coil Control
Coils (turns/m): <html><input type="number" id="txtCD" min="100" max="1000" step="50" value="300" style="width:50px"> / ''R'' (&Omega;): <label id="labelR" style="font-family:'Courier New'"></label></html>
!! labelBattery
''V'' (V): <html><input type="number" id="labelVoltage" min="1" max="24" step="0.1" value="1.5" style="width:40px"></label> / ''I'' (A): <label id="labelCurrent" style="font-family:'Courier New'"></label> / ''T'' (&deg;C): <label id="labelTemp" style="font-family:'Courier New'">???</label></html>
!! labelTime
\(T_\text{calc}\) (ms): <html><label id="labelCalcTime" style="font-family:'Courier New'"></label></html> / \(T_\text{elapsed}\) (ms): <html><label id="labelElapsedTime" style="font-family:'Courier New'"></label></html>
!! labelForce
\(\vec F_{B}\) (N):<html><label id="labelForce" style="font-family:'Courier New'"></label></html>
!! labelA
\(\vec a\) (m/s^^2^^):<html><label id="labelA" style="font-family:'Courier New'"></label></html>
!! labelV
\(\vec v\) (m/s):<html><label id="labelV" style="font-family:'Courier New'"></label></html>
|noborder|k
|Magnetic Train Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tiddler "Magnetic Train Panel##Coil Control">>|<<tiddler "Magnetic Train Panel##Flow Control">> / <<tiddler "Magnetic Train Panel##Friction Control">>|
|<<tiddler "Magnetic Train Panel##labelBattery">>|<<tiddler "Magnetic Train Panel##Magnet Control">> / <<tiddler "Magnetic Train Panel##Force Display Control">>|
|<<tiddler "Magnetic Train Panel##labelTime">>|<<tiddler "Magnetic Train Panel##labelForce">>|
|<<tiddler "Magnetic Train Panel##labelA">>|<<tiddler "Magnetic Train Panel##labelV">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Magnetic Train Code 01]] [[Magnetic Train Code 02]] [[Magnetic Train Code 03]] [[Magnetic Train Code 04]]>>|

<!--{{{-->
<link rel="stylesheet" type="text/css" href="tw.qmo.css">
<script type="importmap">
	{
		"imports": {
			"three": "https://unpkg.com/three@0.161.0/build/three.module.js",
			"three/addons/": "https://unpkg.com/three@0.161.0/examples/jsm/"
		}
	}
</script>
<script type="text/javascript" src="$tw.plugins.min.js"></script>
<script type="text/javascript" src="$tw.VPM.js"></script>
<!--}}}-->

多維 FFT 可以利用一維的函數來完成,減少開發的時間與犯錯的機會,不過也有必須注意的地方,那就是【順序】:逆轉換時順序必須逆著回來。
/***
!! Creation
***/
//{{{
// 假設木頭棒子,楊氏係數約為 11 GPa(參考 https://en.wikipedia.org/wiki/Young%27s_modulus)
// radius = 0.005 (diameter 1 cm)
let __kmax__ = 1.1e10*(Math.PI*Math.pow(0.005,2))

scene.boost(3);

let cradle_sphere = [];
let cradle_string = [];
let cradle_pivot = [];
let tmpV = vector();

txtdT.value = 2.5e-4;
chkGravity.checked = true;
chkGravity.disabled = true;

txtYoungsModulus.value = 1;
chkHertzianModel.checked = true;
//chkAdaptive.checked = false;
//chkAdaptive.disabled = true;
txtTolerance.value = '3e-2';
//}}}
/***
!! Initialization
!!!! Remove existing objects.
***/
//{{{
let n_init = 0;
scene.init = () => {
	if(n_init > 1) n_init = 0;
	let N = +txtSphereNumber.value,
//console.log('N=',N);
		r = +txtSphereRadius.value,
		L = +txtRodLength.value,
		ds = 0;
	if(N !== cradle_sphere.length){
		for(let n=N; n<cradle_sphere.length; n++){
//console.log('removing sphere[',n,']');
			cradle_sphere[n].removeTrail();
			scene.remove(cradle_sphere[n]);
			for(let j=0,J=cradle_pivot[n].length; j<J; j++){
				scene.remove(cradle_pivot[n][j]);
				scene.remove(cradle_string[n][j]);
			}
		}
		//cradle_sphere.length = cradle_string.length = cradle_pivot.length = 0;
		cradle_sphere.length = cradle_string.length = cradle_pivot.length = N;
	}
//}}}
/***
!!!! Crating/Preparing objects
***/
//{{{
	let string_axis = [
		vector(-L/3,0,L).normalize().multiplyScalar(L),
		vector(L/3,0,L).normalize().multiplyScalar(L)
	];
	for(let n=0; n<N; n++){
		if(!cradle_sphere[n]){
//}}}
/***
!!!! Objects not existing, create them
!!!! Creating the spheres
***/
//{{{
			cradle_sphere[n] = sphere({
				pos: vector(0,n*r*2*(1+ds),0),
				radius: r,
				make_trail: false,
				retain: __trail_len__,
				interval: __trail_interval__,
				visible:true,
				opacity: 0.7
			});
//console.log('created sphere[',n,'] at ',cradle_sphere[n].position);
			cradle_sphere[n].velocity = vector();
			cradle_sphere[n].acceleration = vector();
//}}}
/***
!!!! Creating the strings
***/
//{{{
			cradle_string[n] = [
				helix({
					pos:cradle_sphere[n].position,
					axis:string_axis[0],
					radius:+txtRodRadius.value,
					thickness:0.001,
					color:0xFED162,
					visible:true,
					//opacity:0.3,
					coils:60
				}),
				helix({
					pos:cradle_sphere[n].position,
					axis:string_axis[1],
					radius:+txtRodRadius.value,
					thickness:0.001,
					color:0xFED162,
					visible:true,
					//opacity:0.3,
					coils:60
				})
			];
//console.log('created string[',n,']');
//}}}
/***
!!!! Creating the  pivots
***/
//{{{
			cradle_pivot[n] = [
				sphere({
					pos: cradle_sphere[n].position,
					radius: r/2,
					opacity: 0.2
				}),
				sphere({
					pos: cradle_sphere[n].position,
					radius: r/2,
					opacity: 0.2
				})
			];
			cradle_pivot[n].position = vector();
			for(let j=0,J=cradle_pivot[n].length; j<J; j++){
				cradle_pivot[n][j].position.add(
					string_axis[j]
					//cradle_string[n][j].getAxis(tmpV)
				);
				cradle_pivot[n].position.add(cradle_pivot[n][j].position);
			}
			cradle_pivot[n].position.multiplyScalar(1.0/cradle_pivot[n].length);
//console.log('created pivot[',n,'] at ',cradle_pivot[n].position);
//}}}
/***
!!!! End of creating objects
***/
//{{{
		}else{
//}}}
/***
!!!! Objects already existing, prepare them
!!!! Preparing the spheres
***/
//{{{
			cradle_sphere[n].position.set(0,n*r*2*(1+ds),0);
//console.log('setting sphere[',n,'].position to ',cradle_sphere[n].position);
			cradle_sphere[n].setRadius(r);
			cradle_sphere[n].velocity.set(0,0,0);
			cradle_sphere[n].acceleration.set(0,0,0);
//}}}
/***
!!!! Preparing the strings
***/
//{{{
			for(let j=0,J=cradle_pivot[n].length; j<J; j++){
				//cradle_string[n][j].setLength(L);
				cradle_string[n][j].setRadius(+txtRodRadius.value);
				cradle_string[n][j].mass = +txtRodMass.value;
				cradle_string[n][j].position.copy(cradle_sphere[n].position);
				cradle_string[n][j].setAxis(string_axis[j]);
				cradle_string[n][j].L0 = L;
				cradle_string[n][j].k = __kmax__;
			}
		}
//}}}
/***
!!!! Assigning the mass of sphere and clear its trail
***/
//{{{
		cradle_sphere[n].YoungsModulus = +txtYoungsModulus.value*1e9;
		cradle_sphere[n].PoissonsRatio = +txtPoissonsRatio.value;
		cradle_sphere[n].mass = +txtSphereMass.value;
		cradle_sphere[n].clearTrail();
	}
//}}}
/***
!!!! End of Creation/Preparation of objects
!!!! Initialize the motion
***/
//{{{
	let theta = (180-txtTheta0.value)*Math.PI/180;
	let phi = Math.PI/2;
	let NB=+txtSphereNumberToBegin.value;
	/*
	switch(NB){
		case 1: txtdT.value = 2.5e-4; break;
		case 2: txtdT.value = 4e-5; break;
		case 3: txtdT.value = 4e-5; break;
		default:
		case 4: txtdT.value = 4e-5; break;
	}
	*/
	for(let nb=1; nb<=NB; nb++){
		L = tmpV.copy(cradle_sphere[N-nb].position)
				.sub(cradle_pivot[N-nb].position).length();
		cradle_sphere[N-nb].position.set(
			L*Math.sin(theta)*Math.cos(phi),
			L*Math.sin(theta)*Math.sin(phi),
			L*Math.cos(theta)
		).add(cradle_pivot[N-nb].position);
//console.log('lifted sphere[',(N-nb),'] to ',cradle_sphere[N-nb].position);
		for(let j=0,J=cradle_pivot[N-nb].length; j<J; j++){
			cradle_string[N-nb][j].position.copy(
				cradle_sphere[N-nb].position
			);
			cradle_string[N-nb][j].setAxis(
				tmpV.copy(cradle_pivot[N-nb][j].position).sub(
					cradle_sphere[N-nb].position
				)
			);
		}
	}
//}}}
/***
!!!! Initialize DAQ buffers
***/
//{{{
	initializeDAQ(N);
	return ++n_init > 1;
}
//}}}
/***
!!!! Data Plot
***/
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');
//dataPlot[1].setYTitle(
//	(config.browser.isIE ? 'a (m/s)' : '\\(a (\\text{m/s}^2)\\)')
//).setTitle('Acceleration vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'E (J)' : '\\(E (\\text{J)\\)')
).setTitle('Energy vs Time');

labelPlot[0].innerHTML = '\\(v (\\text{m/s})\\):';
//labelPlot[1].innerHTML = '\\(a (\\text{m/s}^2)\\):';
labelPlot[1].innerHTML = '\\(E (\\text{J})\\):';

let initializeDAQ = function(N){
	activateDAQChannels(N*2+1);
	for(let n0=0,n1=N; n0<N; n0++,n1++){
		attachDAQBuffer(0,n0);
		attachDAQBuffer(1,n1);
	}
	attachDAQBuffer(1,2*N);
}
//}}}
/***
!! Camera view angle
***/
//{{{
scene.camera.position.set(100,0,0);
scene.camera.up.set(0,0,1);
//}}}
/***
!! checkStatus
***/
//{{{
scene.checkStatus = () => {
	for(let n=0,N=cradle_sphere.length; n<N; n++){
		getTrailParam(cradle_sphere[n]);
	}
}
//}}}
/***
!! calcA(\(\vec r[], \vec v[], t, \vec a[]\)) 
<<<
Calculate the accelerations of each of the spheres in a Newton's cradle.
<<<
***/
//{{{
let calcA = (r,v,t,a) => {
	let dragging = chkAirDrag.checked;
	let N = r.length;
	for(let n0=0; n0<N; n0++){
		a[n0].set(0,0,0);
	}
	for(let n0=0; n0<N; n0++){
		for(let j=0,J=cradle_pivot[n0].length; j<J; j++){
			tmpV.copy(r[n0]).sub(cradle_pivot[n0][j].position);
			cradle_string[n0][j].dL = tmpV.length()-cradle_string[n0][j].L0;
			a[n0].add(
				tmpV.normalize().multiplyScalar(
					-cradle_string[n0][j].k*cradle_string[n0][j].dL
				)
			);
		}
		// Add drag force if desired.
		if(dragging){
			a[n0].add($tw.physics.airDragSphere(v[n0],cradle_sphere[n0].getRadius(),tmpV));
		}
		a[n0].multiplyScalar(1.0/cradle_sphere[n0].mass).add(scene.g);
	}
}
//}}}
/***
!! Update
***/
//{{{
let data_v = [];
let data_E = [];
let fPE = 1e2, fKE = 1e4;
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.physics.nextPosition(
			cradle_sphere,calcA,t_cur,dt,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	$tw.physics.objCheckCollisions(cradle_sphere,chkHertzianModel.checked);

	cradle_sphere.E = 0;
	cradle_string.E = 0;
	let N=cradle_sphere.length;
	for(let n=0; n<N; n++){
		cradle_sphere[n].PE = cradle_sphere[n].mass*scene.g.value*cradle_sphere[n].position.y*fPE;
		cradle_sphere[n].KE = 0.5*cradle_sphere[n].mass*cradle_sphere[n].velocity.lengthSq()*fKE;
		cradle_sphere.E += cradle_sphere[n].PE + cradle_sphere[n].KE;

		for(let j=0,J=cradle_pivot[n].length; j<J; j++){
			cradle_string[n][j].position.copy(cradle_sphere[n].position);
			cradle_string[n][j].setAxis(
				tmpV.copy(cradle_pivot[n][j].position).sub(cradle_sphere[n].position)
			);
			cradle_string[n][j].PE = 0.5*cradle_string[n][j].k*Math.pow(cradle_string[n][j].dL,2)*fPE;
			cradle_string.E += cradle_string[n][j].PE;
		}

		data_v[n] = cradle_sphere[n].velocity.length();
		data_E[n] = cradle_sphere[n].KE+cradle_sphere[n].PE+cradle_string[n].PE;
	}
	data_E[N] = cradle_sphere.E + cradle_string.E;

	recordData(scene.currentTime(),null,[
		data_v, data_E
	]);
}
//}}}
!! Sphere Control
Sphere number: <html><input type="number" title="number of spheres" id="txtSphereNumber" min="0" max="20" step="1" value="5" style="width:35px"></html> / \(r\) (m): <html><input type="number" title="radius of spheres" id="txtSphereRadius" min="0" max="0.1" step="0.005" value="0.01" style="width:55px"></html> / \(m\) (kg): <html><input type="number" title="mass of spheres" id="txtSphereMass" min="0" max="1" step="0.001" value="0.05" style="width:55px"></html>
!! Rod Control
<<tiddler "Pendulum Panel##Rod Control">>
!! Initial Number of Spheres
To begin with //N//:: <html><input type="number" title="number of spheres" id="txtSphereNumberToBegin" min="0" max="30" step="1" value="1" style="width:35px"></html>
|Newton's Cradle Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "View Control">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Newton's Cradle Panel##Sphere Control">>|<<tiddler "Newton's Cradle Panel##Rod Control">>|
|<<tw3DCommonPanel "Initial Theta">> (CCW from Vertical) / <<tiddler "Newton's Cradle Panel##Initial Number of Spheres">>|<<tw3DCommonPanel "Contact Model">> / <<tw3DCommonPanel "Elastic Properties">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel LabelCPS>> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Newton's Cradle Initial]] [[Newton's Cradle JS Codes]]>>|
/***
!! Camera position and settings
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(0.5);
chkGravity.checked = chkGravity.disabled = true;
chkAutoCPF.checked = true;
chkPivotMovable.disabled = true;
txtKpivot.disabled = true;
txtdT.value = 0.001;
// Size and mass of tennis balls obtained from https://en.wikipedia.org/wiki/Tennis_ball
txtBobRadius.value = 0.05;
txtBobMass.value = 0.01;
txtStringRadius.value = 0.002;
txtStringMass.value = 0.001;
txtStringLength.value = 0.5;
txtStringKz.value = 1000;
txtStringKphi.value = 0.001;
txtOpacity.value = 0.5;
txtTheta0.value = 180;
txtPhi0.value = 60;
txtTolerance.value = '1e-3';
scene.single_coordinate_system = false;
//}}}
/***
!! Hanging Sphere
***/
//{{{
let pendulum = $tw.physics.Pendulum.create({
		bob:{
			wireframe: true
		}
	}),
	mark = sphere({
		color: '#FF0000',
		opacity: 0.7
	}),
	ball = sphere({
		wireframe: true
	}),
	r_angle = +txtBobRadius.value*2,
	theta0 = +txtTheta0.value/180*Math.PI,
	theta_mark = +txtThetaMark.value/180*Math.PI,
	angle_zr = circle({
		radius: r_angle,
		thetaStart: -theta_mark,
		thetaLength: theta_mark,
		color: cylindrical.color[2]
	}),
	label_zr = label({
		text: '\\(\\theta\\)',
		size: '14pt',
		color: cylindrical.color[2]
	}),
	angle_rhor = circle({
		radius: r_angle,
		thetaStart: -Math.PI/2,
		thetaLength: Math.PI/2-theta_mark,
		color: cylindrical.color[1]
	}),
	label_rhor = label({
		text: '\\({\\pi \\over 2}-\\theta\\)',
		size: '10pt',
		color: cylindrical.color[1]
	}),
	angle_rhotheta = circle({
		radius: r_angle,
		thetaStart: -(Math.PI/2+theta_mark),
		thetaLength: theta_mark,
		color: cylindrical.color[0]
	}),
	label_rhotheta = label({
		text: '\\(\\theta\\)',
		size: '14pt',
		color: cylindrical.color[0]
	});
scene.add(pendulum);
attachMotionIndicators(pendulum.bob);
//}}}
/***
!!! updateMark(r_mark)
***/
//{{{
function updateMark(r_mark){
	if(r_mark)
		mark.position.copy(r_mark);
	else
		r_mark = mark.position;

	spherical.updateStatus(r_mark);
	cylindrical.updateStatus(r_mark);

	let matrix = spherical.makeRotationRZ();
	angle_zr.setPosition(r_mark);
	angle_zr.setRotationFromMatrix(matrix);
	angle_rhor.setPosition(r_mark);
	angle_rhor.setRotationFromMatrix(matrix);
	angle_rhotheta.setPosition(r_mark);
	angle_rhotheta.setRotationFromMatrix(matrix);

	let axis = vector();
	spherical.showLabelTheta(
		spherical.axis_arrow[0].getAxis(axis),
		label_zr,angle_zr,
		theta_mark,1.4
	);
	spherical.showLabelTheta(
		spherical.axis_arrow[1].getAxis(axis),
		label_rhotheta,angle_rhotheta,
		theta_mark,1.3
	);
	spherical.showLabelTheta(
		cylindrical.axis_arrow[0].getAxis(axis),
		label_rhor,angle_rhor,
		Math.PI/2-theta_mark,2
	);
}
//}}}
/***
!!! Creation and Initialization
***/
//{{{
let	R = 0.5,
	ball = sphere({
		radius: R,
		wireframe: true
	}),
	theta_mark = 30/180*Math.PI,
	phi = 60/180*Math.PI,
	r_mark = vector(
		R*Math.sin(theta_mark)*Math.cos(phi),
		R*Math.sin(theta_mark)*Math.sin(phi),
		R*Math.cos(theta_mark)
	),
	mark = sphere({
		radius: R/20,
		color: '#FF0000',
		opacity: 0.7
	}),
	r_angle = R/5,
	angle_zr = circle({
		radius: r_angle,
		thetaStart: -theta_mark,
		thetaLength: theta_mark,
		color: cylindrical.color[2]
	}),
	label_zr = label({
		text: '\\(\\theta\\)',
		size: '8pt',
		color: cylindrical.color[2]
	}),
	angle_rhor = circle({
		radius: r_angle,
		thetaStart: -Math.PI/2,
		thetaLength: Math.PI/2-theta_mark,
		color: cylindrical.color[1]
	}),
	label_rhor = label({
		text: '\\({\\pi \\over 2}-\\theta\\)',
		size: '6pt',
		color: cylindrical.color[1]
	}),
	angle_rhotheta = circle({
		radius: r_angle,
		thetaStart: -(Math.PI/2+theta_mark),
		thetaLength: theta_mark,
		color: cylindrical.color[0]
	}),
	label_rhotheta = label({
		text: '\\(\\theta\\)',
		size: '8pt',
		color: cylindrical.color[0]
	}),
	phi_dot = 30/180*Math.PI;
dt = 0.001;
scene.textBookView();
scene.camera.position.multiplyScalar(R);
scene.single_coordinate_system = false;
chkSpherical.checked = chkCylindrical.checked = true;
for(let n=0; n<3; n++){
	spherical.axis_label[n].setSize('10pt');
	cylindrical.axis_label[n].setSize('10pt');
}
updateMark(r_mark);
//}}}
/***
!!! scene.initialized
***/
//{{{
scene.initialized = function(){
	rate(10);
}
//}}}
/***
!!! scene.update
***/
//{{{
scene.update = function(){
	phi += phi_dot*dt;
	ball.rotation.z = phi;
	mark.position.set(
		R*Math.sin(theta_mark)*Math.cos(phi),
		R*Math.sin(theta_mark)*Math.sin(phi),
		R*Math.cos(theta_mark)
	);
	updateMark();
}
//}}}
/***
!! updateMark
***/
//{{{
const updateMark = (r_mark) => {
	if(r_mark)
		mark.position.copy(r_mark);
	else
		r_mark = mark.position;

	spherical.updateStatus(r_mark);
	cylindrical.updateStatus(r_mark);

	let matrix = spherical.makeRotationRZ();
	angle_zr.setPosition(r_mark);
	angle_zr.setRotationFromMatrix(matrix);
	angle_rhor.setPosition(r_mark);
	angle_rhor.setRotationFromMatrix(matrix);
	angle_rhotheta.setPosition(r_mark);
	angle_rhotheta.setRotationFromMatrix(matrix);

	let axis = vector();
	spherical.showLabelTheta(
		spherical.axis_arrow[0].getAxis(axis),
		label_zr,angle_zr,
		theta_mark,1.3
	);
	spherical.showLabelTheta(
		spherical.axis_arrow[1].getAxis(axis),
		label_rhotheta,angle_rhotheta,
		theta_mark,1.2
	);
	spherical.showLabelTheta(
		cylindrical.axis_arrow[0].getAxis(axis),
		label_rhor,angle_rhor,
		Math.PI/2-theta_mark,1.7
	);
}
//}}}
/***
!! Scene.init
***/
//{{{
let factor = 3e2,
	vis = false;
scene.init = () => {
	vis = chkSpherical.checked && chkCylindrical.checked;
	theta_mark = +txtThetaMark.value/180*Math.PI;
	let L0 = +txtStringLength.value,
		phi0 = +txtPhi0.value/180*Math.PI,
		R = +txtBobRadius.value,
		Rb = R, //*2,
		r_bob = vector(
			L0*Math.sin(theta0)*Math.cos(phi0),
			L0*Math.sin(theta0)*Math.sin(phi0),
			L0*Math.cos(theta0)
		),
		r0 = vis ? ball.position : r_bob,
		r_mark = vector(
			Rb*Math.sin(theta_mark)*Math.cos(phi0),
			Rb*Math.sin(theta_mark)*Math.sin(phi0),
			Rb*Math.cos(theta_mark)
		).add(r0);

	ball.setRadius(Rb);
	spherical.setPosition(r0);
	cylindrical.setPosition(r0);

	pendulum.setPosition(null,r_bob);
	pendulum.bob.angular_position.set(0,0,phi0);
	pendulum.movables = [
		pendulum.pivot,
		pendulum.bob
	];

	mark.setRadius(Rb/10);
	updateMark(r_mark);

	pendulum.bob.setRadius(R);
	pendulum.bob.setMass(+txtBobMass.value);
	pendulum.bob.setOpacity(+txtOpacity.value);

	pendulum.string.k = +txtStringKz.value;
	pendulum.string.kphi = +txtStringKphi.value;
	pendulum.string.setRadius(+txtStringRadius.value);
	pendulum.string.setMass(+txtStringMass.value);
	pendulum.string.setOpacity(+txtOpacity.value);

	pendulum.calculateAngularAcceleration(
		pendulum.bob.angular_position,
		pendulum.bob.angular_velocity,
		pendulum.bob.angular_acceleration
	);
	updateMotionIndicators(pendulum.bob,factor);
	scene.checkStatus();
}
//}}}
/***
!! calcA()
***/
//{{{
const calcA = (r,v,t,a) => {
	return pendulum.calculateAcceleration(r,v,a);
}
//}}}
/***
!! calcAlpha()
***/
//{{{
const calcAlpha = (theta,omega,t,alpha) => {
	alpha[1] = pendulum.calculateAngularAcceleration(theta[1],omega[1],alpha[1]);
	return alpha;
}
//}}}
/***
!! scene.update()
***/
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		// calculate next position using 4th order Runge-Kutta mehtod
		delta_pos = $tw.physics.nextPosition(
			pendulum.movables,calcA,t_cur,dt,adaptive,e0
		),
		delta_angle = $tw.physics.nextAngle(
			pendulum.movables,calcAlpha,t_cur,dt,adaptive,e0
		);
	pendulum.adjustString();
	updateMotionIndicators(pendulum.bob,factor);
	if(adaptive) setdT(Math.min(delta_pos[2],delta_angle[2]));

	vis = chkSpherical.checked && chkCylindrical.checked;
	let R = ball.getRadius(),
		theta_mark = +txtThetaMark.value/180*Math.PI,
		r0 = vis ? ball.position : pendulum.bob.position,
		phi = pendulum.bob.angular_position.z;

	mark.position.set(
		R*Math.sin(theta_mark)*Math.cos(phi),
		R*Math.sin(theta_mark)*Math.sin(phi),
		R*Math.cos(theta_mark)
	).add(r0);

	spherical.setPosition(r0);
	cylindrical.setPosition(r0);
	updateMark();
}
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	if(typeof chkMakeTrail !== 'undefined'){
		getTrailParam(pendulum.bob);
	}
	if(typeof chkShowForce !== 'undefined'){
		pendulum.showForce(chkShowForce.checked);

		showVelocityIndicator(pendulum.bob,chkShowVelocity.checked);
		showAccelerationIndicator(pendulum.bob,chkShowAcceleration.checked);
		showForceIndicator(pendulum.bob,chkShowForce.checked);

		showAngularVelocityIndicator(pendulum.bob,chkShowAngularVelocity.checked);
		showAngularAccelerationIndicator(pendulum.bob,chkShowAngularAcceleration.checked);
		showTorqueIndicator(pendulum.bob,chkShowTorque.checked);
	}

	vis = chkSpherical.checked && chkCylindrical.checked;
	ball.show(vis);
	angle_zr.show(vis);
	label_zr.show(vis);
	angle_rhor.show(vis);
	label_rhor.show(vis);
	angle_rhotheta.show(vis);
	label_rhotheta.show(vis);
}
//}}}
! Legendre 電荷密度對應之電位勢
在已知電荷密度為 Legendre 形式的前提下,''如果尚未學過求解 Poisson 方程'',我們可以透過比較 Poisson 方程和 Legendre 方程來【看出】電位勢也應為 Legendre 形式。
*球座標的 Poisson's equation \begin{equation}\nabla ^2 V = {\partial^2 V \over \partial r^2} + {2\over r}{\partial V \over \partial r} + {1 \over r^2}{\partial^2 V \over \partial \theta^2} + {1\over r^2}{\cos\theta \over \sin\theta}{\partial V \over \partial \theta} + {1 \over r^2\sin^2\theta}{\partial^2 V \over \partial \phi^2} = -{\rho \over \epsilon_o},\tag{1}\end{equation} associalted Legendre equation \begin{equation}(1-x^2){d^2 \over dx^2}P^m_\ell(x) - 2x{d \over dx}P^m_\ell(x) + \left[\ell(\ell+1)-{m^2 \over 1-x^2}\right]P^m_\ell(x) = 0. \tag{2}\end{equation}
!!! 第一種情況:均勻磁場平行於旋轉軸
此時 \(\vec B = B\hat z, \quad \omega = \omega \hat z\),且電荷密度解為 \begin{equation}\rho = -2\epsilon_o\omega BP^0_2(\cos\theta).\tag{3}\end{equation} 可以看出 (3) 式僅有 \(\theta\) 一個變數,和 \(\phi\) 無關,可預期 \(V\) 也只有 \(\theta\) 一個變數,因此我們讓 (1) 式只保留 \(\theta\) 微分項:\begin{align} & {1 \over r^2}{d^2 V \over d\theta^2} + {1\over r^2}{\cos\theta \over \sin\theta}{d V \over d\theta} = 2\omega BP^0_2(\cos\theta) \\ \text{乘以 } r^2 \to \quad & \boxed{ {d^2 V \over d\theta^2} + {\cos\theta \over \sin\theta}{d V \over d\theta} = 2r^2\omega BP^0_2(\cos\theta).}\tag{4}\end{align} 另根據 (2) 式我們寫出 \(P^0_2(x)\) 對應的微分方程:\[(1-x^2){d^2 \over dx^2}P^0_2(x) - 2x{d \over dx}P^0_2(x) + 2(2+1)P^0_2(x) = 0,\] 再把 \(x\) 置換成 \(\cos\theta\) 得到 \[(1-\cos\theta^2){d^2 \over d(\cos\theta)^2}P^0_2(\cos\theta) - 2\cos\theta{d \over d(\cos\theta)}P^0_2(\cos\theta) = -2(2+1)P^0_2(\cos\theta),\] 再利用 \begin{align}{df \over d(\cos\theta)} &= {df \over d\theta}{d\theta \over d(\cos\theta)} = {df \over d\theta}{-1 \over \sin\theta} \tag{5a} \\ {d^2f \over d(\cos\theta)^2} &= {d \over d(\cos\theta)}\left({df \over d\theta}{-1 \over \sin\theta}\right) = {d \over d\theta}\left({df \over d\theta}{-1 \over \sin\theta}\right){-1 \over \sin\theta} \tag{5b} \end{align} 把對 \(\cos\theta\) 微分展開成對 \(\theta\) 微分 \begin{align} & \sin^2\theta\left[\left({d^2 \over d\theta^2}P^0_2(\cos\theta)\right){-1 \over \sin\theta} + \left({d \over d\theta}P^0_2(\cos\theta)\right){\cos\theta \over \sin^2\theta}\right]{-1 \over \sin\theta} - 2\cos\theta \left({d \over d\theta}P^0_2(\cos\theta)\right){-1 \over \sin\theta} = -2(2+1)P^0_2(\cos\theta) \nonumber\\ \to \quad & {d^2 \over d\theta^2}P^0_2(\cos\theta) - {\cos\theta \over \sin\theta} {d \over d\theta}P^0_2(\cos\theta) + 2{\cos\theta \over \sin\theta} {d \over d\theta}P^0_2(\cos\theta) = -2(2+1)P^0_2(\cos\theta) \nonumber\\ \to \quad & \boxed{ {d^2 \over d\theta^2}P^0_2(\cos\theta) + {\cos\theta \over \sin\theta}{d \over d\theta}P^0_2(\cos\theta) = -2(2+1) P^0_2(\cos\theta),}\tag{6}\end{align} 其中我們將第一項的 \(1-\cos^2\theta\) 換成 \(\sin^2\theta\)。

比較 (4)、(6) 二式可以很快看出 \[\large \boxed{V = -{\omega Br^2 \over 3}P^0_2(\cos\theta).}\]
!!!第二種情況:均勻磁場垂直於旋轉軸
此時 \(\vec B = B\hat x, \quad \omega = \omega \hat z\),且電荷密度解為 \begin{equation}\rho = \epsilon_o\omega BP^1_2(\cos\theta)\cos\phi.\tag{7}\end{equation} 可以看出 (7) 式有 \(\theta\) 和 \(\phi\) 兩個變數,可預期 \(V\) 也會有同樣兩個變數,因此我們讓 (1) 式有 \(\theta\) 和 \(\phi\) 的微分項都保留:\begin{equation}{1 \over r^2}{d^2 V \over d\theta^2} + {1\over r^2}{\cos\theta \over \sin\theta}{d V \over d\theta} + {1 \over r^2\sin^2\theta}{\partial^2 V \over \partial \phi^2} = -\omega BP^1_2(\cos\theta)\cos\phi.\tag{8}\end{equation} 另根據 (2) 式我們寫出 \(P^1_2(x)\) 對應的微分方程:\[(1-x^2){d^2 \over dx^2}P^1_2(x) - 2x{d \over dx}P^1_2(x) + \left[2(2+1)-{1 \over 1-x^2}\right]P^1_2(x) = 0,\] 再把 \(x\) 置換成 \(\cos\theta\) 得到 \[(1-\cos\theta^2){d^2 \over d(\cos\theta)^2}P^1_2(\cos\theta) - 2\cos\theta{d \over d(\cos\theta)}P^1_2(\cos\theta) = -\left[2(2+1)-{1 \over 1-\cos^2\theta}\right]P^1_2(\cos\theta),\] 同樣利用 \(1-\cos^2\theta = \sin^2\theta\) 以及 (5a)、(5b),把對 \(\cos\theta\) 微分展開成對 \(\theta\) 微分 \begin{equation} \boxed{ {d^2 \over d\theta^2}P^1_2(\cos\theta) + {\cos\theta \over \sin\theta}{d \over d\theta}P^1_2(\cos\theta) = -\left[2(2+1)-{1 \over \sin^2\theta}\right] P^1_2(\cos\theta).}\tag{9}\end{equation}

比較 (8)、(9) 二式可以看出,Poisson 方程 (8) 包含兩個變數 \((\theta, \phi)\),而 Legendre 方程 (9) 則僅有一個變數 \(\theta\),我們得先把 (8) 式中和 \(\phi\) 相關的部分先討論一下才能和 (9) 式進行比對。從 (8) 式可以看出,【左邊與 \(\phi\) 相關的僅有 \(\partial^2 V / \partial \phi^2\) 一項】,而【右邊與 \(\phi\) 相關的僅有 \(\cos\phi\)】,因此可以合理將電位勢 \(V\) 寫成 \[V = V_\theta(\theta)\cos\phi,\] 其中 \(V_\theta\) 僅為 \(\theta\) 的函數。這樣一來 (8) 式可以改寫成 \[{1 \over r^2}{d^2 V_\theta \over d\theta^2}\cos\phi + {1\over r^2}{\cos\theta \over \sin\theta}{d V_\theta \over d\theta}\cos\phi + {1 \over r^2\sin^2\theta}V_\theta(-\cos\phi) = -\omega BP^1_2(\cos\theta)\cos\phi,\] 兩邊同乘 \(r^2\) 及除以 \(\cos\phi\) 之後就得到 \begin{equation}\boxed{ {d^2 V_\theta \over d\theta^2} + {\cos\theta \over \sin\theta}{d V_\theta \over d\theta} - {1 \over \sin^2\theta}V_\theta = -r^2\omega BP^1_2(\cos\theta).} \tag{10}\end{equation} 比較 (9)、(10) 二式不難看出 \[\large \boxed{V_\theta = {\omega Br^2 \over 6}P^1_2(\cos\theta) \quad \to \quad V = V_\theta \cos\phi = {\omega Br^2 \over 6}P^1_2(\cos\theta)\cos\phi.}\]
|Oscillating Sphere Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">> / <<tw3DCommonPanel "Contact Model">>|<<tw3DCommonPanel "Trail Control">>|
|Pivot: <<tw3DCommonPanel "Pivot Properties">>|Wire: <<tw3DCommonPanel "String Geometry">>|
|Ball: <<tw3DCommonPanel "Bob Properties">>|Wire: <<tw3DCommonPanel "String Properties">>|
|<<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">> / <<tw3DCommonPanel "Opacity Control">> / <html>\(\theta_\text{mark}\) (&deg;): <input type="number" title="Theta to show mark." id="txtThetaMark" min="0" max="180" step="0.1" value="30" style="width:40px"></html>|<<tw3DCommonPanel "Spherical">> / <<tw3DCommonPanel "Cylindrical">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Oscillating Sphere Creation]] [[Oscillating Sphere Initialization]] [[Oscillating Sphere Iteration]]>>|
https://raw.githubusercontent.com/parallel-js/parallel.js/master/lib/parallel.js
https://github.com/austinksmith/Hamsters.js/blob/dev/build/hamsters.min.js
/***
!!! A collection of pendulums
***/
//{{{
let Pendulums = function(n){
	let pendulums = [];
	pendulums.create = function(n){
		if(n < 1) n = 1;
		let L = +txtRodLength.value;
		let len = pendulums.length;
		if(n > len){
			// n > length
			for(let i=pendulums.length; i<n; i++){
				pendulums[i] = new Pendulum({
					rod: new Rod({
						radius: 0.003,
						axis: vector(0,-txtRodLength.value,0),
						color: 0xff0000,
						length: L
					},'noadd'),
					bob: new Bob({
						radius: 0.05,
						color: 0x00ff00,
						opacity: 0.3
					},'noadd'),
					pos: vector()
				});
			}
		}else if(n < len){
			// n < length
			for(let i=len-1; i>=n; i--){
				pendulums[i].destroy();
			}
			pendulums.splice(n);
		}
	};
	if(typeof n !== 'number' || n <= 0)
		n = +txtNP.value;
	pendulums.create(n);

	let randomShift = function(max){
		return (max||1)*Math.random()*(Math.random()<0.5?-1:1);
	};

	pendulums.init = function(){
		let N = pendulums.length, n;
		let L = +txtRodLength.value;
		let a0 = txtInitialAngle.value*Math.PI/180;
		let dr = vector(0,0,0);
		let sep = [], width = 0;
		//let movable = chkMovable.checked;
		let pos = vector(), p;
		for(n=0; n<N; n++){
			p = pendulums[n];
			p.initialAngle.set(0,0,1);
			p.initialAngle.value = a0*(1+Math.random()/10)*(n%2?1:-1);
			p.setLength(L*(1+Math.random()/10));
			p.init();
			sep[n] = p.length*Math.sin(p.initialAngle.value)*3;
			width += sep[n];
		}
		// Adjust the ceiling width if necessary.
		if(ceiling.getWidth()<width)
			ceiling.setWidth(width);
		// Determine the maximum separation.
		let maxsep = sep[0];
		for(n=1; n<N; n++)
			if(maxsep < sep[n]) maxsep = sep[n];
		dr.set(maxsep,0,0);
		// Determine the position of the first pendulum.
		// In a 1D case, the first pendulum is the left-most one.
		// We will arrange the pendulums in a symmetrical way.
		let r0 = N === 1
			?	ceiling.position.clone()
			:	dr.clone().multiplyScalar(
					(N % 2
						? -(N-1)/2.0		// even number, half-half
						: -N/2)				// odd number, one at the center, others half-half
				).add(ceiling.position);
		// Now set the pendulum positions.
		for(n=0; n<N; n++){
			pos.copy(dr).multiplyScalar(n).add(r0);
			p = pendulums[n];
			p.setPosition(pos);
			p.calculateVertical();
		}
		return pendulums;
	};
	// 代表張力的箭頭 The arrow representing the tension
	pendulums.arrT = arrow({
		axis: pendulums[0].string.tension.clone(),
		color: 0x00ffff
	});
	//pendulums.arrT.name = 'arrT';

	pendulums.force = vector();
	// 代表合力的箭頭 The arrow representing the total force
	pendulums.arrF = arrow({
		axis: pendulums.force,
		color: 0xff0000
	});
	//pendulums.arrF.name = 'arrT';

	// 代表力矩的箭頭 The arrow representing the torque
	pendulums.arrTau = arrow({
		axis: pendulums[0].torque,
		color: 0xffff00
	});
	//pendulums.arrTau.name = 'arrT';

	// 代表速度的箭頭 The arrow representing the velocity
	pendulums.arrV = arrow({
		axis: pendulums[0].velocity,
		color: 0x00ff00
	});
	//pendulums.arrV.name = 'arrT';

	// 代表加速度的箭頭 The arrow representing the acceleration
	pendulums.arrA = arrow({
		axis: pendulums[0].acceleration,
		color: 0xffffff
	});
	//pendulums.arrA.name = 'arrT';
//}}}
/***
!!!! pendulums.calculateAccelerations (//x//[], //v//[], //t//, //dt//, //a//[])
<<<
根據目前所有的擺與天花板的質心位置與速度,計算所有物體的加速度。
Calculate the accelerations of all the pendulums and the ceiling, using their current center of mass positions and velocities.
* 第一個引數 //x//[] 是一個陣列,其元素為所有擺的質心位置,最後一個元素為天花板的質心位置。<br>The first argument //x//[] is an array, of which the elements are the pendulums' center of mass positions, while the last element is the ceilings center of mass position.
* 第二個引數 //v//[] 也是一個陣列,其元素為所有擺的質心速度,最後一個元素為天花板的質心速度。<br>The second argument //v//[] is also an array, of which the elements are the pendulums' center of mass velocity, while the last element is the ceilings center of mass velocity.
* 第三個引數 //t// 為現在時刻。<br>The third argument //t// is the current time.
* 第四個引數 //dt// 為時間間隔。<br>The fourth argument //dt// is the time step.
* 最後一個引數(非必要)//a//[] 也是一個陣列,其元素為所有擺的質心加速度,最後一個元素為天花板的質心加速度。<br>The last argument (optional) //a//[] is also an array, of which the elements are the pendulums' center of mass acceleration, while the last element is the ceilings center of mass acceleration.
* 傳回一個陣列,其元素為所有擺的質心加速度,最後一個元素為天花板的質心加速度。<br>Returns an array, of which the elements are the pendulums' center of mass accelerations, while the last element is the ceiling's center of mass acceleration.
<<<
***/
//{{{
	pendulums.totalA = vector();
	pendulums.calculateAccelerations = function(x, v, t, dt, a){
		let n, p, len = pendulums.length, ac = 0;
		let pa = [];
		//if(!chkOneD || chkOneD.checked){
			if(chkMovable.checked){
				pa.length = len+1;
				ac = a ? a[len] : ceiling.acceleration.x;
				// Calculate the acceleration of the ceiling.
				pa[len] = 0;
				for(n=0; n<len; n++){
					p = pendulums[n];
					pa[len] -=
						p.mass*p.length*(
							(a?a[n]:p.alpha.z)*Math.cos(x[n])-
								v[n]*v[n]*Math.sin(x[n])
						);
				}
				pa[len] /= (pendulums.mass+ceiling.mass);
			}else
				pa.length = len;

			// Calculate the angle of each pendulum.
			for(n=0; n<len; n++){
				p = pendulums[n];
				pa[n] = -(
					-scene.g.y*Math.sin(x[n])+
					ac*Math.cos(x[n])
				)/p.length;
				//p.alpha.set(0,0,pa[n]);
			}
		//}else{
		/*
			if(chkMovable.checked) len--;
			let tmpa = vector();
			pendulums.totalA.set(0,0,0);
			for(n=0; n<len; n++){
				p = pendulums[n];
				pa[n] = p.calculateAcceleration(x[n],v[n],t,dt,a[n]);
				pendulums.totalA.add(
					tmpa.copy(pa[n]).multiplyScalar(p.mass)
				);
			}
			if(chkMovable.checked){
				pa[len] = pendulums.totalA.clone()
					.projectOnPlane(ceiling.normal)
					.multiplyScalar(-1/ceiling.mass);
			}
		}
		*/
		return pa;
	};
//}}}
/***
!!!! pendulums.calculateCM ()
> 計算全部擺的質心。
> Calculate the center of mass for all the pendulums.
***/
//{{{
	pendulums.rcm = vector();
	pendulums.calculateCM = function(){
		pendulums.rcm.set(0,0,0);
		let tmp = vector();
		for(let n=0,len=pendulums.length; n<len; n++){
			pendulums.rcm.add(tmp.copy(pendulums[n].rcm).multiplyScalar(pendulums[n].mass));
		}
		return pendulums.rcm.multiplyScalar(1/pendulums.mass);
	};
//}}}
/***
!!!! pendulums.rCMs (rcm)
> 取得所有擺的質心位置,存放在陣列裡。引數 rcm 為非必要,如果有傳入,則必須是陣列,且所有擺的質心位置會存放在裡面。
> Returns the center of mass positions of all pendulums in an array. The argument rcm is optional. If given, it must be an array, and all the pendulum's center of mass positions will be stored in it.
***/
//{{{
	pendulums.rCMs = function(rcm){
		if(!rcm) rcm = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			rcm[n] = pendulums[n].rcm;
		}
		return rcm;
	};
//}}}
/***
!!!! pendulums.vCMs (vcm)
> 取得所有擺的質心速度,存放在陣列裡。引數 vcm 為非必要,如果有傳入,則必須是陣列,且所有擺的質心速度會存放在裡面。
> Returns the center of mass velocities of all pendulums in an array. The argument vcm is optional. If given, it must be an array, and all the pendulum's center of mass velocities will be stored in it.
***/
//{{{
	pendulums.vCMs = function(vcm){
		if(!vcm) vcm = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			vcm[n] = pendulums[n].velocity;
		}
		return vcm;
	};
//}}}
/***
!!!! pendulums.getThetas (\(\theta\)[])
> 取得所有擺的現在角度,存放在陣列裡。引數 \(\theta\) 為非必要,如果有傳入,則必須是陣列,且所有擺的角度會存放在裡面。
> Returns the current angles of all pendulums in an array. The argument \(\theta\) is optional. If given, it must be an array, and all the pendulum angles will be stored in it.
***/
//{{{
	pendulums.getThetas = function(theta){
		if(!theta) theta = [];
		for(let n=0,p,len=pendulums.length; n<len; n++){
			p = pendulums[n];
			theta[n] = p.angle.z > 0 ? p.angle.value : -p.angle.value;
		}
		return theta;
	};
	pendulums.getAngles = pendulums.getThetas;
//}}}
/***
!!!! pendulums.getOmegas (\(\omega\)[])
> 取得所有擺的現在角速度,存放在陣列裡。引數 \(\omega\) 為非必要,如果有傳入,則必須是陣列,且所有擺的角速度會存放在裡面。
> Returns the current angular speed of all pendulums in an array. The argument \(\omega\) is optional. If given, it must be an array, and all the pendulum angular speeds will be stored in it.
***/
//{{{
	pendulums.getOmegas = function(omega){
		if(!omega) omega = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			omega[n] = pendulums[n].omega.z;
		}
		return omega;
	};
	pendulums.getAngularSpeeds = pendulums.getOmegas;
//}}}
/***
!!!! pendulums.getAlphas (\(\alpha\)[])
> 取得所有擺的現在角加速度,存放在陣列裡。引數 \(\alpha\) 為非必要,如果有傳入,則必須是陣列,且所有擺的加角速度會存放在裡面。
> Returns the current angular acceleration of all pendulums in an array. The argument \(\alpha\) is optional. If given, it must be an array, and all the pendulum angular accelerations will be stored in it.
***/
//{{{
	pendulums.getAlphas = function(alpha){
		if(!alpha) alpha = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			alpha[n] = pendulums[n].alpha.z;
		}
		return alpha;
	};
	pendulums.getAngularAccelerations = pendulums.getAlphas;
//}}}
/***
!!!! pendulums.updateLinearStatus ()
<<<
更新擺的運動狀態,如速度、加速度等。
Update the status of the pendulum's motion, such as linear velocity and linear acceleration.
<<<
***/
//{{{
	pendulums.updateLinearStatus = function(){
		// 調整代表速度的箭頭 Adjust the representing arrow for velocity.
		pendulums.arrV.setAxis(
			pendulums.arrV.axis.copy(pendulums[0].velocity)
					.multiplyScalar(vafactor)
		);
		pendulums.arrV.position.copy(pendulums[0].rcm);
		pendulums.arrV.visible = chkShowVA.checked;

		// 調整代表加速度的箭頭 Adjust the representing arrow for acceleration.
		pendulums.arrA.setAxis(
			pendulums.arrA.axis.copy(pendulums[0].acceleration)
					.multiplyScalar(vafactor)
		);
		pendulums.arrA.position.copy(pendulums[0].rcm);
		pendulums.arrA.visible = chkShowVA.checked;

		// 調整代表張力的箭頭 Adjust the representing arrow for tension.
		pendulums.arrT.setAxis(
			pendulums.arrT.axis.copy(pendulums[0].string.tension)
					.multiplyScalar(lengthfactor)
		);
		pendulums.arrT.position.copy(pendulums[0].rcm);
		pendulums.arrT.visible = chkShowF.checked;

		pendulums.force.copy(fg).add(pendulums[0].string.tension);
		// 調整代表合力的箭頭 Adjust the representing arrow for net force.
		pendulums.arrF.setAxis(
			pendulums.arrF.axis.copy(pendulums.force)
					.multiplyScalar(lengthfactor)
		);
		pendulums.arrF.position.copy(pendulums[0].rcm);
		pendulums.arrF.visible = chkShowF.checked;

		// 調整代表重力的箭頭 Adjust the representing arrow for gravitational force.
		arrFg.position.copy(pendulums[0].rcm);
		arrFg.visible = chkShowF.checked;

		return pendulums;
	};

	return pendulums;
};
//}}}
/***
!!! Data recording
***/
//{{{
let clearData = function(){
	dataPlot[0].clear([0,2],[0,0.05]);
	dataPlot[1].clear([0,2],[0,0.05]);

	//for(let n=1, N=pendulums.length; n<N; n++){
	//	dataPlot[0].addYData($tw.data.dataArray());
	//	dataPlot[1].addYData($tw.data.dataArray());
	//}
};
let recordData = function(){
	dataPlot[0].addXPoint(t_cur);
	for(let n=0,N=pendulums.length; n<N; n++){
		let p = pendulums[n];
		dataPlot[0].addYPoint(
			p.angle.value/Math.PI*180
				*(p.angle.dot(p.initialAngle)>0?1:-1),
			n
		);
	}
};
//}}}
/***
!!! Status functions
***/
//{{{
let checkStatus = function(){
};
//}}}
/***
!! Initialization Section
Initialize scene and variables.
!!!! The scene and axes.
***/
//{{{
let Rcm = vector();
let sphereRcm = sphere({
	radius: 0.02,
	opacity: 0.6
});
//}}}
/***
!!!! UI widgets
***/
//{{{
let labelT = document.getElementById('labelPeriod');
//}}}
/***
!! Data visualization: The plotters
***/
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'Angle (&deg;)' : '\\(\\theta\\ (^\\circ)\\)')
).setTitle('Angle vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'F (N)' : '\\(\\vec F\\) (N)')
).setTitle('Force on Ceiling vs Time');

labelDataPlot[0].innerHTML = '\\(\\theta\\ (^\\circ)\\)';
labelDataPlot[0].innerHTML = '\\(\\vec F\\) (N)';
//}}}
/***
!! The ceiling
***/
//{{{
let ceiling = box({
	size: 2,
	height: 0.002,
	color:0xbbbbbb
});
ceiling.mass = +txtPivotMass.value;
ceiling.normal = vector(0,0,-1);
ceiling.velocity = vector();
ceiling.acceleration = vector();
ceiling.lastPosition = vector();
//ceiling.name = 'ceiling';
//}}}
/***
!! The pendulums
***/
//{{{
let pendulums = new Pendulums();
//}}}
/***
!! updatePositions ()
> 更新計算過後所有擺的位置,以及天花板的位置,如果天花板可以移動的話。
> Update the positions of all pendulums after calculations, and that of the ceiling, if it is movable.
***/
//{{{
let updatePositions = function(x,v,a){
	let len = pendulums.length;
	if(!x){
		// Nothing is passed in, just update the current positions.
		for(let n=0,p; n<len; n++){
			pendulums[n].setPosition();
			pendulums[n].calculateLinearStatus();
		}
	}else{
		if(chkMovable.checked){
			ceiling.lastPosition.copy(ceiling.position);
			ceiling.position.x = x[len];
			ceiling.velocity.x = v[len];
			ceiling.acceleration.x = a[len];
			let dr = vector();
			dr.copy(ceiling.position).sub(ceiling.lastPosition);
			for(let n=0; n<len; n++){
				pendulums[n].position.add(dr);
				//pendulums[n].setPosition();
				pendulums[n].calculateVertical();
			}
		}
		//if(!chkOneD || chkOneD.checked){
			for(let n=0,p; n<len; n++){
				p = pendulums[n];
				p.alpha.set(0,0,a[n]);
				p.omega.set(0,0,v[n]);
				p.angle.set(0,0,(x[n]>=0 ? 1 : -1));
				p.angle.value = x[n]>=0 ? x[n] : (-x[n]);
				p.setAngle();
				p.setPosition();
				p.calculateLinearStatus();
			}
		//}else{
		//	for(let n=0; n<len; n++){
		//		pendulums[n].setPosition(null,pendulums[n].rcm);
		//	}
		//}
	}

	pendulums.calculateCM();
	calculateSystemCM();
	pendulums.updateLinearStatus();
};
//}}}
/***
!! Calculating the System's Center of Mass
***/
//{{{
let calculateSystemCM = function(){
	// Calculate Rcm of ceiling and pendulums
	Rcm.copy(ceiling.position).multiplyScalar(ceiling.mass).add(
		pendulums.rcm.clone().multiplyScalar(pendulums.mass)
	).multiplyScalar(
		1/(ceiling.mass+pendulums.mass)
	);
	sphereRcm.position.copy(Rcm);
	sphereRcm.visible = chkCM.checked;
};
//}}}
/***
!! The Reset funciton
***/
//{{{
let reset = function(){
	ceiling.acceleration.set(0,0,0);
	ceiling.velocity.set(0,0,0);
	ceiling.position.set(0,0.7,0);
	ceiling.lastPosition.copy(ceiling.position);
	simT = 0;

	NPChanged();
	massChanged();
	lengthChanged();
	angleChanged();
	timeIntervalChanged();

	pendulums.init();
	updatePositions();
	pendulums.updateLinearStatus();
	showPeriod((T=0));
	clearData();
	calculateSystemCM();
	showPositions();
};
chkMovable.title = "Movable ceiling.";
chkMovable.nextSibling.title = chkMovable.title;
if(optPivot)
	optPivot.onchange = function(){
		if(optPivot.value === 'mouse')
			scene.activateRaycaster(pendulums);
		else
			scene.deActivateRaycaster();
		reset();
	}
chkMovable.onclick = reset;
//if(chkRKFour){
//	chkRKFour.title = 'Checked: 4th order Runge-Kutta algorighm.\nUnchecked: Euler method.';
//	chkRKFour.onclick = reset;
//}
chkRandom.title = 'Random initial positions.';
chkRandom.onclick = reset;
//if(chkOneD){
//	chkOneD.title = 'Checked: 3D calculations.';
//	chkOneD.onclick = reset;
//}
txtInitialAngle.onchange = reset;
txtBobMass.onchange = reset;
txtRodMass.onchange = reset;
txtPivotMass.onchange = reset;
txtFineSteps.onchange = reset;
//}}}
/***
!! The Recreation Function
***/
//{{{
let recreate = function(){
	let N = pendulums.length;
	let n;
	for(n=0; n<N; n++){
		scene.remove(pendulums[n]);
	}
	pendulums = new Pendulums(+txtNP.value);
	reset();
};
txtNP.onchange = reset;
txtRodLength.onchange = reset;
//}}}
/***
!! Gravitational force
***/
//{{{
let lengthfactor = 0.1;
let vafactor = 0.5;
let fg = scene.g.clone().multiplyScalar(pendulums[0].mass);
// 代表重力的箭頭 The arrow to represent the gravitational force
let arrFg = arrow({
	axis: fg.clone().multiplyScalar(lengthfactor),
	color: 0xff00ff
});
//}}}
/***
!! Initial angles, camera positions, simulation time steps, etc.
***/
//{{{
// 設定單擺的初始狀態 Set the initial state of pendulum
let dt = +txtdT.value;
let simT = 0;
reset();
checkPlots();
// 移動攝影機到適當位置 Move the camera to a proper position
camera.position.y = ceiling.position.y - pendulums[0].string.getLength()*0.8;
//camera.position.z = 8;
//scene.activateRaycaster();
scene.createTrackballControl();
//}}}
/***
!! update function (that will be called by tw3jsPlugin periodically)
<<<
這個 update 函數大約會以 20~60 次/秒的頻率執行(視模擬物體的多少、計算複雜度、以及電腦與瀏覽器效能而定),在這裡我們將會
This update function is executed at roughly 40~60 times/sec (depending on the performance of computer and browser), in which we will
# 應用 [[龍格-庫塔(Runge-Kutta)演算法|https://zh.wikipedia.org/wiki/%E9%BE%99%E6%A0%BC%EF%BC%8D%E5%BA%93%E5%A1%94%E6%B3%95]] 來求解各運動方程<br>Apply the [[Runge-Katta algorithm|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]] to solve the equations of motion \[\vec a_i = {d^2\vec r_i \over dt^2} = \vec a_{i,c} + \vec a_{i,t} = {v_i^2 \over r_i}(-\hat r_i) + \vec\alpha_i \times \vec r_i,\] 其中 \(\vec a_i\) 是第 //i// 個擺的質心加速度,會隨著該擺的質心位置 \(\vec r_i\)(從該擺的轉軸點量起)、質心速度 \(\vec v_i\),以及角加速度 \(\vec \alpha_i\) 而變,細節請參考上方 {{{pendulum.calculateAcceleration()}}} 及 {{{pendulum.calculateAlpha()}}} 裡的說明。<br> where \(\vec a_i\) is the center of mass acceleration of the //i//^^th^^ pendulum, which changes with its center of mass position \(\vec r_i\) (measured from its pivot position), its center of mass velocity \(\vec v_i,\) and its angular acceleration \(\vec \alpha_i.\) See the explanations in {{{pendulum.calculateAcceleration()}}} and {{{pendulum.calculateAlpha()}}} for details.
# 更新畫面。Update the screen.
<<<
***/
//{{{
	let x = [], v = [], a = [];
	let update = function(){
		//if (NPChanged() || massChanged() || lengthChanged()
		//	|| angleChanged() || timeIntervalChanged()){
		//	reset();
		//	return;
		//}

		if (paused()){
			updatePositions();
		}else{
			let n = txtFineSteps.value;
			for(let i=0,dtn=dt/n;i<n;i++){
				pendulums.getThetas(x);
				pendulums.getOmegas(v);
				pendulums.getAlphas(a);
				if(chkMovable.checked){
					let len = pendulums.length;
					x[len] = ceiling.position.x;
					v[len] = ceiling.velocity.x;
					a[len] = ceiling.acceleration.x;
				}

				nextValue(x,v,pendulums.calculateAccelerations,simT,dtn,a);
				updatePositions(x,v,a);

				simT += dtn;
			}

			recordData();
			showPositions();
			scene.TPF.updateLabel(labelTPF,1);
			scene.FPS.updateLabel(labelFPS,1);
			labelSimT.innerText = $tw.ve.round(simT,3);
		}
		checkPlots();
	};
//}}}
//{{{
		pendulum.calculateAcceleration = function(r,v){
			r = r.clone().sub(pendulum.pivotPosition());
			let ur = r.clone().normalize();
			let a = ur.clone().multiplyScalar(
				-v.lengthSq()/r.length()
			);
			pendulum.string.tension.copy(a).multiplyScalar(pendulum.mass).sub(
				ur.multiplyScalar(fg.dot(ur))
			);
			a.add(pendulum.calculateAlpha(r).cross(r));
			pendulum.acceleration.copy(a);
			//pendulum.string.tension.copy(a).multiplyScalar(pendulum.mass).sub(fg);
			return a;
		};
//}}}
/***
!!!! radialPeriod
<<<
Calculate radal period of a pendulum
<<<
***/
//{{{
let radialPeriod = function(rod,bob){
	return 2.0*Math.PI*Math.sqrt(bob.mass/rod.k);
}
//}}}
//{{{
let swingPeriod = function(rod,bob){
	return 2.0*Math.PI*Math.sqrt(rod.L0/9.8)*
		(1.0+1.0/16.0*rod.A0*rod.A0+1.0/3072.0*Math.pow(rod.A0,4));
}
//}}}
//{{{
let theoreticalPeriod = function(rod,bob,type){
	if(!type) type = oscillationType(rod,bob);
	if(type === 'vertical')
		return radialPeriod(rod,bob);
	else if(type === 'planar')
		return swingPeriod(rod,bob);
	else if(type === 'conical')
		return 2.0*Math.PI*Math.sqrt(rod.L0*Math.cos(rod.A0)/9.8);
	//else if(type === 'planar-complex')
	//	return swingPeriod(rod,bob)
		//return {
		//	'swing': swingPeriod(rod,bob),
		//	'radial': radialPeriod(rod,bob)
		//}
	//else if(type === 'non-planar')
	//	return swingPeriod(rod,bob);
	else
		// Don't know what to do...
		return 0;
}
//}}}
//{{{
let determinePeriod = function(obj,t_now,r0,type){
	/***
	Determine the period of a moving object by finding the turning
	point. The turning point is determined by a sign change in the
	radial compoent of the velocity.
	***/

	if(!type) type = oscillationType(pendulum.string[0],bob[0]);
	let T = 0;
	let rnorm = obj.position.clone().sub(r0).normalize();
	let turning = false;
	if(obj.v_last){
		if(type === 'vertical')
			turning = (obj.v_last.y*obj.velocity.y < 0);
		else if(type === 'planar' || type === 'conical')
			turning = (obj.v_last.x*obj.velocity.x < 0);
		if(turning){
			T = (t - obj.t_last)*2;
			obj.t_last = t_now;
		}
	}else{
		obj.v_last = vector();
		obj.t_last = t_now;
	}
	obj.v_last.copy(obj.velocity);

	return T;
}
//}}}
/***
!!!! oscillationType(rod,bob)
<<<
* The oscillation type of a pendulum can be one of the followings:
## ''planar'': the simplest case that all the textbook would talk about
## ''conical'': another simple case that most of the textbook would talk about
## ''vertical'': more like a vertical spring oscillation
## ''general'': all other cases
* To test whether a pendulum motion is planar, we check if the following three vectors are coplanar:
## \(\vec r,\) the bob's position vector from the pivot;
## \(\hat z,\) the vertical axis;
## \(\vec v,\) the bob's velocity vector.
* To test whether it's conical, we check its speed with the expected conical speed.
* To test whether it's vertical, we check if both \(v_x\) and \(v_y\) are 0.
<<<
***/
//{{{
/***
let oscillationType = function(rod,bob){
	if(Math.abs(bob.velocity.y) < $tw.physics.__epsilon__ &&
		Math.abs(bob.velocity.x) < $tw.physics.__epsilon__){
		return rod.k > 0 ? 'vertical' : 'none';
	}
	if(bob.velocity.length())
			if(rod.k === 0)
				return 'planar';
			else
				return 'planar-complex';
		}
	}else{
		// non-planar pendulum
		let speed = bob.velocity.length();
		let conicalv = conicalSpeed(rod.L0,rod.A0);
		return (Math.abs(speed-conicalv) < __algoerr__)
			? 'conical' : 'non-planar';
	}
}
***/
//}}}
/***
!!! The Pendulum
***/
//{{{
	pendulum.preInit = pendulum.init;
	pendulum.init = function(){
		pendulu..preInit.apply(this,arguments);
		pendulum.alpha.set(0,0,0);
		pendulum.omega.set(0,0,0);
		pendulum.angle.copy(pendulum.initialAngle);
		pendulum.angle.value = pendulum.initialAngle.value;
		pendulum.setAngle();
		return pendulum;
	};

	pendulum.bob = param.bob || new Bob({
		radius: 0.05,
		color: 0x00ff00,
		opacity: 0.3
	},'noadd');
	pendulum.add(pendulum.bob);

	pendulum.destroy = function(){
		scene.remove(pendulum);
		//scene.remove(pendulum.bob);
		//scene.remove(pendulum.string);
		scene.remove(pendulum.spherecm);
		scene.remove(pendulum.pivot);
		scene.remove(pendulum.vertical);
	}

	pendulum.torque = vector();
	pendulum.alpha = vector();
	pendulum.omega = vector();
	pendulum.angle = vector();
	pendulum.velocity = vector();
	pendulum.acceleration = vector();

	if(param.initialAngle){
		if(typeof param.initialAngle === 'number'){
			pendulum.initialAngle = vector(0,0,1);
			pendulum.initialAngle.value = param.initialAngle;
		}else{
			pendulum.initialAngle = param.initialAngle.clone();
			pendulum.initialAngle.value = pendulum.initialAngle.length();
			pendulum.initialAngle.normalize();
		}
	}else{
		pendulum.initialAngle = vector(0,0,1);
		pendulum.initialAngle.value = 0;
	}

	pendulum.setLength = function(L){
		pendulum.string= pendulum.string.setLength(L);
		pendulum.length = pendulum.string.getLength();
		pendulum.setAngle();
		pendulum.setPosition();
	};
	pendulum.isSimple = function(){
		return pendulum.string.mass === 0;
	};
//}}}
/***
!!!! pendulum.calculateCM ()
>計算擺的質心位置。如果是單擺,則質心位置就是擺錘的位置,如果是實體擺,則質心位至按照下列公式計算:
>Calculate the center of mass position of this pendulum. If the pendulum is simple, the center of mass is that of the bob, otherwise calculate the center of mass using the following formula:
>\[\vec r_\text{CM} = {m_\text{bob}\vec r_\text{bob} + m_\text{rod} \vec r_\text{rod} \over m_\text{bob} + m_\text{rod}},\]
>其中 \(m_\text{bob}\) 與 \(m_\text{rod}\) 分別為擺錘與擺繩的質量,而 \(\vec r_\text{bob}\) 及 \(\vec r_\text{rod}\) 則各為其中心位置。
>where \(m_\text{bob}\) and \(m_\text{rod}\) are the masses, while \(\vec r_\text{bob}\) and \(\vec r_\text{rod}\) are the center positions of the bob and the rod, respectively.
***/
//{{{
	pendulum.rcm = vector();
	// 代表質心的球 A sphere to represent the center of mass.
	pendulum.spherecm = sphere({
		radius: 0.01,
		opacity: 0.6
	});
	//pendulum.add(pendulum.spherecm);
	pendulum.calculateCM = function(){
		if(pendulum.isSimple())
			pendulum.rcm.copy(pendulum.bob.position);
		else
			pendulum.rcm.copy(pendulum.string.position)
				.multiplyScalar(pendulum.string.mass)
				.add(
					pendulum.bob.position.clone()
						.multiplyScalar(pendulum.bob.mass)
				).multiplyScalar(
					1/pendulum.mass
				);
		pendulum.rcm.add(pendulum.position);
		pendulum.spherecm.position.copy(pendulum.rcm);
		pendulum.spherecm.visible = chkCM.checked;
	};
//}}}
/***
!!!! pendulum.calculateVertical()
>計算此擺的鉛直線通過點
***/
//{{{
	pendulum.vertical = cylinder({
		radius: 0.003,
		axis: vector(0,-txtRodLength.value/2,0),
		color: 0xaaaaaa,
		opacity: 0.5
	});
	pendulum.calculateVertical = function(){
		pendulum.vertical.position.copy(pendulum.position);
		if(chkMovable.checked){
			let dx = (pendulum.rcm.x - pendulum.position.x)
				*pendulums.mass/(ceiling.mass+pendulums.mass);
			pendulum.vertical.position.x += dx;
			pendulum.vertical.position.y -=
				Math.abs(dx/Math.tan(pendulum.initialAngle.value));
		}
	};
//}}}
/***
!!!! pendulum.pivotPosition (\(\vec r_p\) )
<<<
取得這個擺的轉軸通過點。引數 \(\vec r_p\) 如果有給,它必須是個向量,作為接收結果之用,如果沒給,此函數會傳回一個新建立的向量。
Returns the pivot position of this pendulum. If the argument \(\vec r_p\) is given, it must be a vector to receive the result. If not, this function returns a newly created vector.
* 如果天花板是固定的,則轉軸點就是擺的懸掛點。<br>If the ceiling is fixed, then the pivot position is the hanging position.
* 如果天花板是可水平動的,則如下計算:<br>If the ceiling is horizontally movable, then the pivot position is calculated as the following: \[\begin{eqnarray*}r_{p,x} &=& r_{h,x} + (r_{CM,x} - r_{h,x}){\sum m_p \over \sum m_p + m_c} \\ r_{p,y} &=& R_{CM,y},\end{eqnarray*}\] 其中 \(\vec r_p\) 是轉軸點,\(\vec r_h\) 是懸掛點,\(\vec r_{CM}\) 是擺的質心位置,\(m_p\) 是擺的質量,\(m_c\) 是天花板的質量,而 \(R_{CM}\) 則為系統的質心位置。<br>where \(\vec r_p\) is the pivot position, \(\vec r_h\) is the haning position, \(\vec r_{CM}\) is pendulum's center of mass, \(m_p\) is pendulum's mass, \(m_c\) is ceiling's mass, while \(R_{CM}\) is the system's center of mass position.
<<<
***/
//{{{
	pendulum.pivot = sphere({
		radius: 0.02,
		color: 0xffff00,
		opacity: 0.5
	});
	//pendulum.add(pendulum.pivot);

	pendulum.pivotPosition = function(rp){
		let pivot = rp || vector();
		//if(!chkOneD || chkOneD.checked || !chkMovable.checked){
			pivot.copy(pendulum.position);
		//}else{
		/*
			// The ceiling is movable, calculate the position that would
			// remain fixed in x direction.
			if(optPivot)
				switch(optPivot.value){
					case 'Ref' :
						// Same radius as in the IYPT reference kit.
						pivot.copy(pendulum.position);
						break;
					case 'SysCM' :
						pivot.copy(Rcm);
						break;
					case 'mouse' :
						let target = scene.raycaster.intersects;
						if(target){
							for(let n=0,len=target.length; n<len; n++){
								console.log('pt: '+target[n].point+' obj:'+target[n].object);
							}
						}
						break;
					default :
					case 'MassRatio' :
						//pivot.copy(pendulum.position);
						//let dx = (pendulum.rcm.x - pendulum.position.x)
						//	*pendulums.mass/(ceiling.mass+pendulums.mass);
						//pivot.x += dx;
						//pivot.y -= Math.abs(dx/Math.tan(pendulum.angle.value));
						pivot.copy(pendulum.vertical.position);
						//pivot.y = Rcm.y;
						break;
				}
		}
		*/
		pendulum.pivot.position.copy(pivot);
		return pivot;
	};
//}}}
/***
!!!! pendulum.axisOfRotation ()
>取得這個擺的轉軸,如果擺錘的運動只在 //x//-//y// 平面上,則轉軸為 //z// 軸,若擺軸運動有離開 //x//-//y// 平面,則轉軸為 //y// 軸。
>Obtain the axis of rotation for this pendulum. If the bob's motion remains in the //x//-//y// plane, the axis of rotation is the //z//-axis. If, however, the bob's motion gets in and out of the //x//-//y// plane, the axis of rotation is the //y//-axis.
***/
//{{{
	pendulum.axisOfRotation = function(){
		return pendulum.velocity.z === 0
			? pendulum.initialAngle
			: ceiling.normal;
	};
//}}}
/***
!!!! pendulum.I (\(\vec r_0,\) axis)
>計算此擺相對於通過 \(\vec r_0\) 這個位置的轉軸(沿 axis 的方向,axis 必須為單位向量)之轉動慣量,使用下列公式:
>Calculate the moment of inertia of this pendulum with respect to an axis of rotation (must be a unit vector) passing through \(\vec r_0,\) using the following formula:
>\[\begin{eqnarray*}I &=&& m_\text{bob}|(\vec r_\text{bob} - \vec r_0)_\perp|^2 & \quad \text{將擺錘視為質點的轉動慣量} \\ &&&& \quad \text{The bob's moment of inertia as a particle.}\\ &&+& {1 \over 12}m_\text{rod}L_\text{rod}^2 + m_\text{rod}|(\vec r_\text{rod} - \vec r_0)_\perp|^2 & \quad \text{擺繩的轉動慣量(平行軸定理),} \\ &&&& \quad \text{The rod's moment of inertia (Parallel Axis Theorem),}\end{eqnarray*}\] 其中 \(\vec r_\text{bob}\) 與 \(\vec r_\text{rod}\) 各為擺錘及擺繩的中心位置,而 \((\vec r - \vec r_0)_\perp\) 則表示 \((\vec r - \vec r_0)\) 垂直於轉軸 axis 的分量。
>where \(\vec r_\text{bob}\) and \(\vec r_\text{rod}\) are the center positions of the bob and the rod, respectively, while \((\vec r - \vec r_0)_\perp\) is the component of \((\vec r - \vec r_0)\) that is perpendicular to the axis of rotation.
***/
//{{{
	pendulum.I = function(r0,axis){
		if(!r0) r0 = pendulum.pivotPosition();
		if(!axis) axis = pendulum.initialAngle.clone();
		let vdr = pendulum.bob.position.clone().add(pendulum.position)
					.sub(r0)
					.projectOnPlane(axis);				// component perpendicular to axis
		let Ibob = pendulum.bob.mass*vdr.lengthSq();	// lengthSq() = length()^2
		if(pendulum.isSimple()){
			return Ibob;
		}else{
			vdr.copy(pendulum.string.position).add(pendulum.position)
				.sub(r0).projectOnPlane(axis);
			return Ibob + pendulum.string.mass * (
				  Math.pow(pendulum.length,2)/12
				+ vdr.lengthSq()						// lengthSq() = length()^2
			);
		}
	};
//}}}
/***
!!!! pendulum.calculateAngle ()
>計算擺繩與垂直線(\(y\) 軸)的夾角及其方向 \(\hat \theta\),
>Calculate the angle and direction \(\hat \theta\) between the pendulum rod and the vertical line (\(y\)-axis),
>\[\theta = \cos^{-1}((-\hat y) \cdot \hat r),\] 其中 \(\hat r\) 與 \(\hat y\) 是由懸掛點開始的單位向量,分別沿著擺繩及 \(+y\) 軸的方向。而角度方向則為
> where \(\hat r\) and \(\hat y\) are unit vectors starting from the hanging point of the pendulum, going along the rod and the \(+y\)-axis directions, respectively. The direction of the angle is \[\hat \theta = (-\hat y) \times \hat r = \hat r \times \hat y.\]
***/
//{{{
	pendulum.calculateAngle = function(){
		let r = pendulum.string.rcm.clone().normalize();
		let y = vector(0,1,0);
		let angle = r.clone().cross(y);
		angle.value = Math.acos(-(y.dot(r)));
		return pendulum.setAngle(angle,true);
	};
//}}}
/***
!!!! pendulum.checkAngle (angle)
<<<
* 如果使用歐拉方法,計算誤差足夠大,這裡需要確保角度不會大過初始角,因為計算的時間間隔是有限的,有機會在初始角度附近算出一個超過初始角度的結果,即便這個時候的速度很接近 0,就算超過也是很少,但這個很少的超過就會讓擺槌受力大了一點點,造成後續速度大一點點,因而再次超過初始角度的機會也就跟著大一點點,再次超過就會再次讓受力大一點,也就再提高超過機率一點,如此循環放大,沒多久就會讓擺槌振幅大到一個離譜的程度。<br>If Eular method is applied, the error is large enough, we need to make sure the angle does not exceed the initial angle, because the time interval in the calculation is finite, there is a chance when in the vicinity of initial angle the calculation gives a result exceeding the initial angle. Even though in such cases the velocity of the bob is very close to 0, the exceeded amount is very small, this small extra is going to increase force exerted on the bob, causing a small increase in the velocity, which in turn increases the chances of exceeding the initial angle again, then the force increases again, and chances of exceeding increase again, and on and on and on, resulting in a cyclic amplification, and the amplitude will soon get too large.

* 如果使用 4 階龍格-庫塔方法,誤差較小,不太需要擔心角度過大的問題。根據實際測試結果,時間步伐如果夠大,最大角度其實會逐漸變小。<br>If the 4th order ~Runge-Kutta method is applied, the error is small enough and we probably don't worry about the over-limit problem. According to actual testing results, the maximum angle reduces gradually if time step is large enough.
<<<
***/
//{{{
	pendulum.checkAngle = function(angle){
		// The argument angle shall be a vector pointing along the axis of rotation and
		// having its length equal to the angle of rotation (in radians).
		//if(!chkMovable.checked && angle.value>pendulum.initialAngle.value){
			// If the angle is exceeding the initial value, put it back.
			// This can easily happen when the Euler method is applied,
			// because of the low accuracy in the algorithm.
		//	angle.value = pendulum.initialAngle.value;
		//}
		return pendulum;
	};
//}}}
/***
!!!! pendulum.checkCycle ()
> 檢查是否完成一個週期。如果擺的速度的 //x// 分量在上一個瞬時為正,而現在這個瞬時為負,表示剛剛完成一個週期,以線性內差法估計轉折點的時間,並顯示週期。由於在轉折點附近的速度很小,線性內插足以得到很好的估計值。
> Determine whether a cycle is completed. If the //x//-component of pendulum's velocity is positive in the last moment, while negative in the current moment, that means one cycle is just completed. Apply the linear interpolation to estimate the moment at the turning point and show the period. Because the velocity is very slow around the turning point, linear interpolation suffices to give a very good estimation.
***/
//{{{
	pendulum.lastV = vector();
	pendulum.lastT = 0;
	pendulum.checkCycle = function(){
		if(pendulum!==pendulums[0]) return;
		if(pendulum.lastV.x > 0 && pendulum.velocity.x < 0){
			// Just completed one cycle, apply linear interpolation to estimate
			// the moment at the turning point, and show the period.
			let turnT = simT-dt*(
				pendulum.velocity.x/(pendulum.velocity.x-pendulum.lastV.x)
			);
			showPeriod(turnT - pendulum.lastT);
			pendulum.lastT = turnT;
		}
		pendulum.lastV.copy(pendulum.velocity);
	};
//}}}
/***
!!!! pendulum.setAngle (angle, done)
> 設定此擺與鉛直線之間的角度(弧度),並視情況將擺轉到該角度去(如果第二個引數 done 為 false 的話)
> Sets the angle (in radians) measured from the vertical line, and rotate the pendulum if necessary (if the 2nd argument done is false).
***/
//{{{
	pendulum.setAngle = function(angle,done){
		if(angle){
			//pendulum.checkAngle(angle);
			pendulum.angle.copy(angle);
			if(angle.value===undefined){
				pendulum.angle.value = pendulum.angle.length();
				pendulum.angle.normalize();
			}else{
				pendulum.angle.value = angle.value;
			}
		}else{
			angle = pendulum.angle;
			//pendulum.checkAngle(angle);
		}

		pendulum.checkCycle();

		if(!done){
			// Rotate the pendulum to the desired angle.
			pendulum.string.setDirection(
				pendulum.string.rcm.set(0,-1,0)
					.applyAxisAngle(angle,angle.value)
			);
			pendulum.string.rcm.multiplyScalar(pendulum.length/2);
		}

		return pendulum;
	};
//}}}
/***
!!!! pendulum.calculateAlpha (\(\vec r\))
<<<
計算擺的角加速度 \(\vec \alpha\),使用下列公式:
Calculate the the angular acceleration \(\vec \alpha\) of the pendulum, using the following formula: \[\vec \tau = I \vec\alpha \qquad \to \qquad \boxed{\vec \alpha = \vec \tau I^{-1}},\] 其中 \(\vec \tau\) 是作用在擺的力矩,而 \(I\) 是擺對轉軸的轉動慣量。簡單情況下 \(I\) 只是純量,不過一般情況下它是張量。這裡我們只考慮簡單情況。
where \(\vec \tau\) is the torque acting on the pendulum, while \(I\) is the moment of inertia of the pendulum with respect to the axis of rotation. In simple cases \(I\) is just a scalar. In general cases, however, it is a tensor. Here we only consider the simple cases.
----
引數 \(\vec r\) 用來計算力矩 \(\vec \tau = \vec r \times \vec F\),
The argument \(\vec r\) is used to calculated the torque \(\vec \tau = \vec r \times \vec F,\)
# 在天花板固定的情況下為<br>is, when the ceiling is fixed, \[\vec \tau = (\vec r_{CM} - \vec r_h) \times \vec F_g,\] 其中 \(\vec r_{CM}\) 為擺的質心位置,\(\vec r_h\) 為懸掛位置,\(\vec F_g\) 為擺所受的重力。引數 \(\vec r = \vec r_{CM} - \vec r_h\) 必須在呼叫此函數前先計算好。<br>where \(\vec r_{CM}\) is the pendulum's center of mass position, \(\vec r_h\) is the pendulum's hanging position, while \(\vec F_g\) is the force of gravity acting on the pendulum. The argument \(\vec r = \vec r_{CM} - \vec r_h\) must be calculated accordingly before calling this function.
# 而在天花板可移動的情況下則<br>While the ceiling is movable, however, \[\vec \tau = \vec\tau_g + \vec\tau_c = (\vec r_{CM} - \vec r_p) \times \vec F_g + (\vec r_h - \vec r_p) \times \vec F_c,\] 其中 \(\vec\tau_g\) 為重力產生的力矩,\(\vec r_p\) 表示等效的轉軸點,\(\vec F_c\) 表示因天花板移動造成的力,此力對擺產生的力矩為 \(\vec\tau_c\)。引數 \(\vec r = \vec r_{CM} - \vec r_p\) 必須在呼叫此函數前先計算好。<br>where \(\vec \tau_g\) is the torque due to gravitational force, \(\vec r_p\) is the equivalent pivot position, \(\vec F_c\) is the resultant force due to the motion of the ceiling, the torque exerted on the pendulum due to this force is \(\vec \tau_c.\) The argument \(\vec r = \vec r_{CM} - \vec r_p\) must be calculated accordingly before calling this function.
** 這個天花板移動造成的力 \(\vec F_c\),根據 Ref[1] 以及 Ref[2] 裡的說法,是假想的慣性力 \(\boxed{-m_pd^2\vec r_c / dt^2}\),其中 \(m_p\) 是擺的質量而 \(\vec r_c\) 是天花板的位置。<br>The force \(\vec F_c\) due to ceiling's motion is, according to Ref[1] and Ref[2], a fictitious force of inertia \(\boxed{-m_pd^2\vec r_c / dt^2},\) where \(m_p\) is pendulum's mass while \(\vec r_c\) is the ceiling's position.
** 若假設天花板只能水平移動,則其加速度只能有水平方向的分量,但由於整個系統在水平方向沒有受力,系統質心在水平方向是不會移動的,<br>If we assume the ceiling is only movable horizontally, its acceleration then has only horizontal components. Because there is no horizontal force exerted on the system, the system's center of mass do not move horizontally, \[{d\vec R_{CM,\parallel} \over dt} = {d \over dt}\left(m_c\vec r_{c,\parallel} + \sum(m_p\vec r_{p,\parallel}) \over m_c + \sum m_p\right) = 0,\] 其中加總是對所有懸掛在此天花板下的擺進行的。這讓我們很容易寫下<br>where the summation is carried out over all the pendulums hanging under this ceiling. This allows us to easily write down \[{d^2\vec r_{c,\parallel} \over dt^2} = -{1 \over m_c}{d^2 \over dt^2}\sum(m_p\vec r_{p,\parallel}) = -{1 \over m_c}\sum m_p{d^2\vec r_{p,\parallel} \over dt^2} = -{1 \over m_c}\sum m_p \vec a_{p,\parallel}.\]
** 如此我們可以用擺的加速度之水平分量來寫出這個假想的慣性力<br>Now we can write down this fictitious force in terms of the parallel component of pendulum's acceleration \[\boxed{\vec F_c = {m_p \over m_c}\sum m_p\vec a_{p,\parallel}.}\]
----
# http://www.math.pitt.edu/~bard/classes/mth3380/syncpapers/metronome.pdf
# http://www.physik3.gwdg.de/~ulli/pdf/UMP09.pdf
<<<
***/
//{{{
	pendulum.calculateAlpha = function(r){
		let torque = r.clone().cross(fg);
		if(chkMovable.checked){
			torque.add(
				optPivot && optPivot.value === 'Ref'
					?	ceiling.acceleration.clone()
							.multiplyScalar(-pendulum.mass).cross(r)
					:	pendulum.position.clone().sub(pendulum.pivot.position)
							.cross(
								ceiling.acceleration.clone()
									.multiplyScalar(-pendulum.mass)
							)
			);
		}
		let alpha = torque.multiplyScalar(
			1/pendulum.I(
				 pendulum.pivotPosition()
				,pendulum.axisOfRotation()
			)
		);
		pendulum.alpha.copy(alpha);
		return alpha;
	};
//}}}
/***
!!!! pendulum.calculateLinearStatus ()
<<<
根據現在的角度運動狀態(角速度、角加速度)來計算擺的線性運動狀態(速度、加速度、張力)。
Calculatee the status of the pendulum's linear motion (linear velocity, linear acceleration, tension), using its current angular motion status (angular velocity, angular acceleration).
* 速度只有切線方向,其計算是根據<br>The velocity is only tangential and calculated according to \[\vec v = \vec \omega \times (\vec r_{CM} - \vec r_0),\] 其中 \(\vec r_\text{CM}\) 是擺的質心位置,而 \(\vec r_0\) 則是轉軸通過的位置,可能是擺的懸掛點(如果天花板固定),或者是整體的質心位置(如果天花板可移動)。<br>where \(\vec r_{CM}\) is the pendulum's center of mass position, while \(\vec r_0\) is the pivot position, which could be the hanging point (if the ceiling is fixed), or the whole system's center of mass position (if the ceiling is movable).

* 加速度則有切線及向心兩個分量,<br>The acceleration has tangential and centripetal components, \[\vec a = \vec a_c + \vec a_t = |\vec r_{CM}-\vec r_0|\omega^2(-\hat l) + |\vec r_{CM}-\vec r_0|\alpha \hat t,\] 其中 \(\vec a_c\) 是向心(centripetal)而 \(\vec a_t\) 是切線(tangential)加速度,\(\hat l\) 是沿著擺繩在離心方向的單位向量,而 \(\hat t\) 是切線方向的單位向量。<br>where \(\vec a_c\) is the centripetal and \(\vec a_t\) is the tangential acceleration, \(\hat l\) is the unit vector along the rod pointing in the centrifugal direction, while \(\hat t\) is the unit vector in the tangential direction.
<<<
***/
//{{{
	pendulum.calculateLinearStatus = function(){
		// 計算線速度 Calculate linear velocity
		let dr = pendulum.rcm.clone().sub(pendulum.pivotPosition());
		pendulum.velocity.copy(pendulum.omega).cross(dr);
		// 計算加速度 Calculate the acceleration.
		pendulum.acceleration.copy(dr).multiplyScalar(-pendulum.omega.lengthSq())
				.add(dr.multiplyScalar(pendulum.alpha.length()).cross(pendulum.angle));
		// 計算張力 Calculate the tension.
		pendulum.calculateTension();
	};

	return pendulum;
};
//}}}
/***
!!!! pendulum.pivotPosition (\(\vec r_p\))
<<<
取得這個擺的轉軸通過點。引數 \(\vec r_p\) 如果有給,它必須是個向量,作為接收結果之用,如果沒給,此函數會傳回一個新建立的向量。
Returns the pivot position of this pendulum. If the argument \(\vec r_p\) is given, it must be a vector to receive the result. If not, this function returns a newly created vector.
* 如果天花板是固定的,則轉軸點就是擺的懸掛點。<br>If the ceiling is fixed, then the pivot position is the hanging position.
* 如果天花板是可水平動的,則如下計算:<br>If the ceiling is horizontally movable, then the pivot position is calculated as the following: \[\begin{eqnarray*}r_{p,x} &=& r_{h,x} + (r_{CM,x} - r_{h,x}){\sum m_p \over \sum m_p + m_c} \\ r_{p,y} &=& R_{CM,y},\end{eqnarray*}\] 其中 \(\vec r_p\) 是轉軸點,\(\vec r_h\) 是懸掛點,\(\vec r_{CM}\) 是擺的質心位置,\(m_p\) 是擺的質量,\(m_c\) 是天花板的質量,而 \(R_{CM}\) 則為系統的質心位置。<br>where \(\vec r_p\) is the pivot position, \(\vec r_h\) is the haning position, \(\vec r_{CM}\) is pendulum's center of mass, \(m_p\) is pendulum's mass, \(m_c\) is ceiling's mass, while \(R_{CM}\) is the system's center of mass position.
<<<
***/
/*{{{*/
	pendulum.pivot = sphere({
		radius: 0.02,
		color: 0xffff00,
		opacity: 0.5
	});
	//pendulum.add(pendulum.pivot);

	pendulum.pivotPosition = function(rp){
		let pivot = rp || vector();
		//if(!chkOneD || chkOneD.checked || !chkMovable.checked){
			pivot.copy(pendulum.position);
		//}else{
		/*
			// The ceiling is movable, calculate the position that would
			// remain fixed in x direction.
			if(optPivot)
				switch(optPivot.value){
					case 'Ref' :
						// Same radius as in the IYPT reference kit.
						pivot.copy(pendulum.position);
						break;
					case 'SysCM' :
						pivot.copy(Rcm);
						break;
					case 'mouse' :
						let target = scene.raycaster.intersects;
						if(target){
							for(let n=0,len=target.length; n<len; n++){
								console.log('pt: '+target[n].point+' obj:'+target[n].object);
							}
						}
						break;
					default :
					case 'MassRatio' :
						//pivot.copy(pendulum.position);
						//let dx = (pendulum.rcm.x - pendulum.position.x)
						//	*pendulums.mass/(ceiling.mass+pendulums.mass);
						//pivot.x += dx;
						//pivot.y -= Math.abs(dx/Math.tan(pendulum.angle.value));
						pivot.copy(pendulum.vertical.position);
						//pivot.y = Rcm.y;
						break;
				}
		}
		*/
		pendulum.pivot.position.copy(pivot);
		return pivot;
	};
/*}}}*/
/***
!!!! pendulum.axisOfRotation ()
>取得這個擺的轉軸,如果擺錘的運動只在 //x//-//y// 平面上,則轉軸為 //z// 軸,若擺軸運動有離開 //x//-//y// 平面,則轉軸為 //y// 軸。
>Obtain the axis of rotation for this pendulum. If the bob's motion remains in the //x//-//y// plane, the axis of rotation is the //z//-axis. If, however, the bob's motion gets in and out of the //x//-//y// plane, the axis of rotation is the //y//-axis.
***/
/*{{{*/
	pendulum.axisOfRotation = function(){
		return pendulum.velocity.z === 0
			? pendulum.initialAngle
			: ceiling.normal;
	};
/*}}}*/
/***
!!!! pendulum.I (\(\vec r_0,\) axis)
>計算此擺相對於通過 \(\vec r_0\) 這個位置的轉軸(沿 axis 的方向,axis 必須為單位向量)之轉動慣量,使用下列公式:
>Calculate the moment of inertia of this pendulum with respect to an axis of rotation (must be a unit vector) passing through \(\vec r_0,\) using the following formula:
>\[\begin{eqnarray*}I &=&& m_\text{bob}|(\vec r_\text{bob} - \vec r_0)_\perp|^2 & \quad \text{將擺錘視為質點的轉動慣量} \\ &&&& \quad \text{The bob's moment of inertia as a particle.}\\ &&+& {1 \over 12}m_\text{rod}L_\text{rod}^2 + m_\text{rod}|(\vec r_\text{rod} - \vec r_0)_\perp|^2 & \quad \text{擺繩的轉動慣量(平行軸定理),} \\ &&&& \quad \text{The rod's moment of inertia (Parallel Axis Theorem),}\end{eqnarray*}\] 其中 \(\vec r_\text{bob}\) 與 \(\vec r_\text{rod}\) 各為擺錘及擺繩的中心位置,而 \((\vec r - \vec r_0)_\perp\) 則表示 \((\vec r - \vec r_0)\) 垂直於轉軸 axis 的分量。
>where \(\vec r_\text{bob}\) and \(\vec r_\text{rod}\) are the center positions of the bob and the rod, respectively, while \((\vec r - \vec r_0)_\perp\) is the component of \((\vec r - \vec r_0)\) that is perpendicular to the axis of rotation.
***/
/*{{{*/
	pendulum.I = function(r0,axis){
		if(!r0) r0 = pendulum.pivotPosition();
		if(!axis) axis = pendulum.initialAngle.clone();
		let vdr = pendulum.bob.position.clone().add(pendulum.position)
					.sub(r0)
					.projectOnPlane(axis);				// component perpendicular to axis
		let Ibob = pendulum.bob.mass*vdr.lengthSq();	// lengthSq() = length()^2
		if(pendulum.isSimple()){
			return Ibob;
		}else{
			vdr.copy(pendulum.string.position).add(pendulum.position)
				.sub(r0).projectOnPlane(axis);
			return Ibob + pendulum.string.mass * (
				  Math.pow(pendulum.length,2)/12
				+ vdr.lengthSq()						// lengthSq() = length()^2
			);
		}
	};
/*}}}*/
/***
!!!! pendulum.theoreticalPeriod ()
>計算此擺的理論週期,使用下列公式:
>Calculate the theoretical period of this pendulum, using the following formula:
>\[\begin{eqnarray*}T &=&& 2\pi \sqrt{L \over g}\left(1 + {1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4 + \cdots\right) & \quad \text{單擺 Simple pendulum} \\ &\text{or}&& 2\pi \sqrt{I \over mL g}\left(1 + {1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4 + \cdots\right) & \quad \text{實體擺(物理擺)Physical pendulum.}\end{eqnarray*}\] 參考文獻:
>Ref: [[Wikipedia Pendulum|https://en.wikipedia.org/wiki/Pendulum_(mathematics)]].
***/
/*{{{*/
	pendulum.theoreticalPeriod = function(){
		let anglesquared = Math.pow(pendulum.initialAngle.value,2);
		let r0 = pendulum.pivotPosition();
		let r = pendulum.rcm.clone().sub(r0);
		return 2*Math.PI
			*Math.sqrt(
				(pendulum.isSimple()
					?	r.length()
					:	pendulum.I(
							r0,pendulum.axisOfRotation()
						)/pendulum.mass/r.length())
				/(-scene.g.y)
			)*(
				  1
				+ anglesquared/16
				+ Math.pow(anglesquared,2)*11/3072
			);
	};
/*}}}*/
/***
!!!! pendulum.calculateAngle ()
>計算擺繩與垂直線(\(y\) 軸)的夾角及其方向 \(\hat \theta\),
>Calculate the angle and direction \(\hat \theta\) between the pendulum rod and the vertical line (\(y\)-axis),
>\[\theta = \cos^{-1}((-\hat y) \cdot \hat r),\] 其中 \(\hat r\) 與 \(\hat y\) 是由懸掛點開始的單位向量,分別沿著擺繩及 \(+y\) 軸的方向。而角度方向則為
> where \(\hat r\) and \(\hat y\) are unit vectors starting from the hanging point of the pendulum, going along the rod and the \(+y\)-axis directions, respectively. The direction of the angle is \[\hat \theta = (-\hat y) \times \hat r = \hat r \times \hat y.\]
***/
/*{{{*/
	pendulum.calculateAngle = function(){
		let r = pendulum.string.rcm.clone().normalize();
		let y = vector(0,1,0);
		let angle = r.clone().cross(y);
		angle.value = Math.acos(-(y.dot(r)));
		return pendulum.setAngle(angle,true);
	};
/*}}}*/
/***
!!!! pendulum.setPosition (\(\vec r_h, \vec r_{CM}\))
> 設定此擺的位置,視情況計算擺的角度或是質心位置。兩個引數都是非必要的,第一個引數 \(\vec r_h\) 為//懸掛//位置,如果有給,此函數便將擺的懸掛位置設定到給定的地方,如果沒有,則維持不變。第二個引數 \(\vec r_{CM}\) 為擺的質心位置,如果有,表示擺的質心位置已知,則擺的角度要據此計算出來。如果沒有,表示擺的角度已知,則擺的質心位置要根據角度計算出來。
> Sets the position of this pendulum, and calculates either its angle or center of mass position. Both arguments are optional. The first one \(\vec r_h\) is the //hanging// positio. If given, the hanging position will be set to the specified place. If not given, it remains unchanged. The second argument \(\vec r_{CM}\) is the center of mass position. If given, meaning the center of position is already known, then the angle of this pendulum shall be calculated accordingly. If not, meaning the angle of this pendulum is known, then the center of mass position shall be calculated accordingly.
***/
/*{{{*/
	pendulum.setPosition = function(rh,cm){
		if(rh){
			pendulum.position.copy(rh);
		}
		if(cm){
			// 擺的質心位置已知,將擺旋轉到該有的方向
			// Pendulum's center of mass is known, rotate the rod to the desired orientation.
			pendulum.string.setDirection(
				pendulum.string.rcm.copy(cm)
					.sub(pendulum.position).normalize()
			);
			pendulum.string.rcm.multiplyScalar(pendulum.length/2);
		}
		// 把擺繩放在正確位置 Put the rod at the right position.
		pendulum.string.position.copy(pendulum.string.rcm);
		// 把球放在擺繩的下端 Attach the bob to the bottom end of the rod.
		pendulum.bob.position.copy(pendulum.string.position).add(pendulum.string.rcm);
		if(cm){
			// 擺的質心位置已知,計算對應的角度及系統的質心
			// Pendulum's center of mass is known, calculate the corresponding
			// angular position and system's center of mass.
			pendulum.spherecm.position.copy(pendulum.rcm);
			pendulum.spherecm.visible = chkCM.checked;
			pendulum.calculateAngle();
 		}else{
			// 擺的質心位置未知,把它計算出來。
			// Pendulum's center of mass is unknown, calculate it.
			pendulum.calculateCM();
			//calculateSystemCM();
		}
		return pendulum;
	};
/*}}}*/
/***
!!!! pendulum.shiftPosition (\(d\vec r\))
> 移動此擺的懸掛位置到 \(\vec r_h + d\vec r\),其中 \(\vec r_h\) 為現在的懸掛位置。
> Shift the hanging position of this pendulum to \(\vec r_h + d\vec r,\) where \(\vec r_h\) is its current hanging position.
***/
/*{{{*/
	pendulum.shiftPosition = function(dr){
		if(dr) pendulum.position.add(dr);
		return pendulum.setPosition(null,pendulum.rcm);
	};
/*}}}*/
/***
!!!! pendulum.checkAngle (angle)
<<<
* 如果使用歐拉方法,計算誤差足夠大,這裡需要確保角度不會大過初始角,因為計算的時間間隔是有限的,有機會在初始角度附近算出一個超過初始角度的結果,即便這個時候的速度很接近 0,就算超過也是很少,但這個很少的超過就會讓擺槌受力大了一點點,造成後續速度大一點點,因而再次超過初始角度的機會也就跟著大一點點,再次超過就會再次讓受力大一點,也就再提高超過機率一點,如此循環放大,沒多久就會讓擺槌振幅大到一個離譜的程度。<br>If Eular method is applied, the error is large enough, we need to make sure the angle does not exceed the initial angle, because the time interval in the calculation is finite, there is a chance when in the vicinity of initial angle the calculation gives a result exceeding the initial angle. Even though in such cases the velocity of the bob is very close to 0, the exceeded amount is very small, this small extra is going to increase force exerted on the bob, causing a small increase in the velocity, which in turn increases the chances of exceeding the initial angle again, then the force increases again, and chances of exceeding increase again, and on and on and on, resulting in a cyclic amplification, and the amplitude will soon get too large.

* 如果使用 4 階龍格-庫塔方法,誤差較小,不太需要擔心角度過大的問題。根據實際測試結果,時間步伐如果夠大,最大角度其實會逐漸變小。<br>If the 4th order ~Runge-Kutta method is applied, the error is small enough and we probably don't worry about the over-limit problem. According to actual testing results, the maximum angle reduces gradually if time step is large enough.
<<<
***/
/*{{{*/
	pendulum.checkAngle = function(angle){
		// The argument angle shall be a vector pointing along the axis of rotation and
		// having its length equal to the angle of rotation (in radians).
		//if(!chkMovable.checked && angle.value>pendulum.initialAngle.value){
			// If the angle is exceeding the initial value, put it back.
			// This can easily happen when the Euler method is applied,
			// because of the low accuracy in the algorithm.
		//	angle.value = pendulum.initialAngle.value;
		//}
		return pendulum;
	};
/*}}}*/
/***
!!!! pendulum.checkCycle ()
> 檢查是否完成一個週期。如果擺的速度的 //x// 分量在上一個瞬時為正,而現在這個瞬時為負,表示剛剛完成一個週期,以線性內差法估計轉折點的時間,並顯示週期。由於在轉折點附近的速度很小,線性內插足以得到很好的估計值。
> Determine whether a cycle is completed. If the //x//-component of pendulum's velocity is positive in the last moment, while negative in the current moment, that means one cycle is just completed. Apply the linear interpolation to estimate the moment at the turning point and show the period. Because the velocity is very slow around the turning point, linear interpolation suffices to give a very good estimation.
***/
/*{{{*/
	pendulum.lastV = vector();
	pendulum.lastT = 0;
	pendulum.checkCycle = function(){
		if(pendulum!==pendulums[0]) return;
		if(pendulum.lastV.x > 0 && pendulum.velocity.x < 0){
			// Just completed one cycle, apply linear interpolation to estimate
			// the moment at the turning point, and show the period.
			let turnT = simT-dt*(
				pendulum.velocity.x/(pendulum.velocity.x-pendulum.lastV.x)
			);
			showPeriod(turnT - pendulum.lastT);
			pendulum.lastT = turnT;
		}
		pendulum.lastV.copy(pendulum.velocity);
	};
/*}}}*/
/***
!!!! pendulum.setAngle (angle, done)
> 設定此擺與鉛直線之間的角度(弧度),並視情況將擺轉到該角度去(如果第二個引數 done 為 false 的話)
> Sets the angle (in radians) measured from the vertical line, and rotate the pendulum if necessary (if the 2nd argument done is false).
***/
/*{{{*/
	pendulum.setAngle = function(angle,done){
		if(angle){
			//pendulum.checkAngle(angle);
			pendulum.angle.copy(angle);
			if(angle.value===undefined){
				pendulum.angle.value = pendulum.angle.length();
				pendulum.angle.normalize();
			}else{
				pendulum.angle.value = angle.value;
			}
		}else{
			angle = pendulum.angle;
			//pendulum.checkAngle(angle);
		}

		pendulum.checkCycle();

		if(!done){
			// Rotate the pendulum to the desired angle.
			pendulum.string.setDirection(
				pendulum.string.rcm.set(0,-1,0)
					.applyAxisAngle(angle,angle.value)
			);
			pendulum.string.rcm.multiplyScalar(pendulum.length/2);
		}

		return pendulum;
	};
/*}}}*/
/***
!!!! pendulum.calculateTension ()
> 計算擺繩張力 \(\vec T\),根據【沿著擺繩方向(\(\hat r\))的合力做為擺的向心力】這個想法來進行:
> Calculate the tension \(\vec T\) in the rod, following the idea that //"the net force along the rod direction (\(\hat r\)) serves as the centripetal force for the pendulum"//:\[\begin{eqnarray*} & \vec T &+& (m\vec g \cdot \hat r)\hat r = m \vec a_c = m |\vec r_{CM}-\vec r_0| \omega^2 (-\hat r) \\ \to \quad & \boxed{\vec T} &=& -(m\vec g \cdot \hat r)\hat r - m_|\vec r_{CM}-\vec r_0|\omega^2 \hat r \\ \to \quad &&=& \boxed{-m(\vec g \cdot \hat r + |\vec r_{CM} - \vec r_0|\omega^2)\hat r},\end{eqnarray*}\] 其中 \(m\) 是擺的質量,\(\hat r\) 是沿著擺繩,從懸掛點指向擺錘方向的單位向量,\(\vec a_c\) 為擺的向心加速度,\(\vec r_{CM}\) 是擺的質心位置,\(\vec r_0\) 為轉軸通過的位置,可能為懸掛點(如果天花板為固定),或者是系統整體的質心(如果天花板可移動);而 \(\vec \omega\) 則為擺的角速度。
>where \(m\) is the mass of the bob, \(\hat r\) is the unit vector along the rod, pointing from the hanging point towards the bob, \(\vec a_c\) is the centripetal acceleration of the bob, \(\vec r_{CM}\) is the center of mass position of the pendulum, \(\vec r_0\) is the pivot position, which could be the hanging point (if the ceiling is fixed), or the system's center of mass position (if the ceiling is movable); while \(\vec \omega\) is the angular velocity of the pendulum.
>
>//注意:\(\vec T\) 的方向為向上,而 \(\vec g\) 與 \(\hat r\) 是向下。//
>//Note: The direction of \(\vec T\) is upward while that of \(\vec g\) and \(\hat r\) are downward.//
***/
//{{{
	pendulum.calculateTension = function(){
		let dr = pendulum.rcm.clone().sub(pendulum.pivotPosition());
		let ur = dr.clone().normalize();

		pendulum.string.tension.copy(ur).multiplyScalar(
			- fg.dot(ur)
			- pendulum.mass*dr.length()*pendulum.omega.lengthSq()
		);
		return pendulum;
	};
//}}}
/***
!!!! pendulum.calculateAlpha (\(\vec r\))
<<<
計算擺的角加速度 \(\vec \alpha\),使用下列公式:
Calculate the the angular acceleration \(\vec \alpha\) of the pendulum, using the following formula: \[\vec \tau = I \vec\alpha \qquad \to \qquad \boxed{\vec \alpha = \vec \tau I^{-1}},\] 其中 \(\vec \tau\) 是作用在擺的力矩,而 \(I\) 是擺對轉軸的轉動慣量。簡單情況下 \(I\) 只是純量,不過一般情況下它是張量。這裡我們只考慮簡單情況。
where \(\vec \tau\) is the torque acting on the pendulum, while \(I\) is the moment of inertia of the pendulum with respect to the axis of rotation. In simple cases \(I\) is just a scalar. In general cases, however, it is a tensor. Here we only consider the simple cases.
----
引數 \(\vec r\) 用來計算力矩 \(\vec \tau = \vec r \times \vec F\),
The argument \(\vec r\) is used to calculated the torque \(\vec \tau = \vec r \times \vec F,\)
# 在天花板固定的情況下為<br>is, when the ceiling is fixed, \[\vec \tau = (\vec r_{CM} - \vec r_h) \times \vec F_g,\] 其中 \(\vec r_{CM}\) 為擺的質心位置,\(\vec r_h\) 為懸掛位置,\(\vec F_g\) 為擺所受的重力。引數 \(\vec r = \vec r_{CM} - \vec r_h\) 必須在呼叫此函數前先計算好。<br>where \(\vec r_{CM}\) is the pendulum's center of mass position, \(\vec r_h\) is the pendulum's hanging position, while \(\vec F_g\) is the force of gravity acting on the pendulum. The argument \(\vec r = \vec r_{CM} - \vec r_h\) must be calculated accordingly before calling this function.
# 而在天花板可移動的情況下則<br>While the ceiling is movable, however, \[\vec \tau = \vec\tau_g + \vec\tau_c = (\vec r_{CM} - \vec r_p) \times \vec F_g + (\vec r_h - \vec r_p) \times \vec F_c,\] 其中 \(\vec\tau_g\) 為重力產生的力矩,\(\vec r_p\) 表示等效的轉軸點,\(\vec F_c\) 表示因天花板移動造成的力,此力對擺產生的力矩為 \(\vec\tau_c\)。引數 \(\vec r = \vec r_{CM} - \vec r_p\) 必須在呼叫此函數前先計算好。<br>where \(\vec \tau_g\) is the torque due to gravitational force, \(\vec r_p\) is the equivalent pivot position, \(\vec F_c\) is the resultant force due to the motion of the ceiling, the torque exerted on the pendulum due to this force is \(\vec \tau_c.\) The argument \(\vec r = \vec r_{CM} - \vec r_p\) must be calculated accordingly before calling this function.
** 這個天花板移動造成的力 \(\vec F_c\),根據 Ref[1] 以及 Ref[2] 裡的說法,是假想的慣性力 \(\boxed{-m_pd^2\vec r_c / dt^2}\),其中 \(m_p\) 是擺的質量而 \(\vec r_c\) 是天花板的位置。<br>The force \(\vec F_c\) due to ceiling's motion is, according to Ref[1] and Ref[2], a fictitious force of inertia \(\boxed{-m_pd^2\vec r_c / dt^2},\) where \(m_p\) is pendulum's mass while \(\vec r_c\) is the ceiling's position.
** 若假設天花板只能水平移動,則其加速度只能有水平方向的分量,但由於整個系統在水平方向沒有受力,系統質心在水平方向是不會移動的,<br>If we assume the ceiling is only movable horizontally, its acceleration then has only horizontal components. Because there is no horizontal force exerted on the system, the system's center of mass do not move horizontally, \[{d\vec R_{CM,\parallel} \over dt} = {d \over dt}\left(m_c\vec r_{c,\parallel} + \sum(m_p\vec r_{p,\parallel}) \over m_c + \sum m_p\right) = 0,\] 其中加總是對所有懸掛在此天花板下的擺進行的。這讓我們很容易寫下<br>where the summation is carried out over all the pendulums hanging under this ceiling. This allows us to easily write down \[{d^2\vec r_{c,\parallel} \over dt^2} = -{1 \over m_c}{d^2 \over dt^2}\sum(m_p\vec r_{p,\parallel}) = -{1 \over m_c}\sum m_p{d^2\vec r_{p,\parallel} \over dt^2} = -{1 \over m_c}\sum m_p \vec a_{p,\parallel}.\]
** 如此我們可以用擺的加速度之水平分量來寫出這個假想的慣性力<br>Now we can write down this fictitious force in terms of the parallel component of pendulum's acceleration \[\boxed{\vec F_c = {m_p \over m_c}\sum m_p\vec a_{p,\parallel}.}\]
----
# http://www.math.pitt.edu/~bard/classes/mth3380/syncpapers/metronome.pdf
# http://www.physik3.gwdg.de/~ulli/pdf/UMP09.pdf
<<<
***/
/*{{{*/
	pendulum.calculateAlpha = function(r){
		let torque = r.clone().cross(fg);
		if(chkMovable.checked){
			torque.add(
				optPivot && optPivot.value === 'Ref'
					?	ceiling.acceleration.clone()
							.multiplyScalar(-pendulum.mass).cross(r)
					:	pendulum.position.clone().sub(pendulum.pivot.position)
							.cross(
								ceiling.acceleration.clone()
									.multiplyScalar(-pendulum.mass)
							)
			);
		}
		let alpha = torque.multiplyScalar(
			1/pendulum.I(
				 pendulum.pivotPosition()
				,pendulum.axisOfRotation()
			)
		);
		pendulum.alpha.copy(alpha);
		return alpha;
	};
/*}}}*/
/***
!!!! pendulum.calculateAcceleration (\(\vec r, \vec v\))
<<<
根據現在位置 \(\vec r\)(從圓周運動的中心量起)與速度 \(\vec v\) 計算現在所受的加速度,
Calculate the acceleration of this pendulum according to its current position \(\vec r\) (measured from the center of the circular motion) and velocity \(\vec v,\) \[\vec a = \vec a_c + \vec a_t = {v^2 \over r}(-\hat r) + \vec\alpha \times \vec r,\] 其中 \(\vec a_c\) 及 \(\vec a_t\) 分別為向心與切線分量,\(\vec \alpha\) 為擺的角加速度。
where \(\vec a_c\) and \(\vec a_t\) are the centripetal and tangential components, respectively, while \(\vec \alpha\) is the angular acceleration of the pendulum.
一併計算擺繩的張力,
Also calculate the tension in the rod, \[\vec T + m\vec g = m\vec a \qquad \to \qquad \vec T = m\vec a - m\vec g.\]
<<<
***/
/*{{{*/
	pendulum.calculateAcceleration = function(r,v){
		r = r.clone().sub(pendulum.pivotPosition());
		let ur = r.clone().normalize();
		let a = ur.clone().multiplyScalar(
			-v.lengthSq()/r.length()
		);
		pendulum.string.tension.copy(a).multiplyScalar(pendulum.mass).sub(
			ur.multiplyScalar(fg.dot(ur))
		);
		a.add(pendulum.calculateAlpha(r).cross(r));
		pendulum.acceleration.copy(a);
		//pendulum.string.tension.copy(a).multiplyScalar(pendulum.mass).sub(fg);
		return a;
	};
/*}}}*/
/***
!!!! pendulum.calculateLinearStatus ()
<<<
根據現在的角度運動狀態(角速度、角加速度)來計算擺的線性運動狀態(速度、加速度、張力)。
Calculatee the status of the pendulum's linear motion (linear velocity, linear acceleration, tension), using its current angular motion status (angular velocity, angular acceleration).
* 速度只有切線方向,其計算是根據<br>The velocity is only tangential and calculated according to \[\vec v = \vec \omega \times (\vec r_{CM} - \vec r_0),\] 其中 \(\vec r_\text{CM}\) 是擺的質心位置,而 \(\vec r_0\) 則是轉軸通過的位置,可能是擺的懸掛點(如果天花板固定),或者是整體的質心位置(如果天花板可移動)。<br>where \(\vec r_{CM}\) is the pendulum's center of mass position, while \(\vec r_0\) is the pivot position, which could be the hanging point (if the ceiling is fixed), or the whole system's center of mass position (if the ceiling is movable).

* 加速度則有切線及向心兩個分量,<br>The acceleration has tangential and centripetal components, \[\vec a = \vec a_c + \vec a_t = |\vec r_{CM}-\vec r_0|\omega^2(-\hat l) + |\vec r_{CM}-\vec r_0|\alpha \hat t,\] 其中 \(\vec a_c\) 是向心(centripetal)而 \(\vec a_t\) 是切線(tangential)加速度,\(\hat l\) 是沿著擺繩在離心方向的單位向量,而 \(\hat t\) 是切線方向的單位向量。<br>where \(\vec a_c\) is the centripetal and \(\vec a_t\) is the tangential acceleration, \(\hat l\) is the unit vector along the rod pointing in the centrifugal direction, while \(\hat t\) is the unit vector in the tangential direction.
<<<
***/
/*{{{*/
	pendulum.calculateLinearStatus = function(){
		// 計算線速度 Calculate linear velocity
		let dr = pendulum.rcm.clone().sub(pendulum.pivotPosition());
		pendulum.velocity.copy(pendulum.omega).cross(dr);
		// 計算加速度 Calculate the acceleration.
		pendulum.acceleration.copy(dr).multiplyScalar(-pendulum.omega.lengthSq())
				.add(dr.multiplyScalar(pendulum.alpha.length()).cross(pendulum.angle));
		// 計算張力 Calculate the tension.
		pendulum.calculateTension();
	};

	return pendulum;
};
/*}}}*/
/***
>End of pendulum definitions.
!!! A collection of pendulums
***/
/*{{{*/
let Pendulums = function(n){
	let pendulums = [];
	pendulums.create = function(n){
		if(n < 1) n = 1;
		let L = +txtRodLength.value;
		let len = pendulums.length;
		if(n > len){
			// n > length
			for(let i=pendulums.length; i<n; i++){
				pendulums[i] = new Pendulum({
					rod: new Rod({
						radius: 0.003,
						axis: vector(0,-txtRodLength.value,0),
						color: 0xff0000,
						length: L
					},'noadd'),
					bob: new Bob({
						radius: 0.05,
						color: 0x00ff00,
						opacity: 0.3
					},'noadd'),
					pos: vector()
				});
				//pendulums[i].string.name = 'rod'+i;
				//pendulums[i].bob.name = 'bob'+i;
				//pendulums[i].spherecm.name = 'cm'+i;
			}
		}else if(n < len){
			// n < length
			for(let i=len-1; i>=n; i--){
				pendulums[i].destroy();
			}
			pendulums.splice(n);
		}
	};
	if(typeof n !== 'number' || n <= 0)
		n = +txtNP.value;
	pendulums.create(n);

	let randomShift = function(max){
		return (max||1)*Math.random()*(Math.random()<0.5?-1:1);
	};

	pendulums.init = function(){
		let N = pendulums.length, n;
		let L = +txtRodLength.value;
		let a0 = txtInitialAngle.value*Math.PI/180;
		let dr = vector(0,0,0);
		let sep = [], width = 0;
		//let movable = chkMovable.checked;
		let pos = vector(), p;
		for(n=0; n<N; n++){
			p = pendulums[n];
			p.initialAngle.set(0,0,1);
			p.initialAngle.value = a0*(1+Math.random()/10)*(n%2?1:-1);
			p.init();
			//p.setAngle();
			p.setLength(L*(1+Math.random()/10));
			//p.setPosition();
			sep[n] = p.length*Math.sin(p.initialAngle.value)*3;
			width += sep[n];
		}
		// Adjust the ceiling width if necessary.
		if(ceiling.getWidth()<width)
			ceiling.setWidth(width);
		// Determine the maximum separation.
		let maxsep = sep[0];
		for(n=1; n<N; n++)
			if(maxsep < sep[n]) maxsep = sep[n];
		dr.set(maxsep,0,0);
		// Determine the position of the first pendulum.
		// In a 1D case, the first pendulum is the left-most one.
		// We will arrange the pendulums in a symmetrical way.
		let r0 = N === 1
			?	ceiling.position.clone()
			:	dr.clone().multiplyScalar(
					(N % 2
						? -(N-1)/2.0		// even number, half-half
						: -N/2)				// odd number, one at the center, others half-half
				).add(ceiling.position);
		// Now set the pendulum positions.
		for(n=0; n<N; n++){
			pos.copy(dr).multiplyScalar(n).add(r0);
			p = pendulums[n];
			p.setPosition(pos);
			p.calculateVertical();
		}
		return pendulums;
	};
	// 代表張力的箭頭 The arrow representing the tension
	pendulums.arrT = arrow({
		axis: pendulums[0].string.tension.clone(),
		color: 0x00ffff
	});
	//pendulums.arrT.name = 'arrT';

	pendulums.force = vector();
	// 代表合力的箭頭 The arrow representing the total force
	pendulums.arrF = arrow({
		axis: pendulums.force,
		color: 0xff0000
	});
	//pendulums.arrF.name = 'arrT';

	// 代表力矩的箭頭 The arrow representing the torque
	pendulums.arrTau = arrow({
		axis: pendulums[0].torque,
		color: 0xffff00
	});
	//pendulums.arrTau.name = 'arrT';

	// 代表速度的箭頭 The arrow representing the velocity
	pendulums.arrV = arrow({
		axis: pendulums[0].velocity,
		color: 0x00ff00
	});
	//pendulums.arrV.name = 'arrT';

	// 代表加速度的箭頭 The arrow representing the acceleration
	pendulums.arrA = arrow({
		axis: pendulums[0].acceleration,
		color: 0xffffff
	});
	//pendulums.arrA.name = 'arrT';
/*}}}*/
/***
!!!! pendulums.calculateAccelerations (//x//[], //v//[], //t//, //dt//, //a//[])
<<<
根據目前所有的擺與天花板的質心位置與速度,計算所有物體的加速度。
Calculate the accelerations of all the pendulums and the ceiling, using their current center of mass positions and velocities.
* 第一個引數 //x//[] 是一個陣列,其元素為所有擺的質心位置,最後一個元素為天花板的質心位置。<br>The first argument //x//[] is an array, of which the elements are the pendulums' center of mass positions, while the last element is the ceilings center of mass position.
* 第二個引數 //v//[] 也是一個陣列,其元素為所有擺的質心速度,最後一個元素為天花板的質心速度。<br>The second argument //v//[] is also an array, of which the elements are the pendulums' center of mass velocity, while the last element is the ceilings center of mass velocity.
* 第三個引數 //t// 為現在時刻。<br>The third argument //t// is the current time.
* 第四個引數 //dt// 為時間間隔。<br>The fourth argument //dt// is the time step.
* 最後一個引數(非必要)//a//[] 也是一個陣列,其元素為所有擺的質心加速度,最後一個元素為天花板的質心加速度。<br>The last argument (optional) //a//[] is also an array, of which the elements are the pendulums' center of mass acceleration, while the last element is the ceilings center of mass acceleration.
* 傳回一個陣列,其元素為所有擺的質心加速度,最後一個元素為天花板的質心加速度。<br>Returns an array, of which the elements are the pendulums' center of mass accelerations, while the last element is the ceiling's center of mass acceleration.
<<<
***/
/*{{{*/
	pendulums.totalA = vector();
	pendulums.calculateAccelerations = function(x, v, t, dt, a){
		let n, p, len = pendulums.length, ac = 0;
		let pa = [];
		//if(!chkOneD || chkOneD.checked){
			if(chkMovable.checked){
				pa.length = len+1;
				ac = a ? a[len] : ceiling.acceleration.x;
				// Calculate the acceleration of the ceiling.
				pa[len] = 0;
				for(n=0; n<len; n++){
					p = pendulums[n];
					pa[len] -=
						p.mass*p.length*(
							(a?a[n]:p.alpha.z)*Math.cos(x[n])-
								v[n]*v[n]*Math.sin(x[n])
						);
				}
				pa[len] /= (pendulums.mass+ceiling.mass);
			}else
				pa.length = len;

			// Calculate the angle of each pendulum.
			for(n=0; n<len; n++){
				p = pendulums[n];
				pa[n] = -(
					-scene.g.y*Math.sin(x[n])+
					ac*Math.cos(x[n])
				)/p.length;
				//p.alpha.set(0,0,pa[n]);
			}
		//}else{
		/*
			if(chkMovable.checked) len--;
			let tmpa = vector();
			pendulums.totalA.set(0,0,0);
			for(n=0; n<len; n++){
				p = pendulums[n];
				pa[n] = p.calculateAcceleration(x[n],v[n],t,dt,a[n]);
				pendulums.totalA.add(
					tmpa.copy(pa[n]).multiplyScalar(p.mass)
				);
			}
			if(chkMovable.checked){
				pa[len] = pendulums.totalA.clone()
					.projectOnPlane(ceiling.normal)
					.multiplyScalar(-1/ceiling.mass);
			}
		}
		*/
		return pa;
	};
/*}}}*/
/***
!!!! pendulums.calculateCM ()
> 計算全部擺的質心。
> Calculate the center of mass for all the pendulums.
***/
/*{{{*/
	pendulums.rcm = vector();
	pendulums.calculateCM = function(){
		pendulums.rcm.set(0,0,0);
		let tmp = vector();
		for(let n=0,len=pendulums.length; n<len; n++){
			pendulums.rcm.add(tmp.copy(pendulums[n].rcm).multiplyScalar(pendulums[n].mass));
		}
		return pendulums.rcm.multiplyScalar(1/pendulums.mass);
	};
/*}}}*/
/***
!!!! pendulums.rCMs (rcm)
> 取得所有擺的質心位置,存放在陣列裡。引數 rcm 為非必要,如果有傳入,則必須是陣列,且所有擺的質心位置會存放在裡面。
> Returns the center of mass positions of all pendulums in an array. The argument rcm is optional. If given, it must be an array, and all the pendulum's center of mass positions will be stored in it.
***/
/*{{{*/
	pendulums.rCMs = function(rcm){
		if(!rcm) rcm = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			rcm[n] = pendulums[n].rcm;
		}
		return rcm;
	};
/*}}}*/
/***
!!!! pendulums.vCMs (vcm)
> 取得所有擺的質心速度,存放在陣列裡。引數 vcm 為非必要,如果有傳入,則必須是陣列,且所有擺的質心速度會存放在裡面。
> Returns the center of mass velocities of all pendulums in an array. The argument vcm is optional. If given, it must be an array, and all the pendulum's center of mass velocities will be stored in it.
***/
/*{{{*/
	pendulums.vCMs = function(vcm){
		if(!vcm) vcm = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			vcm[n] = pendulums[n].velocity;
		}
		return vcm;
	};
/*}}}*/
/***
!!!! pendulums.getThetas (\(\theta\)[])
> 取得所有擺的現在角度,存放在陣列裡。引數 \(\theta\) 為非必要,如果有傳入,則必須是陣列,且所有擺的角度會存放在裡面。
> Returns the current angles of all pendulums in an array. The argument \(\theta\) is optional. If given, it must be an array, and all the pendulum angles will be stored in it.
***/
/*{{{*/
	pendulums.getThetas = function(theta){
		if(!theta) theta = [];
		for(let n=0,p,len=pendulums.length; n<len; n++){
			p = pendulums[n];
			theta[n] = p.angle.z > 0 ? p.angle.value : -p.angle.value;
		}
		return theta;
	};
	pendulums.getAngles = pendulums.getThetas;
/*}}}*/
/***
!!!! pendulums.getOmegas (\(\omega\)[])
> 取得所有擺的現在角速度,存放在陣列裡。引數 \(\omega\) 為非必要,如果有傳入,則必須是陣列,且所有擺的角速度會存放在裡面。
> Returns the current angular speed of all pendulums in an array. The argument \(\omega\) is optional. If given, it must be an array, and all the pendulum angular speeds will be stored in it.
***/
/*{{{*/
	pendulums.getOmegas = function(omega){
		if(!omega) omega = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			omega[n] = pendulums[n].omega.z;
		}
		return omega;
	};
	pendulums.getAngularSpeeds = pendulums.getOmegas;
/*}}}*/
/***
!!!! pendulums.getAlphas (\(\alpha\)[])
> 取得所有擺的現在角加速度,存放在陣列裡。引數 \(\alpha\) 為非必要,如果有傳入,則必須是陣列,且所有擺的加角速度會存放在裡面。
> Returns the current angular acceleration of all pendulums in an array. The argument \(\alpha\) is optional. If given, it must be an array, and all the pendulum angular accelerations will be stored in it.
***/
/*{{{*/
	pendulums.getAlphas = function(alpha){
		if(!alpha) alpha = [];
		for(let n=0,len=pendulums.length; n<len; n++){
			alpha[n] = pendulums[n].alpha.z;
		}
		return alpha;
	};
	pendulums.getAngularAccelerations = pendulums.getAlphas;
/*}}}*/
/***
!!!! pendulums.updateLinearStatus ()
<<<
更新擺的運動狀態,如速度、加速度等。
Update the status of the pendulum's motion, such as linear velocity and linear acceleration.
<<<
***/
/*{{{*/
	pendulums.updateLinearStatus = function(){
		// 調整代表速度的箭頭 Adjust the representing arrow for velocity.
		pendulums.arrV.setAxis(
			pendulums.arrV.axis.copy(pendulums[0].velocity)
					.multiplyScalar(vafactor)
		);
		pendulums.arrV.position.copy(pendulums[0].rcm);
		pendulums.arrV.visible = chkShowVA.checked;

		// 調整代表加速度的箭頭 Adjust the representing arrow for acceleration.
		pendulums.arrA.setAxis(
			pendulums.arrA.axis.copy(pendulums[0].acceleration)
					.multiplyScalar(vafactor)
		);
		pendulums.arrA.position.copy(pendulums[0].rcm);
		pendulums.arrA.visible = chkShowVA.checked;

		// 調整代表張力的箭頭 Adjust the representing arrow for tension.
		pendulums.arrT.setAxis(
			pendulums.arrT.axis.copy(pendulums[0].string.tension)
					.multiplyScalar(lengthfactor)
		);
		pendulums.arrT.position.copy(pendulums[0].rcm);
		pendulums.arrT.visible = chkShowF.checked;

		pendulums.force.copy(fg).add(pendulums[0].string.tension);
		// 調整代表合力的箭頭 Adjust the representing arrow for net force.
		pendulums.arrF.setAxis(
			pendulums.arrF.axis.copy(pendulums.force)
					.multiplyScalar(lengthfactor)
		);
		pendulums.arrF.position.copy(pendulums[0].rcm);
		pendulums.arrF.visible = chkShowF.checked;

		// 調整代表重力的箭頭 Adjust the representing arrow for gravitational force.
		arrFg.position.copy(pendulums[0].rcm);
		arrFg.visible = chkShowF.checked;

		return pendulums;
	};

	return pendulums;
};
/*}}}*/
/***
!!! Data recording
***/
/*{{{*/
let clearData = function(){
	anglePlot.clear([0,2],[0,0.05]);
	anglePlot.setXData($tw.data.dataArray()).setYData($tw.data.dataArray());
	if(omegaPlot){
		omegaPlot.clear([0,2],[0,0.05]);
		omegaPlot.setXData($tw.data.dataArray()).setYData($tw.data.dataArray());
	}
	if(alphaPlot){
		alphaPlot.clear([0,2],[0,0.05]);
		alphaPlot.setXData($tw.data.dataArray()).setYData($tw.data.dataArray());
	}
	if(xPlot){
		xPlot.clear([0,2],[0,0.05]);
		xPlot.setXData($tw.data.dataArray()).setYData($tw.data.dataArray());
	}
	if(yPlot){
		yPlot.clear([0,2],[0,0.05]);
		yPlot.setXData($tw.data.dataArray()).setYData($tw.data.dataArray());
	}
	if(forcePlot){
		forcePlot.clear([0,2],[0,0.05]);
		forcePlot.setXData($tw.data.dataArray()).setYData($tw.data.dataArray());
	}

	for(let n=1, N=pendulums.length; n<N; n++){
		anglePlot.addYData($tw.data.dataArray());
		xPlot.addYData($tw.data.dataArray());
	}
};
let recordData = function(){
	anglePlot.addXPoint(simT);
	for(let n=0,N=pendulums.length; n<N; n++){
		let p = pendulums[n];
		anglePlot.addYPoint(
			p.angle.value/Math.PI*180
				*(p.angle.dot(p.initialAngle)>0?1:-1),
			n
		);
	}
	if(chkPlotAngle.checked) anglePlot.update();

	if(omegaPlot){
		omegaPlot.add(
			 simT
			,pendulums[0].omega.length()
				*(pendulums[0].omega.dot(pendulums[0].initialAngle)>0?1:-1)
		);
		if(chkPlotOmega.checked) omegaPlot.update();
	}

	if(alphaPlot){
		alphaPlot.add(
			 simT
			,pendulums[0].alpha.length()
				*(pendulums[0].alpha.dot(pendulums[0].initialAngle)>0?1:-1)
		);
		if(chkPlotAlpha.checked) alphaPlot.update();
	}

	if(chkMovable.checked){
		if(xPlot){
			xPlot.addXPoint(simT)
			for(let n=0,N=pendulums.length; n<N; n++){
				//xPlot.addYPoint(ceiling.position.x,n);
				xPlot.addYPoint(pendulums[n].bob.position.x,n);
			}
		}
		if(yPlot) yPlot.add(simT, Rcm.y);
		//if(xPlot) xPlot.add(simT, pendulums[0].pivot.position.x);
		//if(yPlot) yPlot.add(simT, pendulums[0].pivot.position.y);
	}else{
		if(xPlot){
			xPlot.addXPoint(simT)
			for(let n=0,N=pendulums.length; n<N; n++){
				xPlot.addYPoint(pendulums[n].bob.position.x,n);
			}
		}
		if(yPlot) yPlot.add(simT, pendulums[0].bob.position.y);
	}
	if(xPlot && chkPlotPositionX.checked) xPlot.update();
	if(yPlot && chkPlotPositionY.checked) yPlot.update();

	if(forcePlot){
		forcePlot.add(simT, pendulums.force.x);
		if(chkPlotForce.checked) forcePlot.update();
	}
};
/*}}}*/
/***
!!! Status functions
***/
/*{{{*/
let checkPlots = function(){
	anglePlot.style('display',(chkPlotAngle.checked ? '' : 'none'));
	if(omegaPlot) omegaPlot.style('display',(chkPlotOmega.checked ? '' : 'none'));
	if(alphaPlot) alphaPlot.style('display',(chkPlotAlpha.checked ? '' : 'none'));
	if(xPlot) xPlot.style('display',(chkPlotPositionX.checked ? '' : 'none'));
	if(yPlot) yPlot.style('display',(chkPlotPositionY.checked ? '' : 'none'));
	if(forcePlot) forcePlot.style('display',(chkPlotForce.checked ? '' : 'none'));
};
let NPChanged = function(){
	let NP = +txtNP.value;
	if(NP !== pendulums.length){
		pendulums.create(NP);
		return true;
	}
	return false;
};
let lengthChanged = function(){
	let L = +txtRodLength.value;
	let changed = false;
	for(let n=0, len=pendulums.length; n<len; n++){
		if (L !== pendulums[n].length){
			pendulums[n].setLength(L);
			changed = true;
		}
	}
	return changed;
};
let angleChanged = function(){
	let angle = txtInitialAngle.value/180*Math.PI;
	let changed = false;
	for(let n=0, len=pendulums.length; n<len; n++){
		if (!pendulums[n].initialAngle.value
			|| angle !== pendulums[n].initialAngle.value){
			pendulums[n].initialAngle.set(0,0,1);
			pendulums[n].initialAngle.value = angle;
			changed = true;
		}
	}
	return changed;
};
let massChanged = function(){
	let mceiling = +txtCeilingMass.value;
	let ceilingchanged = mceiling !== ceiling.mass;
	if(ceilingchanged) ceiling.mass = mceiling;

	let mbob = +txtBobMass.value;
	let mrod = +txtRodMass.value;
	let bobchanged = false;
	let rodchanged = false;
	pendulums.mass = 0;
	for(let n=0, len=pendulums.length; n<len; n++){
		if(mbob !== pendulums[n].bob.mass) bobchanged = true;
		if(mrod !== pendulums[n].string.mass) rodchanged = true;
		if(bobchanged || rodchanged){
			// The bob's and/or the rod's mass of this pendulum are/is changed.
			pendulums[n].bob.mass = mbob;
			pendulums[n].string.mass = mrod;
		}
		pendulums.mass += (
			pendulums[n].mass = pendulums[n].bob.mass + pendulums[n].string.mass
		);
	}

	fg.copy(scene.g).multiplyScalar(pendulums[0].mass);
	arrFg.setAxis(arrFg.axis.copy(fg).multiplyScalar(lengthfactor));

	return (ceilingchanged || bobchanged || rodchanged);
};
let timeIntervalChanged = function(){
	let t = +txtdT.value
	if (dt !== t){
		dt = t;
		return true;
	}
	return false;
};
let nperiods = 0;
let showPeriod = function(T,ndec){
	if(typeof ndec !== 'number') ndec = 3;
	if(T===0) nperiods = 0;
	labelT.innerText =
		(typeof T === 'number' ? $tw.ve.round(T,ndec) : 'T')
		+ '/' + $tw.ve.round(pendulums[0].theoreticalPeriod(),ndec)
		+ ' (cycles:'+nperiods+')';
	nperiods++;
};
let showPositions = function(){
	let ndec = 4;
	let initial = +txtInitialAngle.value;
	let max = anglePlot.getDataMax();
	let min = anglePlot.getDataMin();
	labelAngle.innerText =
		$tw.ve.round(
			pendulums[0].angle.value*180/Math.PI
				*(pendulums[0].angle.dot(pendulums[0].initialAngle)>0?1:-1)
			,ndec
		)+'/('+$tw.ve.round(min,ndec)+','+$tw.ve.round(max,ndec)+')';
	labelAngleErr.innerText =
			'('+$tw.ve.round(-min/initial*100,ndec-2)+'%,'
			+$tw.ve.round(max/initial*100,ndec-2)+'%)';

	if(labelOmega)
		labelOmega.innerText =
			$tw.ve.round(
				pendulums[0].omega.length()
					*(pendulums[0].omega.dot(pendulums[0].initialAngle)>0?1:-1)
				,ndec
			)+'/('+$tw.ve.round(omegaPlot.getDataMin(),ndec)
			+','+$tw.ve.round(omegaPlot.getDataMax(),ndec)+')';

	if(labelAlpha)
		labelAlpha.innerText =
			$tw.ve.round(
				pendulums[0].alpha.length()
				*(pendulums[0].alpha.dot(pendulums[0].initialAngle)>0?1:-1)
				,ndec
			)+'/('+$tw.ve.round(alphaPlot.getDataMin(),ndec)
			+','+$tw.ve.round(alphaPlot.getDataMax(),ndec)+')';

	if(labelPositionX){
		max = xPlot.getDataMax();
		min = xPlot.getDataMin();
		labelPositionX.innerText = $tw.ve.round(
			chkMovable.checked
				? ceiling.position.x : pendulums[0].bob.position.x
			,ndec
		)+'/('+$tw.ve.round(min,ndec)
		+','+$tw.ve.round(max,ndec)+')';
		labelPositionXErr.innerText = chkMovable.checked
			? '('+$tw.ve.round((max-min)*1e15,ndec)+'E-15)' : '';
	}

	if(labelPositionY){
		max = yPlot.getDataMax();
		min = yPlot.getDataMin();
		labelPositionY.innerText = $tw.ve.round(
			chkMovable.checked
				? Rcm.y : pendulums[0].bob.position.y
			,ndec
		)+'/('+$tw.ve.round(min,ndec)
		+','+$tw.ve.round(max,ndec)+')';
		labelPositionYErr.innerText = chkMovable.checked
			? '('+$tw.ve.round(max-min,ndec)+')' : '';
	}

	if(labelForce){
		max = forcePlot.getDataMax();
		min = forcePlot.getDataMin();
		labelForce.innerText = $tw.ve.round(
			pendulums.force.x
			,ndec
		)+'/('+$tw.ve.round(min,ndec)
		+','+$tw.ve.round(max,ndec)+')';
	}
};
/*}}}*/
/***
!! Initialization Section
Initialize scene and variables.
!!!! The scene and axes.
***/
/*{{{*/
scene.axes({
	pos: vector(-1.7,0,0),
	length: 0.2
});
let Rcm = vector();
let sphereRcm = sphere({
	radius: 0.02,
	opacity: 0.6
});
// 初始先暫停
if (!chkPause.checked) chkPause.click();
//sphereRcm.name = 'Rcm';
/*}}}*/
/***
!!!! UI widgets
***/
/*{{{*/
let txtInitialAngle = document.getElementById('txtInitialAngle');

let txtBobMass = document.getElementById('txtBobMass');
let txtRodMass = document.getElementById('txtRodMass');
let txtCeilingMass = document.getElementById('txtCeilingMass');

let txtNP = document.getElementById('txtNP');
if(txtNP) txtNP.parentNode.previousSibling.title = txtNP.title;

let txtRodLength = document.getElementById('txtRodLength');

let txtdT = document.getElementById('txtdT');
let txtFineSteps = document.getElementById('txtFineSteps');

let optPivot = document.getElementById('optPivot');

let labelT = document.getElementById('labelPeriod');
let labelFPS = document.getElementById('labelFPS');
let labelTPF = document.getElementById('labelTPF');
let labelSimT = document.getElementById('labelSimT');
let labelAngle = document.getElementById('labelAngle');
let labelOmega = document.getElementById('labelOmega');
let labelAlpha = document.getElementById('labelAlpha');
let labelPositionX = document.getElementById('labelPositionX');
if(labelPositionX)
	labelPositionX.title = "X-component of the system's center of mass.";
let labelPositionXErr = document.getElementById('labelPositionXErr');
if(labelPositionXErr)
	labelPositionXErr.title = "Error in the x-component of the system's center of mass.";
let labelPositionY = document.getElementById('labelPositionY');
let labelPositionYErr = document.getElementById('labelPositionYErr');

let labelForce = document.getElementById('labelForce');
/*}}}*/
/***
!! Data visualization: The plotters
***/
/*{{{*/
let anglePlot = $tw.data.linearPlot({
	id: 'anglePlot',
	xTitle: 'Time (sec)',
	yTitle: (config.browser.isIE ? 'Angle (&deg;)' : '\\(\\theta\\ (^\\circ)\\)'),
	Title: 'Angle vs Time'
});
let omegaPlot = null;
if(labelOmega) omegaPlot = $tw.data.linearPlot({
	id: 'omegaPlot',
	xTitle: 'Time (sec)',
	yTitle: (config.browser.isIE ? 'Omega (rad/s)' : '\\(\\omega\\ \\text{(rad/s)}\\)'),
	Title: 'Omega vs Time'
});
let alphaPlot = null;
if(labelAlpha) alphaPlot = $tw.data.linearPlot({
	id: 'alphaPlot',
	xTitle: 'Time (sec)',
	yTitle: (config.browser.isIE ? 'Alpha (rad/s^2)' : '\\(\\alpha\\ (\\text{rad/s}^2\text{)}\\)'),
	Title: 'Alpha vs Time'
});
let xPlot = null;
if(labelPositionX){
	xPlot = $tw.data.linearPlot({
		id: 'xPlot',
		xTitle: 'Time (sec)',
		yTitle: (config.browser.isIE ? 'x (m)' : '\\(x\\) (m)'),
		Title: 'Position.X vs Time'
	});
	chkPlotPositionX.title = labelPositionX.title;
}
let yPlot = null;
if(labelPositionY) yPlot = $tw.data.linearPlot({
	id: 'yPlot',
	xTitle: 'Time (sec)',
	yTitle: (config.browser.isIE ? 'y (m)' : '\\(y\\) (m)'),
	Title: 'Position.Y vs Time'
});
let forcePlot = null;
if(labelForce) forcePlot = $tw.data.linearPlot({
	id: 'forcePlot',
	xTitle: 'Time (sec)',
	yTitle: (config.browser.isIE ? 'F (N)' : '\\(\\mathbf{F}\\) (N)'),
	Title: 'Force on Ceiling vs Time'
});
/*}}}*/
/***
!! The ceiling
***/
/*{{{*/
let ceiling = box({
	size: 2,
	height: 0.002,
	color:0xbbbbbb
});
ceiling.mass = +txtCeilingMass.value;
ceiling.normal = vector(0,-1,0);
ceiling.velocity = vector();
ceiling.acceleration = vector();
ceiling.lastPosition = vector();
//ceiling.name = 'ceiling';
/*}}}*/
/***
!! The pendulums
***/
/*{{{*/
let pendulums = new Pendulums();
/*}}}*/
/***
!! updatePositions ()
> 更新計算過後所有擺的位置,以及天花板的位置,如果天花板可以移動的話。
> Update the positions of all pendulums after calculations, and that of the ceiling, if it is movable.
***/
/*{{{*/
let updatePositions = function(x,v,a){
	let len = pendulums.length;
	if(!x){
		// Nothing is passed in, just update the current positions.
		for(let n=0,p; n<len; n++){
			pendulums[n].setPosition();
			pendulums[n].calculateLinearStatus();
		}
	}else{
		if(chkMovable.checked){
			let dr = vector();
			dr.copy(ceiling.position).sub(ceiling.lastPosition);
			ceiling.lastPosition.copy(ceiling.position);
			ceiling.position.x = x[len];
			ceiling.velocity.x = v[len];
			ceiling.acceleration.x = a[len];
			for(let n=0; n<len; n++){
				pendulums[n].position.add(dr);
				//pendulums[n].setPosition();
				pendulums[n].calculateVertical();
			}
		}
		//if(!chkOneD || chkOneD.checked){
			for(let n=0,p; n<len; n++){
				p = pendulums[n];
				p.alpha.set(0,0,a[n]);
				p.omega.set(0,0,v[n]);
				p.angle.set(0,0,(x[n]>=0 ? 1 : -1));
				p.angle.value = x[n]>=0 ? x[n] : (-x[n]);
				p.setAngle();
				p.setPosition();
				p.calculateLinearStatus();
			}
		//}else{
		//	for(let n=0; n<len; n++){
		//		pendulums[n].setPosition(null,pendulums[n].rcm);
		//	}
		//}
	}

	pendulums.calculateCM();
	calculateSystemCM();
	pendulums.updateLinearStatus();
};
/*}}}*/
/***
!! Calculating the System's Center of Mass
***/
/*{{{*/
let calculateSystemCM = function(){
	// Calculate Rcm of ceiling and pendulums
	Rcm.copy(ceiling.position).multiplyScalar(ceiling.mass).add(
		pendulums.rcm.clone().multiplyScalar(pendulums.mass)
	).multiplyScalar(
		1/(ceiling.mass+pendulums.mass)
	);
	sphereRcm.position.copy(Rcm);
	sphereRcm.visible = chkCM.checked;
};
/*}}}*/
/***
!! The Reset funciton
***/
/*{{{*/
let reset = function(){
	ceiling.acceleration.set(0,0,0);
	ceiling.velocity.set(0,0,0);
	ceiling.position.set(0,0.7,0);
	ceiling.lastPosition.copy(ceiling.position);
	simT = 0;

	NPChanged();
	massChanged();
	lengthChanged();
	angleChanged();
	timeIntervalChanged();

	pendulums.init();
	//updatePositions();
	pendulums.updateLinearStatus();
	showPeriod((T=0));
	clearData();
	calculateSystemCM();
	showPositions();
};
chkMovable.title = "Movable ceiling.";
chkMovable.nextSibling.title = chkMovable.title;
if(optPivot)
	optPivot.onchange = function(){
		if(optPivot.value === 'mouse')
			scene.activateRaycaster(pendulums);
		else
			scene.deActivateRaycaster();
		reset();
	}
chkMovable.onclick = reset;
//if(chkRKFour){
//	chkRKFour.title = 'Checked: 4th order Runge-Kutta algorighm.\nUnchecked: Euler method.';
//	chkRKFour.onclick = reset;
//}
chkRandom.title = 'Random initial positions.';
chkRandom.onclick = reset;
//if(chkOneD){
//	chkOneD.title = 'Checked: 3D calculations.';
//	chkOneD.onclick = reset;
//}
txtInitialAngle.onchange = reset;
txtBobMass.onchange = reset;
txtRodMass.onchange = reset;
txtCeilingMass.onchange = reset;
txtFineSteps.onchange = reset;
/*}}}*/
/***
!! The Recreation Function
***/
//{{{
let recreate = function(){
	let N = pendulums.length;
	let n;
	for(n=0; n<N; n++){
		scene.remove(pendulums[n]);
	}
	pendulums = new Pendulums(+txtNP.value);
	reset();
};
txtNP.onchange = reset;
txtRodLength.onchange = reset;
//}}}
/***
!! Gravitational force
***/
/*{{{*/
let lengthfactor = 0.1;
let vafactor = 0.5;
let fg = scene.g.clone().multiplyScalar(pendulums[0].mass);
// 代表重力的箭頭 The arrow to represent the gravitational force
let arrFg = arrow({
	axis: fg.clone().multiplyScalar(lengthfactor),
	color: 0xff00ff
});
/*}}}*/
/***
!! Initial angles, camera positions, simulation time steps, etc.
***/
/*{{{*/
// 設定單擺的初始狀態 Set the initial state of pendulum
let dt = +txtdT.value;
let simT = 0;
reset();
checkPlots();
// 移動攝影機到適當位置 Move the camera to a proper position
camera.position.y = ceiling.position.y - pendulums[0].string.getLength()*0.8;
camera.position.z = 8;
scene.activateRaycaster();
scene.createTrackballControl();
/*}}}*/
/***
!! update function (that will be called by tw3jsPlugin periodically)
<<<
這個 update 函數大約會以 40~60 次/秒的頻率執行(視電腦及瀏覽器的效能而定),在這裡我們將會
This update function is executed at roughly 40~60 times/sec (depending on the performance of computer and browser), in which we will
# 應用 [[龍格-庫塔(Runge-Kutta)演算法|https://zh.wikipedia.org/wiki/%E9%BE%99%E6%A0%BC%EF%BC%8D%E5%BA%93%E5%A1%94%E6%B3%95]](如果勾選 ~RK4,精確度較高)或是歐拉法(如果未勾選 ~RK4,精確度較低)來求解各運動方程<br>Apply the [[Runge-Katta algorithm|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]] (if ~RK4 is checked, higher precision) or the Euler method (if ~RK4 is unchecked, lower precision) to solve the equations of motion \[\vec a_i = {d^2\vec r_i \over dt^2} = \vec a_{i,c} + \vec a_{i,t} = {v_i^2 \over r_i}(-\hat r_i) + \vec\alpha_i \times \vec r_i,\] 其中 \(\vec a_i\) 是第 //i// 個擺的質心加速度,會隨著該擺的質心位置 \(\vec r_i\)(從該擺的轉軸點量起)、質心速度 \(\vec v_i\),以及角加速度 \(\vec \alpha_i\) 而變,細節請參考上方 {{{pendulum.calculateAcceleration()}}} 及 {{{pendulum.calculateAlpha()}}} 裡的說明。<br> where \(\vec a_i\) is the center of mass acceleration of the //i//^^th^^ pendulum, which changes with its center of mass position \(\vec r_i\) (measured from its pivot position), its center of mass velocity \(\vec v_i,\) and its angular acceleration \(\vec \alpha_i.\) See the explanations in {{{pendulum.calculateAcceleration()}}} and {{{pendulum.calculateAlpha()}}} for details.
# 更新畫面。Update the screen.
<<<
***/
/*{{{*/
	let x = [], v = [], a = [];
	let update = function(){
		//if (NPChanged() || massChanged() || lengthChanged()
		//	|| angleChanged() || timeIntervalChanged()){
		//	reset();
		//	return;
		//}

		if (paused()){
			updatePositions();
		}else{
			let n = txtFineSteps.value;
			let feval = rk42a //(!chkOneD || chkOneD.checked)
				//? ((!chkRKFour || chkRKFour.checked) ? rk42a : eulera)
				//: ((!chkRKFour || chkRKFour.checked) ? rk42va : eulerva);
			for(let i=0,dtn=dt/n;i<n;i++){
				//if(!chkOneD || chkOneD.checked){
					pendulums.getThetas(x);
					pendulums.getOmegas(v);
					pendulums.getAlphas(a);
				//}else{
				//	pendulums.rCMs(x);
				//	pendulums.vCMs(v);
				//	pendulums.aCMs(a);
				//}
				if(chkMovable.checked){
					let len = pendulums.length;
					x[len] = ceiling.position.x;
					v[len] = ceiling.velocity.x;
					a[len] = ceiling.acceleration.x;
				}

				feval(x,v,pendulums.calculateAccelerations,simT,dtn,a);
				updatePositions(x,v,a);

				simT += dtn;
			}

			recordData();
			showPositions();
			scene.TPF.updateLabel(labelTPF,1);
			scene.FPS.updateLabel(labelFPS,1);
			labelSimT.innerText = $tw.ve.round(simT,3);
		}
		checkPlots();
	};
/*}}}*/
!! Pendulum Control
@@color:red;Pendulums:@@ <html><input type="number" title="Number of pendulums." id="txtNP" min="1" max="30" step="1" value="1" style="width:40px"></html> / [ =chkRandom] Random
!! Pivot Control
Pivot: \(m\) (kg) <html><input type="number" title="Mass of pivot." id="txtPivotMass" min="0.01" max="1" step="0.001" value="0.1" style="width:55px"></html>
!! String Control
String: //L//~~0~~ (m) <html><input type="number" title="Length of rod." id="txtStringLength" min="0.1" max="10" step="0.01" value="0.13" style="width:50px"></html> / \(m\) (kg) <html><input type="number" title="Mass of string." id="txtStringMass" min="0.001" max="1" step="0.001" value="0.005" style="width:60px"></html> / \(r\) (m) <html><input type="number" title="Radius of string." id="txtStringRadius" min="0.0005" max="0.05" step="0.001" value="0.0005" style="width:55px"></html>
!! Rod Control
Rod: //L//~~0~~ (m) <html><input type="number" title="Length of rod." id="txtRodLength" min="0.1" max="10" step="0.01" value="0.13" style="width:50px"></html> / \(m\) (kg) <html><input type="number" title="Mass of rod." id="txtRodMass" min="0.001" max="1" step="0.001" value="0.005" style="width:60px"></html> / \(r\) (m) <html><input type="number" title="Radius of rod." id="txtRodRadius" min="0.0001" max="0.05" step="0.0005" value="0.0005" style="width:55px"></html>
!! Bob Control
Bob: \(m\) (kg) <html><input type="number" title="Mass of bob." id="txtBobMass" min="0.001" max="1" step="0.01" value="0.029" style="width:55px"></html> / \(r\) (m) <html><input type="number" title="Radius of bob." id="txtBobRadius" min="1e-2" max="1" step="0.01" value="0.025" style="width:50px"></html>
!! Initial Velocity
<<tw3DCommonPanel "Initial Speed">> / <<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">>
!! Period Label
<html>''T'' (s): <label id="labelPeriod" title="Period of oscillation." style="font-family:'Courier New'"></label></html>
/***
!! Definition Section
Definition for the Rod, Bob, etc.
!!! The Rod
***/
/***
!!! The Bob
***/
/*{{{*/
let Bob = function(param){
	return Bob.prototype.init.call(sphere(param),param);
};
Bob.prototype.init = function(param){
	let bob = this;
	bob.mass = param && param.mass || 0.5;
	return bob;
};
/*}}}*/
/***
!!! The Pendulum
***/
/*{{{*/
let Pendulum = function(param){
	let pendulum = group();
	scene.add(pendulum);
	pendulum.init = function(){
		pendulum.alpha.set(0,0,0);
		pendulum.omega.set(0,0,0);
		pendulum.velocity.set(0,0,0);
		pendulum.pivot.position.copy(pendulum.position);
		//pendulum.string.setDirection(vector(0,-1,0));
		pendulum.angle.copy(pendulum.initialAngle);
		pendulum.angle.value = pendulum.initialAngle.value;
		pendulum.setAngle();
		//pendulum.calculateLinearStatus();
		pendulum.lastT = simT;
		return pendulum;
	};

	pendulum.string= param.string || param.rod || new String({
		radius: 0.003,
		axis: vector(0,0,1),
		color: 0xff0000
	},'noadd');
	pendulum.add(pendulum.string);

	pendulum.bob = param.bob || new Bob({
		radius: 0.05,
		color: 0x00ff00,
		opacity: 0.3
	},'noadd');
	pendulum.add(pendulum.bob);

	pendulum.destroy = function(){
		scene.remove(pendulum);
		//scene.remove(pendulum.bob);
		//scene.remove(pendulum.string);
		scene.remove(pendulum.spherecm);
		scene.remove(pendulum.pivot);
		scene.remove(pendulum.vertical);
	}

	pendulum.torque = vector();
	pendulum.alpha = vector();
	pendulum.omega = vector();
	pendulum.angle = vector();
	pendulum.velocity = vector();
	pendulum.acceleration = vector();

	if(param.initialAngle){
		if(typeof param.initialAngle === 'number'){
			pendulum.initialAngle = vector(0,0,1);
			pendulum.initialAngle.value = param.initialAngle;
		}else{
			pendulum.initialAngle = param.initialAngle.clone();
			pendulum.initialAngle.value = pendulum.initialAngle.length();
			pendulum.initialAngle.normalize();
		}
	}else{
		pendulum.initialAngle = vector(0,0,1);
		pendulum.initialAngle.value = 0;
	}

	pendulum.setLength = function(L){
		pendulum.string= pendulum.string.setLength(L);
		pendulum.length = pendulum.string.getLength();
		pendulum.setAngle();
		pendulum.setPosition();
	};
	pendulum.isSimple = function(){
		return pendulum.string.mass === 0;
	};
/*}}}*/
/***
!!!! pendulum.calculateCM ()
>計算擺的質心位置。如果是單擺,則質心位置就是擺錘的位置,如果是實體擺,則質心位至按照下列公式計算:
>Calculate the center of mass position of this pendulum. If the pendulum is simple, the center of mass is that of the bob, otherwise calculate the center of mass using the following formula:
>\[\vec r_\text{CM} = {m_\text{bob}\vec r_\text{bob} + m_\text{rod} \vec r_\text{rod} \over m_\text{bob} + m_\text{rod}},\]
>其中 \(m_\text{bob}\) 與 \(m_\text{rod}\) 分別為擺錘與擺繩的質量,而 \(\vec r_\text{bob}\) 及 \(\vec r_\text{rod}\) 則各為其中心位置。
>where \(m_\text{bob}\) and \(m_\text{rod}\) are the masses, while \(\vec r_\text{bob}\) and \(\vec r_\text{rod}\) are the center positions of the bob and the rod, respectively.
***/
/*{{{*/
	pendulum.rcm = vector();
	// 代表質心的球 A sphere to represent the center of mass.
	pendulum.spherecm = sphere({
		radius: 0.01,
		opacity: 0.6
	});
	//pendulum.add(pendulum.spherecm);
	pendulum.calculateCM = function(){
		if(pendulum.isSimple())
			pendulum.rcm.copy(pendulum.bob.position);
		else
			pendulum.rcm.copy(pendulum.string.position)
				.multiplyScalar(pendulum.string.mass)
				.add(
					pendulum.bob.position.clone()
						.multiplyScalar(pendulum.bob.mass)
				).multiplyScalar(
					1/pendulum.mass
				);
		pendulum.rcm.add(pendulum.position);
		pendulum.spherecm.position.copy(pendulum.rcm);
		pendulum.spherecm.visible = chkCM.checked;
	};
/*}}}*/
/***
!!!! pendulum.calculateVertical()
>計算此擺的鉛直線通過點
***/
/*{{{*/
	pendulum.vertical = cylinder({
		radius: 0.003,
		axis: vector(0,-txtRodLength.value/2,0),
		color: 0xaaaaaa,
		opacity: 0.5
	});
	pendulum.calculateVertical = function(){
		pendulum.vertical.position.copy(pendulum.position);
		if(chkMovable.checked){
			let dx = (pendulum.rcm.x - pendulum.position.x)
				*pendulums.mass/(ceiling.mass+pendulums.mass);
			pendulum.vertical.position.x += dx;
			pendulum.vertical.position.y -=
				Math.abs(dx/Math.tan(pendulum.initialAngle.value));
		}
	};
/*}}}*/
/***
!!!! pendulum.setAngle (angle, done)
> 設定此擺與鉛直線之間的角度(弧度),並視情況將擺轉到該角度去(如果第二個引數 done 為 false 的話)
> Sets the angle (in radians) measured from the vertical line, and rotate the pendulum if necessary (if the 2nd argument done is false).
***/
/*{{{*/
	pendulum.setAngle = function(angle,done){
		if(angle){
			//pendulum.checkAngle(angle);
			pendulum.angle.copy(angle);
			if(angle.value===undefined){
				pendulum.angle.value = pendulum.angle.length();
				pendulum.angle.normalize();
			}else{
				pendulum.angle.value = angle.value;
			}
		}else{
			angle = pendulum.angle;
			//pendulum.checkAngle(angle);
		}

		pendulum.checkCycle();

		if(!done){
			// Rotate the pendulum to the desired angle.
			pendulum.string.setDirection(
				pendulum.string.rcm.set(0,-1,0)
					.applyAxisAngle(angle,angle.value)
			);
			pendulum.string.rcm.multiplyScalar(pendulum.length/2);
		}

		return pendulum;
	};
/*}}}*/
/***
!!!! pendulum.calculateTension ()
> 計算擺繩張力 \(\vec T\),根據【沿著擺繩方向(\(\hat r\))的合力做為擺的向心力】這個想法來進行:
> Calculate the tension \(\vec T\) in the rod, following the idea that //"the net force along the rod direction (\(\hat r\)) serves as the centripetal force for the pendulum"//:\[\begin{eqnarray*} & \vec T &+& (m\vec g \cdot \hat r)\hat r = m \vec a_c = m |\vec r_{CM}-\vec r_0| \omega^2 (-\hat r) \\ \to \quad & \boxed{\vec T} &=& -(m\vec g \cdot \hat r)\hat r - m_|\vec r_{CM}-\vec r_0|\omega^2 \hat r \\ \to \quad &&=& \boxed{-m(\vec g \cdot \hat r + |\vec r_{CM} - \vec r_0|\omega^2)\hat r},\end{eqnarray*}\] 其中 \(m\) 是擺的質量,\(\hat r\) 是沿著擺繩,從懸掛點指向擺錘方向的單位向量,\(\vec a_c\) 為擺的向心加速度,\(\vec r_{CM}\) 是擺的質心位置,\(\vec r_0\) 為轉軸通過的位置,可能為懸掛點(如果天花板為固定),或者是系統整體的質心(如果天花板可移動);而 \(\vec \omega\) 則為擺的角速度。
>where \(m\) is the mass of the bob, \(\hat r\) is the unit vector along the rod, pointing from the hanging point towards the bob, \(\vec a_c\) is the centripetal acceleration of the bob, \(\vec r_{CM}\) is the center of mass position of the pendulum, \(\vec r_0\) is the pivot position, which could be the hanging point (if the ceiling is fixed), or the system's center of mass position (if the ceiling is movable); while \(\vec \omega\) is the angular velocity of the pendulum.
>
>//注意:\(\vec T\) 的方向為向上,而 \(\vec g\) 與 \(\hat r\) 是向下。//
>//Note: The direction of \(\vec T\) is upward while that of \(\vec g\) and \(\hat r\) are downward.//
***/
//{{{
	pendulum.calculateTension = function(){
		let dr = pendulum.rcm.clone().sub(pendulum.pivotPosition());
		let ur = dr.clone().normalize();

		pendulum.string.tension.copy(ur).multiplyScalar(
			- fg.dot(ur)
			- pendulum.mass*dr.length()*pendulum.omega.lengthSq()
		);
		return pendulum;
	};
//}}}
/***
>End of pendulum definitions.
!!! A collection of pendulums
***/
/*{{{*/
let Pendulums = function(n){
	let pendulums = [];
	pendulums.create = function(n){
		if(n < 1) n = 1;
		let L = +txtRodLength.value;
		let len = pendulums.length;
		if(n > len){
			// n > length
			for(let i=pendulums.length; i<n; i++){
				pendulums[i] = new Pendulum({
					rod: new Rod({
						radius: 0.003,
						axis: vector(0,-txtRodLength.value,0),
						color: 0xff0000,
						length: L
					},'noadd'),
					bob: new Bob({
						radius: 0.05,
						color: 0x00ff00,
						opacity: 0.3
					},'noadd'),
					pos: vector()
				});
				//pendulums[i].string.name = 'rod'+i;
				//pendulums[i].bob.name = 'bob'+i;
				//pendulums[i].spherecm.name = 'cm'+i;
			}
		}else if(n < len){
			// n < length
			for(let i=len-1; i>=n; i--){
				pendulums[i].destroy();
			}
			pendulums.splice(n);
		}
	};
	if(typeof n !== 'number' || n <= 0)
		n = +txtNP.value;
	pendulums.create(n);

	let randomShift = function(max){
		return (max||1)*Math.random()*(Math.random()<0.5?-1:1);
	};

	pendulums.init = function(){
		let N = pendulums.length, n;
		let L = +txtRodLength.value;
		let a0 = txtInitialAngle.value*Math.PI/180;
		let dr = vector(0,0,0);
		let sep = [], width = 0;
		//let movable = chkMovable.checked;
		let pos = vector(), p;
		for(n=0; n<N; n++){
			p = pendulums[n];
			p.initialAngle.set(0,0,1);
			p.initialAngle.value = a0*(1+Math.random()/10)*(n%2?1:-1);
			p.init();
			//p.setAngle();
			p.setLength(L*(1+Math.random()/10));
			//p.setPosition();
			sep[n] = p.length*Math.sin(p.initialAngle.value)*3;
			width += sep[n];
		}
		// Adjust the ceiling width if necessary.
		if(ceiling.getWidth()<width)
			ceiling.setWidth(width);
		// Determine the maximum separation.
		let maxsep = sep[0];
		for(n=1; n<N; n++)
			if(maxsep < sep[n]) maxsep = sep[n];
		dr.set(maxsep,0,0);
		// Determine the position of the first pendulum.
		// In a 1D case, the first pendulum is the left-most one.
		// We will arrange the pendulums in a symmetrical way.
		let r0 = N === 1
			?	ceiling.position.clone()
			:	dr.clone().multiplyScalar(
					(N % 2
						? -(N-1)/2.0		// even number, half-half
						: -N/2)				// odd number, one at the center, others half-half
				).add(ceiling.position);
		// Now set the pendulum positions.
		for(n=0; n<N; n++){
			pos.copy(dr).multiplyScalar(n).add(r0);
			p = pendulums[n];
			p.setPosition(pos);
			p.calculateVertical();
		}
		return pendulums;
	};
	// 代表張力的箭頭 The arrow representing the tension
	pendulums.arrT = arrow({
		axis: pendulums[0].string.tension.clone(),
		color: 0x00ffff
	});
	//pendulums.arrT.name = 'arrT';

	pendulums.force = vector();
	// 代表合力的箭頭 The arrow representing the total force
	pendulums.arrF = arrow({
		axis: pendulums.force,
		color: 0xff0000
	});
	//pendulums.arrF.name = 'arrT';

	// 代表力矩的箭頭 The arrow representing the torque
	pendulums.arrTau = arrow({
		axis: pendulums[0].torque,
		color: 0xffff00
	});
	//pendulums.arrTau.name = 'arrT';

	// 代表速度的箭頭 The arrow representing the velocity
	pendulums.arrV = arrow({
		axis: pendulums[0].velocity,
		color: 0x00ff00
	});
	//pendulums.arrV.name = 'arrT';

	// 代表加速度的箭頭 The arrow representing the acceleration
	pendulums.arrA = arrow({
		axis: pendulums[0].acceleration,
		color: 0xffffff
	});
	//pendulums.arrA.name = 'arrT';
/*}}}*/
/***
!!!! pendulums.calculateCM ()
> 計算全部擺的質心。
> Calculate the center of mass for all the pendulums.
***/
/*{{{*/
	pendulums.rcm = vector();
	pendulums.calculateCM = function(){
		pendulums.rcm.set(0,0,0);
		let tmp = vector();
		for(let n=0,len=pendulums.length; n<len; n++){
			pendulums.rcm.add(tmp.copy(pendulums[n].rcm).multiplyScalar(pendulums[n].mass));
		}
		return pendulums.rcm.multiplyScalar(1/pendulums.mass);
	};
/*}}}*/
/***
!! Initialization Section
Initialize scene and variables.
!!!! The scene and axes.
***/
/*{{{*/
scene.axes({
	pos: vector(-1.7,0,0),
	length: 0.2
});
let Rcm = vector();
let sphereRcm = sphere({
	radius: 0.02,
	opacity: 0.6
});
/*}}}*/
/***
!! The ceiling
***/
/*{{{*/
let ceiling = box({
	size: 2,
	height: 0.002,
	color:0xbbbbbb
});
ceiling.mass = +txtCeilingMass.value;
ceiling.normal = vector(0,-1,0);
ceiling.velocity = vector();
ceiling.acceleration = vector();
ceiling.lastPosition = vector();
/*}}}*/
/***
!! The pendulums
***/
/*{{{*/
let pendulums = new Pendulums();
/*}}}*/
/***
!! Calculating the System's Center of Mass
***/
/*{{{*/
let calculateSystemCM = function(){
	// Calculate Rcm of ceiling and pendulums
	Rcm.copy(ceiling.position).multiplyScalar(ceiling.mass).add(
		pendulums.rcm.clone().multiplyScalar(pendulums.mass)
	).multiplyScalar(
		1/(ceiling.mass+pendulums.mass)
	);
	sphereRcm.position.copy(Rcm);
	sphereRcm.visible = chkCM.checked;
};
/*}}}*/
/***
!! Gravitational force
***/
/*{{{*/
let lengthfactor = 0.1;
let vafactor = 0.5;
let fg = scene.g.clone().multiplyScalar(pendulums[0].mass);
// 代表重力的箭頭 The arrow to represent the gravitational force
let arrFg = arrow({
	axis: fg.clone().multiplyScalar(lengthfactor),
	color: 0xff00ff
});
/*}}}*/
/***
!! Initial angles, camera positions, simulation time steps, etc.
***/
/*{{{*/
// 設定單擺的初始狀態 Set the initial state of pendulum
let dt = +txtdT.value;
let simT = 0;
reset();
checkPlots();
// 移動攝影機到適當位置 Move the camera to a proper position
camera.position.y = ceiling.position.y - pendulums[0].string.getLength()*0.8;
camera.position.z = 8;
scene.activateRaycaster();
scene.createTrackballControl();
/*}}}*/
//{{{
scene.checkStatus = () => {
	getTrailParam(pendulum.pivot);
	getTrailParam(pendulum.bob);
	pendulum.showPivotSpring(
		typeof chkPivotSpring !== 'undefined' ? chkPivotSpring.checked : false
	);
}
//}}}
//{{{
const calcA = (r,v,t,a) => {
	return pendulum.calculateAcceleration(r,v,a);
}
//}}}
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta = $tw.numeric.ODE.nextValue(
			__r,__v,calcA,t_cur,dt,__a,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	pendulum.setPosition(__r[0],__r[1]);

	pendulum.pivot.velocity.copy(__v[0]);
	pendulum.bob.velocity.copy(__v[1]);

	pendulum.pivot.acceleration.copy(__a[0]);
	pendulum.bob.acceleration.copy(__a[1]);
};
//}}}
/***
!! The pendulum
***/
//{{{
let pendulum = $tw.physics.Pendulum.create({
	pivot: {
		radius:0.01,
		opacity:0.2,
		mass: 0.001,
		make_trail: false,
		retain: __trail_len__,
		interval: __trail_interval__
	},
	rod: {
		axis:vector(0,0,-1),
		radius:+txtRodRadius.value,
		thickness:0.005,
		color:0xFED162,
		//opacity:0.3,
		//soft:true,
		coils:60
	},
	bob: {
		radius: +txtBobRadius.value,
		mass: +txtBobMass.value,
		make_trail: false,
		retain: __trail_len__,
		interval: __trail_interval__,
		opacity: 0.5
	}
});
scene.add(pendulum);
console.log($tw.physics.equivalentSpringConstant('wood',0.001));
pendulum.pivot.attachSprings(0.15,$tw.physics.equivalentSpringConstant('wood',0.001));
//}}}
//{{{
scene.init = () => {
	pendulum.pivot.setMass(+txtPivotMass.value);

	pendulum.string.setRadius(+txtRodRadius.value);
	pendulum.string.setLength(pendulum.string.L0=+txtRodLength.value);
	pendulum.string.theta0 = txtTheta0.value*Math.PI/180.0;
	pendulum.string.phi0 = txtPhi0.value*Math.PI/180.0;
	pendulum.string.mass = +txtRodMass.value;

	pendulum.bob.setRadius(+txtBobRadius.value);
	pendulum.bob.setMass(+txtBobMass.value);		// kg

	let r_pivot = vector(), r_bob = vector(), L = 0;
	let theta = Math.PI-pendulum.string.theta0;
	let sin_theta = Math.sin(theta);

	// Bob is displaced initially.
	r_pivot.set(0,0,0);
	L = pendulum.string.L0;
	r_bob.set(
		L*sin_theta*Math.cos(pendulum.string.phi0),
		L*sin_theta*Math.sin(pendulum.string.phi0),
		L*Math.cos(theta)
	).add(r_pivot);
	pendulum.setPosition(r_pivot,r_bob);

	pendulum.showForce(false);
}
//}}}
//{{{
scene.camera.position.multiplyScalar(0.1);
let __r = [], __v = [], __a = [];
scene.initialized = () => {
	__r[0] = pendulum.pivot.position.clone();
	__r[1] = pendulum.bob.position.clone();

	__v[0] = pendulum.pivot.velocity.clone();
	__v[1] = pendulum.bob.velocity.clone();

	__a[0] = pendulum.pivot.acceleration.clone();
	__a[1] = pendulum.bob.acceleration.clone();
console.log(pendulum.string.k);
}
//}}}
|Pendulum Sync Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<<tiddler "Pendulum Panel##Pivot Control">> / <<tiddler "Pendulum Panel##Period Label">>|<<tiddler "Pendulum Panel##Rod Control">>|
|<<tiddler "Pendulum Panel##Bob Control">>|<<tw3DCommonPanel "Initial Theta">> / <<tw3DCommonPanel "Initial Phi">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Pendulum Sync Init]] [[Pendulum Sync Code]]>>|
/***
!!!! Definition
***/
//{{{
let PendulumX = {
	create : function(param){
		let pendulum = $tw.physics.Pendulum.create(param);

		pendulum.bob = [
			pendulum.bob,
			pendulum.getComponent(
				param.bob || param.Bob,
				$tw.physics.Pendulum.Bob.create
			)
		];
		pendulum.add(pendulum.bob[1]);

		pendulum.initialized = function(){
			//pendulum.bob[1].mass = pendulum.bob[0].mass;
			//pendulum.bob[1].setRadius(pendulum.bob[0].getRadius());
			pendulum.bob[1].position.copy(pendulum.pivot.position).add(
				pendulum.string.getAxis().multiplyScalar(0.5)
			);
			pendulum.L0_bob = pendulum.string.L0 * 0.5;
			pendulum.L0_pivot = pendulum.string.L0 - pendulum.L0_bob;

			return pendulum;
		};
//}}}
//{{{
		let pre__fill_in_positions = pendulum.__fill_in_positions;
		pendulum.__fill_in_positions = function(r){
			pre__fill_in_positions.call(this,r);
			if(pendulum.bob.length && !r[2])
				r[2] = pendulum.bob[1].position.clone();
			return pendulum;
		};
		let pre__fill_in_velocities = pendulum.__fill_in_velocities;
		pendulum.__fill_in_velocities = function(v){
			pre__fill_in_velocities.call(this,v);
			if(pendulum.bob.length && !v[2])
				v[2] = pendulum.bob[1].velocity.clone();
			return pendulum;
		};
//}}}
//{{{
		let pre__adjust_rod_dir = pendulum.__adjust_rod_dir;
		pendulum.__adjust_rod_dir = function(r_pivot,r_bob,r_bob_free){
			if(!r_bob_free){
				pre__adjust_rod_dir.call(this,r_pivot,r_bob);
			}else{
				pendulum.bob[1].setPosition(r_bob_free);
				pendulum.string.setAxis(tmpV.copy(r_bob_free).sub(r_pivot));
				pendulum.L0_pivot = tmpV.length();
				pendulum.L0_bob = tmpV.copy(r_bob).sub(r_bob_free).length();
			}
			return pendulum;
		};
//}}}
//{{{
		pendulum.calculateBobTension = function(r,T){
			if(!T) T = vector();
			if(!pendulum.Tbob) pendulum.Tbob = vector();
			// The tension between the two bobs
			T.copy(r[1]).sub(r[2]);
			T.L = T.length();
			pendulum.string.tension.copy
			pendulum.Tbob.copy(
				T.normalize().multiplyScalar(
					-pendulum.string.k*(T.L-pendulum.L0_bob)//*(pendulum.string.L0/T.L)
				)
			);
			return T;
		};
//}}}
//{{{
		pendulum.calculatePivotTension = function(r,T){
			if(!T) T = vector();
			if(!pendulum.Tpivot) pendulum.Tpivot = vector();
			T.copy(r[0]).sub(r[2]);
			T.L = T.length();
			pendulum.Tpivot.copy(
				T.normalize().multiplyScalar(
					-pendulum.string.k*(T.L-pendulum.L0_pivot)//*(pendulum.string.L0/T.L)
				)
			);
			return T;
		};
//}}}
//{{{
		let preCalculateForce = pendulum.calculateForce;
		pendulum.calculateForce = function(r,v,f,drag){
			f = preCalculateForce.call(this,r,v,f,drag);
			if(pendulum.bob.length && pendulum.bob.length > 1){
				f[2].copy(pendulum.bob[1].Fg).sub(pendulum.Tpivot).sub(pendulum.Tbob);
			}
			return f;
		};

		let preCalculateAcceleration = pendulum.calculateAcceleration;
		pendulum.calculateAcceleration = function(r,v,a,drag){
			a = preCalculateAcceleration.call(this,r,v,a,drag);
			if(pendulum.bob.length){
				for(let n=1,N=pendulum.bob.length; n<N; n++){
					a[n+1].multiplyScalar(1/pendulum.bob[n].mass);
				}
			}
			return a;
		};

		return pendulum;
	}
};
//}}}
在 radix-4 演算法則中,需要注意轉換的正負號定義(\(e^{+i2\pi/N}\) 或是 \(e^{-i2\pi/N}\)),由於公式中有乘以 \(e^{\pm i2\pi k/(N/4)} = \mp j, (j = \sqrt{-1})\),不同的正負號選擇會影響 \(j\) 的正負號,這個選擇對於轉換結果並沒有影響,但不同參考文獻使用不同的定義,因此公式上在虛部會有正負的差異,寫程式時必須確認自己的正負號選擇跟公式是一致的。

在 radix-2 方法中則沒有這個虛部正負號的問題,因為是乘以 \(e^{\pm i2\pi k/(N/2)}= \pm 1\)。
!! The 0 frequency component
* The 0 frequency component is simply the sum of all the data points, which has to be real.
** If, however, we make use of the complex FFT routine for the real value data, this component is a complex number.
** The actual result of this component shall be the sum of the real and imaginary part of that from the complex transform.
** If we need to do inverse transform later, then the imaginary part has to be kept somewhere or it will be lost.

|Ring Oiler Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene>>|
!! Axis of rotation \(\hat u\) (normalized)
Given a unit vector \(\hat u\) as the axis of rotation, we are verifying some of the algebraic properties about it.
!!! Definition of cross product matrix \([\hat u]_\times\)
\[ [\hat u]_\times \equiv \begin{pmatrix}0 & -u_z & u_y \\ \\ u_z & 0 & -u_x \\ \\ -u_y & u_x & 0\end{pmatrix}\] It is called the //cross product matrix// because \([\hat u]_\times \vec r = \hat u \times \vec r\), we can see that directly: \[ [\hat u]_\times \vec r = \begin{pmatrix}0 & -u_z & u_y \\ \\ u_z & 0 & -u_x \\ \\ -u_y & u_x & 0\end{pmatrix} \begin{pmatrix}r_x \\ \\ r_y \\ \\ r_z\end{pmatrix} = \begin{pmatrix}-u_zr_y+u_yr_z \\ \\ u_zr_x-u_xr_z \\ \\ -u_yr_x+u_xr_y\end{pmatrix} = \hat u \times \vec r\]
!!! The cross product matrix squared
\[ [\hat u]^2_\times = \begin{pmatrix}0 & -u_z & u_y \\ \\ u_z & 0 & -u_x \\ \\ -u_y & u_x & 0\end{pmatrix} \begin{pmatrix}0 & -u_z & u_y \\ \\ u_z & 0 & -u_x \\ \\ -u_y & u_x & 0\end{pmatrix} = \begin{pmatrix}-u_z^2-u_y^2 & u_yu_x & u_zu_x \\ \\ u_xu_y & -u_z^2-u_x^2 & u_zu_y \\ \\ u_xu_z & u_yu_z & -u_y^2-u_x^2\end{pmatrix}\] \[ \hat u \hat u^T = \begin{pmatrix}u_x^2 & u_xu_y & u_xu_z \\ \\ u_yu_x & u_y^2 & u_yu_z \\ \\ u_zu_x & u_zu_y & u_z^2\end{pmatrix}\] \[ \to \hat u\hat u^T - \hat I = [\hat u]_\times^2\]
!!! The cross product matrix cubed
\begin{aligned}[\hat u]_\times^3 = (\hat u \hat u^T - \hat I)[\hat u]_\times &= \begin{pmatrix}-u_z^2-u_y^2 & u_yu_x & u_zu_x \\ \\ u_xu_y & -u_z^2-u_x^2 & u_zu_y \\ \\ u_xu_z & u_yu_z & -u_y^2-u_x^2\end{pmatrix}\begin{pmatrix}0 & -u_z & u_y \\ \\ u_z & 0 & -u_x \\ \\ -u_y & u_x & 0\end{pmatrix} \\ \\ &= \begin{pmatrix}u_yu_xu_z-u_zu_xu_y & (u_z^2+u_y^2)u_z+u_zu_x^2 & -(u_z^2+u_y^2)u_y-u_yu_x^2 \\ \\ -(u_z^2+u_x^2)u_z-u_zu_y^2 & -u_xu_yu_z+u_zu_yu_x & u_xu_y^2+(u_z^2+u_x^2)u_x \\ \\ u_yu_z^2+(u_y^2+u_x^2)u_y & -u_xu_z^2-(u_y^2+u_x^2)u_x & u_xu_zu_y-u_yu_zu_x\end{pmatrix} \\ \\ &= \begin{pmatrix}0 & u_z & -u_y \\ \\ -u_z & 0 & u_x \\ \\ u_y & -u_x & 0\end{pmatrix} = -[\hat u]_\times\end{aligned}
!!! The parallel component matrix \(\hat u \hat u^T\)
The matrix \(\hat u \hat u^T\) gives the component of a vector \(\vec r\) that is __parallel__ to \(\hat u\). \begin{aligned}\hat u \hat u^T \vec r &= \begin{pmatrix}u_x^2 & u_xu_y & u_xu_z \\ \\ u_yu_x & u_y^2 & u_yu_z \\ \\ u_zu_x & u_zu_y & u_z^2\end{pmatrix} \begin{pmatrix}r_x \\ \\ r_y \\ \\ r_z\end{pmatrix} \\ \\ &= \begin{pmatrix}ux^2r_x+u_xu_yr_y+u_xu_zr_z \\ \\ u_yu_xr_x+u_y^2r_y+u_yu_zr_z \\ \\ u_zu_xr_x+u_zu_yr_y+u_z^2r_z\end{pmatrix} \\ \\ &= \begin{pmatrix}ux(u_xr_x+u_yr_y+u_zr_z) \\ \\ u_y(u_xr_x+u_yr_y+u_zr_z) \\ \\ u_z(u_xr_x+u_yr_y+u_zr_z)\end{pmatrix} = (\hat u \cdot \vec r) \hat u = \vec r_\parallel\end{aligned}
!!! The cross product matrix squared gives the opposite of the perpendicular component of a vector \(\vec r\)
The cross product matrix squared, \([\hat u]_\times^2 = \hat u \hat u^T - \hat I\), gives the //opposite// of the component of a vector \(\vec r\) that is __perpendicular__ to \(\hat u\). We can see that immediately from the above result, or we can carry it out directly: \begin{aligned}(\hat u\hat u^T - I) \vec r &= \begin{pmatrix}-u_z^2-u_y^2 & u_yu_x & u_zu_x \\ \\ u_xu_y & -u_z^2-u_x^2 & u_zu_y \\ \\ u_xu_z & u_yu_z & -u_y^2-u_x^2\end{pmatrix} \begin{pmatrix}r_x \\ \\ r_y \\ \\ r_z\end{pmatrix} \\ \\ &= \begin{pmatrix}-r_x(u_z^2+u_y^2) + r_y(u_yu_x) + rz(u_zu_x) \\ \\ r_x(u_xu_y) -r_y(u_z^2+u_x^2) + r_z(u_zu_y) \\ \\ r_x(u_xu_z) + r_y(u_yu_z)-r_z(u_y^2+u_x^2)\end{pmatrix} \\ \\ &= \begin{pmatrix}-r_x(u_x^2+u_y^2+u_z^2) + (r_xu_x)u_x+(r_yu_y)u_x+(r_zu_z)u_x \\ \\ -r_y(u_x^2+u_y^2+u_z^2)+(r_xu_x)u_y+(r_yu_y)u_y+(r_zu_z)u_y \\ \\ -r_z(u_x^2+u_y^2+u_z^2)+(r_xu_x)u_z+(r_yu_y)u_z+(r_zu_z)u_z\end{pmatrix} \\ \\ &= \begin{pmatrix}-r_x(1 - (r_xu_x+r_yu_y+r_zu_z)u_x) \\ \\ -r_y(1 - (r_xu_x+r_yu_y+r_zu_z)u_y) \\ \\ -r_z(1 - (r_xu_x+r_yu_y+r_zu_z)u_z)\end{pmatrix}\end{aligned} \[\vec r_\parallel = (\vec r \cdot \hat u)\hat u = \begin{pmatrix}(r_xu_x+r_yu_y+r_zu_z)u_x \\ \\ (r_xu_x+r_yu_y+r_zu_z)u_y \\ \\ (r_xu_x+r_yu_y+r_zu_z)u_z\end{pmatrix} \qquad \qquad \vec r\perp = \vec r - \vec r_\parallel = \begin{pmatrix}r_x(1-(r_xu_x+r_yu_y+r_zu_z)u_x) \\ \\ r_y(1-(r_xu_x+r_yu_y+r_zu_z)u_y) \\ \\ r_z(1-(r_xu_x+r_yu_y+r_zu_z)u_z)\end{pmatrix}\] \[\to (\hat u\hat u^T - I) \vec r = -\vec r_\perp\]
/***
''These codes are to solve @@color:red;1^^st^^ order differential equations@@ using the 4^^th^^ order ~Runge-Kutta method, for the following cases:''
# single particle 1D motion;
# single particle 3D motion;
# many particles 1D motion;
# many particles 3D motion.
***/
/***
!! Single Particle 1D Python
***/
/*{{{*/
def RK4(r,V,t,dt,v_now = None):
	# Returns the next position or function value after time dt,
	# using the 4th order Runge-Kutta algorithms.
	#		r: current position or function value
	#		V: Function v(r,t) shall return the velocity or first derivative at
	#			position r and time t.
	#		t: current time
	#		dt: time step
	# 		v_now: current velocity or first derivative (optional).

	r1 = r
	v1 = v_now
	if v1 is None: v1 = V(r1,t)

	v2 = V(r+dt/2.0*v1,t+dt2)

	v3 = V(r+dt/2.0*v2,t+dt3)

	v4 = V(r+dt*v3,t+dt)

	r += (v1+(v2+v3)*2.0+v4)*dt/6.0

	v_now = V(r,t+dt)
	return (r,v_now)
/*}}}*/
/***
!! Single Particle 3D Python
***/
/*{{{*/
def RK4Vector(r,V,t,dt,v_now = None):
	# Returns the next position or function value after time dt,
	# using the 4th order Runge-Kutta algorithms.
	#		r: current position or function value
	#		V: Function v(r,t) shall return the velocity or first derivative at
	#			position r and time t.
	#		t: current time
	#		dt: time step
	# 		v_now: current velocity or first derivative (optional).

	// Python knows what to do with vectors.
	return RK4(r,V,t,dt,v_now)
/*}}}*/
/***
!! Many Particles 1D Python
***/
/*{{{*/
def RK4ListVector(r,v,A,t,dt,a_now = None):
	# Returns into the original arrays the next r's and v's, after time dt
	# has passed, using the 4th order Runge-Kutta algorithms.
	#		r: current positions, must be a vector array
	#		v: current velocities, must be a vector array
	#		A: function A(r,v,t) shall return the acceleration at position
	#			r, velocity v, and time t.
	#		t: current time
	#		dt: time step
	#		a_now: current acceleration (optional), must be a vector array
	#			if given.

	N = len(r)
	#rangeN = range(N)

	r1 = r
	v1 = v
	a1 = a_now
	if a1 is None: a1 = A(r1,v1,t)

	rt = [None]*N

	r2 = rt
	v2 = [None]*N
	dt2 = dt*0.5
	for n in xrange(N):
		r2[n] = r[n] + dt2*v1[n]
		v2[n] = v[n] + dt2*a1[n]
	a2 = A(r2,v2,t+dt2)

	r3 = rt
	v3 = [None]*N
	for n in xrange(N):
		r3[n] = r[n] + dt2*v2[n]
		v3[n] = v[n] + dt2*a2[n]
	a3 = A(r3,v3,t+dt3)

	r4 = rt
	v4 = [None]*N
	for n in xrange(N):
		r4[n] = r[n] + dt*v3[n]
		v4[n] = v[n] + dt*a3[n]
	a4 = A(r4,v4,t+dt)

	dt /= 6.0
	for n in xrange(N):
		r[n] += (v1[n]+(v2[n]+v3[n])*2.0+v4[n])*dt
		v[n] += (a1[n]+(a2[n]+a3[n])*2.0+a4[n])*dt
	a_now = A(r,v,t+dt)

	return (r,v,a_now)
/*}}}*/
/***
!! Many Particles 3D Python
***/
//{{{
	def rk4va(y, f, t, dt):
		// Returns the next y after time dt has passed, using the 4th
		// order Runge-Kutta algorithms.
		//		y: current value for all the particles, must be a vector array.
		//		f: function f(y,t) shall return the derivatives of y's at time t,
		//				both the argument y and the return value of this function
		//				must be vector arrays.
		//		t: current time
		//		dt: time step

		// Python knows what to do with vectors.
		return rk4a(y, f, t, dt)
//}}}
/***
!! nextValue(r,v,a,t,dt,a_now)
***/
//{{{
def nextValue(r,v,a,t,dt,a_now):
	typer = type(r)
	if typer is list:
		typer0 = type(r[0])
		if typer0 is tuple:
			return RK4ListTuple(r,v,a,t,dt,a_now)
		elif typer0 is numpy.ndarray:
			return RK4ListArray(r,v,a,t,dt,a_now)
		elif typer0 is list:
			return RK4ListArray(r,v,a,t,dt,a_now)
		else:
			return RK4ListVector(r,v,a,t,dt,a_now)
	elif typer is numpy.ndarray:
		return RK4ListArray(r,v,a,t,dt,a_now)
	else:
		return RK4(r,v,a,t,dt,a_now)
//}}}
/***
''These codes are to solve @@color:red;2^^nd^^ order differential equations@@ using the 4^^th^^ order ~Runge-Kutta method, for the following cases:''
# single particle 1D/3D motion;
# many particles 3D/1D motion.
!!! The Idea
The idea is shown in the figure below, @@which is captured from [[this video|https://youtu.be/smfX0Jt_f0I?t=325]]@@.
[img(60%,)[image/teaching/RK4 for 2ndODE.JPG]]
***/
/***
!! Single Particle 3D/1D Python
***/
/*{{{*/
	def rk42(r, v, a, t, dt, a_cur):
		# Returns a tuple containing the next r, next v, and the
		# next a, using the 4th order Runge-Kutta algorithm.
		#		r: current position/distance
		#		v: current velocity/speed
		#		a: function a(r,v,t) shall return the acceleration at
		#			position r, velocity v, and time t
		#		t: current time
		#		dt: time step
		#		a_cur: (optional) current acceleration

		r1 = r					# current postion
		v1 = v					# current velocity
		a1 = a_cur				# current acceleration
		if a1 == None: a1 = a(r1,v1,t,dt)

		dt2 = dt*0.5			# first half step, using v1, a1
		r2 = r+dt2*v1			# position at half step
		v2 = v+dt2*a1			# velocity at half step
		a2 = a(r2,v2,t+dt2)		# acceleration at half step

		r3 = r+dt2*v2			# position at half step
		v3 = v+dt2*a2			# velocity at half step
		a3 = a(r3,v3,t+dt2)		# acceleration at half step

								# using v3, a3 to calculate the next step
		r4 = r+dt*v3			# position at next step
		v4 = v+dt*a3			# velocity at next step
		a4 = a(r4,v4,t+dt)		# acceleration at the next step

		# use a "some kind of average" of a1, a2, a3 and a4 to
		# calculate the next v, and that of v1, v2, v3 and v4 to
		# calculate the next r.
		r += (v1+2*v2+2*v3+v4)/6*dt
		v += (a1+2*a2+2*a3+a4)/6*dt
		a_cur = a(r,v,t+dt)

		# returns
		#		the next position
		#		the next velocity
		#		the next acceleration
		return (r,v,a_cur)
/*}}}*/
/***
!! Many Particles 3D/1D Python
***/
/*{{{*/
	def rk42a(r, v, a, t, dt, a_cur):
		// Returns into the original arrays the next r's and v's at time t + dt,
		// using the 4th order Runge-Kutta algorithms.
		//		r: current positions/distances, must be an array of vectors/scalars
		//		v: current velocities/speeds, must be an array of vectors/scalars
		//		a: function a(r,v,t) shall return the accelerations at positions r,
		//			velocities v, and time t, the arguments r, v and the return value
		//			of this function must all be arrays of scalars/vectors
		//		t: current time
		//		dt: time step
		//		a_cur: (optional) current accelerations

		n=0
		rangey = range(len(y))

		y1 = y
		yp1 = yp
		k1 = a_cur
		if k1 == None: k1 = f(y1,yp1,t)		# current acceleration

		dt2 = dt * 0.5
		y2 = [0 for n in rangey]
		yp2 = [0 for n in rangey]
		for n in rangey:
			y2[n] = y[n] + dt2*yp1[n]
			yp2[n] = yp[n] + dt2*k1[n]
		k2 = f(y2,yp2,t+dt2)				# acceleration at half step

		y3 = [0 for n in rangey]
		yp3 = [0 for n in rangey]
		for n in rangey:
			y3[n] = y[n] + dt2*yp2[n]
			yp3[n] = yp[n] + dt2*k2[n]
		k3 = f(y3,yp3,t+dt2)

		y4 = [0 for n in rangey]
		yp4 = [0 for n in rangey]
		for n in rangey:
			y4[n] = y[n] + dt*yp3[n]
			yp4[n] = yp[n] + dt*k3[n]
		k4 = f(y4,yp4,t+dt)

		for n in rangey:
			y[n] = y[n] + (yp1[n]+(yp2[n]+yp3[n])*2+yp4[n])*dt/6
			yp[n] = yp[n] + (k1[n]+(k2[n]+k3[n])*2+k4[n])*dt/6
/*}}}*/
[
{
	"time": 0.05199999999999991,
	"position": [
		{
			"x": 0.000004198046320096357,
			"y": 0.0011084423116865657,
			"z": -1.9797592682761537e-7
		},
		{
			"x": 0.06038800742794546,
			"y": 0.06040338054853219,
			"z": -0.09868134577199156
		}
	],
	"velocity": [
		{
			"x": 0.0003310828472081821,
			"y": 0.035806583857671576,
			"z": -0.00017054798722981884
		},
		{
			"x": -0.1820518273500317,
			"y": -0.18085870096279635,
			"z": -0.24212749411770906
		}
	],
	"acceleration": [
		{
			"x": 0.5002139886826744,
			"y": 0.05372703754613406,
			"z": -1.1783309230426717
		},
		{
			"x": -3.3521725266147078,
			"y": -3.291674987805836,
			"z": -4.30899464712137
		}
	]
}
]
 -- 遊戲學習、學習遊戲
模擬教室
[[twDataCSS]]
/*{{{*/
.viewer th, .viewer thead td, .options th, .options thead td {
	background-color: #c9c9c9;
	color: black;
}

table.noborder, .noborder tr, .noborder th, .noborder td, .noborder thead td{
	border:0;
}

#displayArea {position:absolute; left:-12.5em; width:80%;}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0 1em 1em; left:0px; top:0px;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0; margin:0.4em 0 0.2em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0 0; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0;}
.wizardFooter .status {padding:0 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0; font-size:.9em;}
.editorFooter .button {padding-top:0px; padding-bottom:0px;}

.fieldsetFix {border:0; padding:0; margin:1px 0px;}

.sparkline {line-height:1em;}
.sparktick {outline:0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin-left:3em; padding:1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/*{{{*/
@media print {
.header, .siteTitle, .siteSubtitle, #titleArea, .tagged, #mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none !important;}
.oddRow, evenRow {
	background-color: #fff;
}
#displayArea {margin: 0; width: 100%;}
#tiddlerDisplay {margin: 0 0 0 13em; padding: 0; width: 120%;}
noscript {display:none;} /* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
}
/*}}}*/
chkHOTEnabled: true
chktw3DEnabled: true
chktwDataEnabled: true
chktwLaTexEnabled: true
chktwLaTexIncludeDollarSignsDisplay: false
chktwLaTexIncludeDollarSignsInline: false
chktwLaTexMathJaxAutoNumber: true
chktwLaTexPluginEnabled: true
chktwNumericDebug: false
chktwNumericEnabled: true
chktwNumericForceRadix2: true
chktwPhysicsEnabled: false
chktwVEnabled: true
chktwVPMAsync: false
chktwVPMEnabled: true
chktwVPMUseParallelJS: false
chktwVPMUseTransferables: false
chktwmathAutoNumber: false
chktwmathEnabled: true
chktwmathMathJaxAutoNumber: true
chktwmathUseMathJax: true
chktwmathUsekamath: false
chktwveCoreClickAway: true
chktwveCoreConfirmToDelete: true
chktwveCoreEditWrappers: true
chktwveCoreEnabled: true
chktwveCoreManualSave: true
chktwveCoreManualUpload: true
chktwveCorePreview: true
chktwveCoreShowFocus: true
chktwveExtraCountText: true
chktwveExtraCyclicNavi: false
chktwveExtraCyclickNavi: false
chktwveExtraIncludeSubs: true
chktwveExtraInline: true
chktwveExtraLocateChar: false
chktwveExtraMathAutoNumber: true
chktwveExtraNoClick: false
chktwveExtracyclicNavi: false
chktwveNumericDebug: false
chktwveNumericEnabled: true
chktwveNumericForceRadix2: false
chktwvePluginEnabled: true
chktwveTableEditAll: false
chktwveTableEnabled: true
chktwveTableIncludeCSS: false
chktwveTableLargeEnabled: true
chktwveTableMultiLine: false
chktwveTableTranspose: true
chktwveTcalcAllTables: false
chktwveTcalcAsync: false
chktwveTcalcColorNegativeNumbers: false
chktwveTcalcDebugMode: false
chktwveTcalcEnabled: true
chktwveTcalcInTextCalc: false
chktwveTcalcThousandSeparated: false
txttwveCoreMinEditWidth: 6
txttwveCorePreviewCaret: %7C
txttwveCorePreviewHeight: 15
txttwveExtraCountHow: word
txttwveExtraPreviewOpacity: 1.00
txttwveTableFixCols: 0
txttwveTableFixRows: 0
txttwveTableMaxHeight: 70000px
txttwveTableMaxWidth: 100%25
txttwveTableMinCellWidth: 2
txttwveTcalcDecimalMark: .
txttwveTcalcNegativeNumberColor: red
txttwveTcalcThousandSeparator: %2C
txttwveTcalcWorkFlow: TopDownLeftRight
/***
!! Camera position and settings
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(0.5);
chkGravity.checked = chkGravity.disabled = true;
chkAutoCPF.checked = true;
// Size and mass of tennis balls obtained from https://en.wikipedia.org/wiki/Tennis_ball
txtSphereRadius.value = 0.034;
txtSphereMass.value = 0.06;
txtOpacity.value = 0.5;
//}}}
/***
!! Tennis balls
***/
//{{{
let ball = [];
for(let n=0,N=3; n<N; n++){
	ball[n] = $tw.threeD.physicalObject(
		sphere({
			radius: +txtSphereRadius.value,
			opacity: +txtOpacity.value
		}),{
			pos: vector(+txtSphereRadius.value*n,0,0),
			mass: +txtSphereMass.value
		}
	);
}
//}}}
|Tennis Ball Tower Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">> / <<tw3DCommonPanel "Contact Model">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Elastic Properties">> / <<tw3DCommonPanel "Friction">>|Ball: <<tw3DCommonPanel "Sphere Properties">> <<tw3DCommonPanel "Opacity Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Tennis Ball Tower Creation]] [[Tennis Ball Tower Initialization]] [[Tennis Ball Tower Iteration]]>>|
/***
!! Camera position and settings
***/
//{{{
scene.textBookView();
scene.camera.position.multiplyScalar(1.2);
chkGravity.checked = chkGravity.disabled = true;
chkAutoCPF.checked = true;
chkRandom.checked = true;
chkVelocity.checked = false;
txtCylinderRadius.value = 0.009*10;
txtCylinderLength.value = 0.008*10;
txtCylinderMass.value = 0.009*100*10;
txtRPM.value = 1200;
txtTolerance.value = '1e-1';
//}}}
/***
!! Creation and Definitions
<<<
Creation of a floor and a cylindrical dice.
<<<
!!!! Floor
***/
//{{{
let ceiling = $tw.threeD.physicalObject(box({
		width: 5,				// 5m
		height: 5,				// 5m
		depth: 0.15,			// 15cm
		opacity: 0.3
	})),
	floor = $tw.threeD.physicalObject(box({
		width: 5,
		height: 5,
		depth: 0.15,
		opacity: 0.3
	})),
	right_wall = $tw.threeD.physicalObject(box({
		width: 0.15,
		height: 5,
		depth: 3,
		opacity: 0.3
	})),
	left_wall = $tw.threeD.physicalObject(box({
		width: 0.15,
		height: 5,
		depth: 3,
		opacity: 0.3
	})),
	front_wall = $tw.threeD.physicalObject(box({
		width: 5,
		height: 0.15,
		depth: 3,
		opacity: 0.3
	})),
	back_wall = $tw.threeD.physicalObject(box({
		width: 5,
		height: 0.15,
		depth: 3,
		opacity: 0.3
	})),
	walls = [
		ceiling,
		right_wall,
		left_wall,
		front_wall,
		back_wall,
		floor
	];
//}}}
/***
!!!! Cylindrical dice
***/
//{{{
let dice = $tw.threeD.physicalObject(cylinder({
	radius: +txtCylinderRadius.value,
	length: +txtCylinderLength.value,
	opacity: 0.5,
	wireframe: true,
	make_trail: false
}));
attachMotionIndicators(dice,'12pt');
showCornerIndicators(dice);
dice.pivot = sphere({
	radius: +txtCylinderRadius.value/10,
	color: 0xff00ff,
	opacity: 0.5
});
dice.ptr = sphere({
	radius: +txtCylinderRadius.value/10,
	color: 0xff00ff,
	opacity: 0.5
});
dice.fartip = sphere({
	radius: +txtCylinderRadius.value/10,
	color: 0xff00ff,
	make_trail: false,
	opacity: 0.5
});
dice.dr = vector();
dice.rt = vector();
dice.rpt = vector();
dice.rott = new $tw.threeD.THREE.Euler();
dice.dqt = new $tw.threeD.THREE.Quaternion();
dice.arrdr = arrow({
	color: 0x888888,
	visible: false
});
dice.calculatePositions = angle => {
	let h2 = dice.getHeight()/2;
	dice.dqt.setFromEuler(dice.rott.setFromVector3(
		dice.dr.copy(angle).sub(dice.angular_position)
			.applyQuaternion(dice.quaternion)
	));
	dice.rt.copy(dice.position).applyQuaternion(dice.dqt);

	dice.ptr.position.copy(dice.rt);
	dice.rpt.copy(dice.pivot.position).applyQuaternion(dice.dqt);
	dice.pivot.position.copy(dice.rpt);
	return dice;
};
dice.updatePivot = () => {
	dice.pivot.position.set(0,0,-dice.getHeight()/2).applyQuaternion(dice.quaternion).add(dice.position);
	dice.arrdr.position.copy(dice.pivot.position);
	dice.arrdr.setAxis(dice.dr.copy(dice.position).sub(dice.pivot.position));
	return dice;
};
dice.calculateTorque = (r,rp) => {
	dice.torque.copy(r).sub(rp).cross(dice.force);
	//dice.torque.set(0,0,0);
	return dice;
};
dice.coord = CartesianCoordinateSystem().showComponents(false).setUnitLength(0.2).show(false);
dice.coord.setAxisLabel(0,'\\(\\hat x^\\prime\\)')
	.setAxisLabel(1,'\\(\\hat y^\\prime\\)').setAxisLabel(2,'\\(\\hat z^\\prime\\)');
//}}}
/***
!!!! Data Plot
***/
//{{{
dataPlot[0].setYTitle(
	(config.browser.isIE ? 'Theta~~p~~ (&deg;)' : '\\(\\theta_p\\ (^\\circ)\\)')
).setTitle('Theta vs Time');
dataPlot[1].setYTitle(
	(config.browser.isIE ? 'v (m/s)' : '\\(v (\\text{m/s})\\)')
).setTitle('Speed vs Time');

labelPlot[0].innerHTML = '\\(\\theta_p\\) (&deg;):';
labelPlot[1].innerHTML = '\\(v (\\text{m/s})\\):';

activateDAQChannels(2);
attachDAQBuffer(0,0);
attachDAQBuffer(1,1);
//}}}
/***
!!!! scene.init
***/
//{{{
const f_motion = 0.01;
scene.init = () => {
	ceiling.position.z = 3;
	left_wall.position.x = -(right_wall.position.x = ceiling.getWidth()/2);
	back_wall.position.y = -(front_wall.position.y = ceiling.getHeight()/2);

	right_wall.position.z = left_wall.position.z =
		front_wall.position.z = back_wall.position.z = ceiling.position.z/2;

	dice.coord.resetRotation();
	dice.setMass(+txtCylinderMass.value).setRadius(+txtCylinderRadius.value).setLength(+txtCylinderLength.value);
	dice.pivot.setRadius(+txtCylinderRadius.value/10);
	dice.force.copy(scene.g).multiplyScalar(dice.mass);

	dice.YoungsModulus = +txtYoungsModulus.value;
	dice.PoissonsRatio = +txtPoissonsRatio.value;

	if(chkVelocity.checked)
		txtInitialVelocity.value = [
			'(',$tw.ve.round(Math.random(),3),',',
				$tw.ve.round(Math.random(),3),',',
				$tw.ve.round(Math.random(),3),')'
		].join('');
	else
		txtInitialVelocity.value = '(0,0,0)';
	dice.velocity.copy($tw.threeD.evalVector(txtInitialVelocity.value));

	if(chkRandom.checked)
		txtInitialAngularVelocity.value = [
			'(0,',
			$tw.ve.round(+txtRPM.value*Math.PI/30*Math.random(),3),
			',0)'
		].join('');
	else
		txtInitialAngularVelocity.value = [
			'(0,',
			$tw.ve.round(+txtRPM.value*Math.PI/30,3),
			',0)'
		].join('');
	dice.angular_velocity.copy($tw.threeD.evalVector(txtInitialAngularVelocity.value));
	dice.acceleration.copy(scene.g);
	dice.angular_acceleration.set(0,0,0);

	dice.angular_position.set(0,0,0);
	dice.rotation.set(0,0,0);
	dice.position.set(0,0,dice.getHeight()*10);
	dice.updatePivot();
	dice.coord.position.copy(dice.position);

	dice.ptr.position.copy(dice.position);
	dice.coord.setQuaternion(dice.quaternion);

	dice.calculateTorque(dice.position,dice.pivot.position);
	updateMotionIndicators(dice,f_motion);
	//updateCornerIndicators(dice);
	dice.arrTorque.scaleLength(1e1);
	dice.arrOmega.scaleLength(2);

	// The above scaleLength has to go before the applyQuaternion below, otherwise
	// we will need to call updateLabel(dice.quaternion) again

	// It's supposed to "set" quaternion and not applyQuaterrnion here, but for
	// reasons unknown to me it has to be applyQuaternion to give the correct visual
	// result.
	dice.arrOmega.applyQuaternion(dice.quaternion).updateLabel(dice.quaternion);

	dice.fartip.position.copy(dice.arrOmega.getAxis().applyQuaternion(dice.quaternion)).add(dice.arrOmega.position);

	dice.I = dice.principleInertiaMoments();
	let x2 = dice.position.x*dice.position.x,
		y2 = dice.position.y*dice.position.y,
		z2 = dice.position.z*dice.position.z;
	dice.Ip = [
		dice.mass*(y2+z2),
		dice.mass*(z2+x2),
		dice.mass*(x2+y2)
	];
console.log('dice.I=',dice.I,'dice.Ip=',dice.Ip);
};
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	dice.arrV.show(chkShowVelocity.checked);
	dice.arrA.show(chkShowAcceleration.checked);
	dice.arrOmega.show(dice.fartip.visible = chkShowAngularVelocity.checked);
	dice.arrAlpha.show(chkShowAngularAcceleration.checked);
	dice.arrF.show(chkShowForce.checked);
	dice.arrTorque.show(chkShowTorque.checked);
	dice.arrTorque.label.visible = false;
	dice.arrdr.show(chkShowTorque.checked);
	getTrailParam(dice);
	getTrailParam(dice.fartip);
	getFrictionParam(dice);
};
//}}}
/***
!!!! dice.afterCollision
***/
//{{{
dice.afterCollision = () => {
	dice.velocity.multiplyScalar(0.85);
	dice.angular_velocity.multiplyScalar(-0.85);
	//if(dice.angular_velocity.lengthSq() < 1e-12)
	//	dice.velocity.set(0,0,0);
	//if(dice.velocity.lengthSq() < 1e-12)
	//	dice.angular_velocity.set(0,0,0);
}
//}}}
/***
!!!! calculateA
***/
//{{{
const calculateA = (r,v,t,a) => {
	return a.copy(scene.g);
},
//}}}
/***
!!!! calculateAlpha
***/
//{{{
calculateAlpha = (theta,omega,t,alpha) => {
	dice.calculatePositions(theta).calculateTorque(dice.rt,dice.rpt);
	alpha.x = (dice.torque.dot(dice.coord.axis[0])-(dice.I[2]-dice.I[1])*omega.y*omega.z) / dice.I[0];
	alpha.y = (dice.torque.dot(dice.coord.axis[1])-(dice.I[0]-dice.I[2])*omega.z*omega.x) / dice.I[1];
	alpha.z = (dice.torque.dot(dice.coord.axis[2])-(dice.I[1]-dice.I[0])*omega.x*omega.y) / dice.I[2];
	return alpha;
}
//}}}
/***
!!!! scene.update
***/
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		delta_pos = $tw.physics.nextPosition(dice,calculateA,t_cur,dt,adaptive,e0),
		delta_angle = $tw.physics.nextAngle(dice,calculateAlpha,t_cur,dt,adaptive,e0);
	if(adaptive) setdT(Math.min(delta_angle[2],delta_pos[2]));
	dice.updatePivot();
	dice.coord.setPosition(dice.position)//.setQuaternion(dice.quaternion);
	updateMotionIndicators(dice,f_motion);
	//updateCornerIndicators(dice);
	dice.arrTorque.scaleLength(1e2);
	dice.arrOmega.scaleLength(2);
	dice.arrOmega.applyQuaternion(dice.quaternion).updateLabel(dice.quaternion);
	dice.fartip.position.copy(dice.arrOmega.getAxis().applyQuaternion(dice.quaternion)).add(dice.position);
	let hertzian = chkHertzianModel.checked,
		result = $tw.physics.objCheckCollisions(dice,hertzian,walls);
	/*
	dice.torque.set(0,0,0);
	for(let n1=0,N1=result.intersects_on_walls.length; n1<N1; n1++){
		let intersects = result.intersects_on_walls[n1];
		for(let n2=0,N2=intersects.length; n2<N2; n2++){
			let intersect = intersects[n2];
			if(intersect.collision){
				dice.torque.copy(intersect[0].point).sub(dice.position);
				if(hertzian){
					dice.torque.cross($tw.physics.__df[0]);
				}else{
					dice.torque.cross($tw.physics.__dp[0]).multiplyScalar(1/dt);
				}
				dice.worldToLocal(dice.torque);
				break;
			}
		}
	}
	//*/
}
//}}}
|Three Sided Dice Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">> / <<tw3DCommonPanel "Contact Model">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Elastic Properties">> / <<tw3DCommonPanel "Friction">>|[ =chkVelocity] <<tw3DCommonPanel "Initial Velocity">>|
|Dice: <<tw3DCommonPanel "Cylinder Properties">>|[ =chkRandom] Random / <<tw3DCommonPanel "Initial RPM">> / <<tw3DCommonPanel "Initial Angular Velocity">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Three Sided Dice Creation]] [[Three Sided Dice Initialization]] [[Three Sided Dice Iteration]]>>|
* power spectrum
** real data 將實座標軸 shift
** 增加 data.t
** 計算頻率或 k
* transform
** bluestein algorithm
* benchmark
* fftnflat 改成使用 data._dN 及 data._index
* fftnary
* 用 Buffer view 的方式改寫?
* 畫圖加上 texture
** tw3jsobject 多一個 setTexture(),用 material.setvalues({map:texture});
|~ViewToolbar|closeTiddler closeOthers editTiddler > fields syncing permalink references jump|
|~EditToolbar|+saveTiddler -cancelTiddler deleteTiddler|
/***
!!! 設計場景
***/
//{{{
//scene.camera.position.multiplyScalar(2);

let __tmpV = vector();								// 共用暫時向量(計算過程使用)

let floor = box({                               	// 產生一個地板
    width:2,                                    	// 寬度 1 公尺
	height:0.02,                                	// 高度 0.02 公尺(2 公分)
	length:2,                                   	// 長度 2 公尺
	opacity:0.5                                 	// 半透明
});

let spring = [
	helix({												// 產生一個彈簧
		pos:floor.pos,                              	// 把固定端放在地板的中心
		axis:vector(1,0,0),								// 水平放置
		radius:0.03,                                	// 半徑 0.03 公尺(3 公分)
		coils:40                                    	// 圈數
	}),
	helix({												// 小彈簧
    	pos:floor.pos,                              	// 把固定端放在地板的中心
		axis:vector(1,0,0),								// 水平放置
		opacity:0.1,
		radius:0.01,                                	// 半徑 0.03 公尺(3 公分)
		coils:20                                    	// 圈數 20
	})
];

let ball = [
	sphere({												// 產生一顆球
	    pos:__tmpV.copy(spring[0].position).add(spring[0].axis),	// 放在彈簧的活動端
		radius:0.1,                                 		// 半徑 0.1 公尺(10 公分)
		opacity:0.5,										// 半透明
    	make_trail:false,                           		// 要產生軌跡(但暫時不顯示)
    	interval:30                                 		// 每 30 次計算留下一個軌跡點
	}),
	sphere({												// 小球
	    pos:__tmpV.copy(spring[1].position).add(spring[1].axis),	// 放在小彈簧的活動端
		radius:0.02,                                 		// 半徑 0.02 公尺(2 公分)
		opacity:0.2,										// 半透明
	    make_trail:false,                           		// 要產生軌跡(但暫時不顯示)
	    interval:30                                 		// 每 30 次計算留下一個軌跡點
	})
];

chkGravity.checked = true;								// 開啟模擬重力場
chkGravity.disabled = true;								// 禁止關閉重力場
chkFriction.checked = true;								// 開啟摩擦力

txtDAQRate.value = 100;
txtTmax.value = 50;
txtSpringK.value = 50;
txtSpringL0.value = floor.getLength()/4;
labelPlot[0].innerHTML = '\\(x(t)\\)';
dataPlot[0].setTitle('\\(x(t)\\)')
	.setYTitle('\\(x \\text{ (m)}\\)').setXTitle('\\(t(s)\\)');
labelPlot[1].innerHTML = '\\(P(f)\\)';
dataPlot[1].setTitle('\\(P(f)\\)')
	.setYTitle('\\(P\\)').setXTitle('\\(f(s^{-1})\\)');

activateDAQChannels(3);
attachDAQBuffer(0,0);
//attachDAQBuffer(0,1);
//attachDAQBuffer(1,2);
//}}}
/***
!!! 初始條件
***/
//{{{
let muk = 0.1;
let __r = [], __v = [], __a = [], __m = [], __k = [], __L0 = [], __r0 = [];
scene.init = () => {
	muk = +txtMuk.value;										// 動摩擦係數
	spring[0].L0 = +txtSpringL0.value;	                			// 紀錄彈簧原長
	spring[0].setLength(spring[0].L0);								// 調整彈簧長度
	spring[0].k = +txtSpringK.value;                      			// 設定彈簧常數

	spring[1].L0 = spring[0].L0 / 2;
	spring[1].setLength(spring[1].L0);
	spring[1].k = spring[0].k / 10;

	ball[0].mass = +txtBallMass.value;                         	// 球的質量
	ball[0].mg = ball[0].mass*9.8;                            		// 球所受到的重力
	ball[0].setRadius(+txtBallRadius.value);                      	// 球的質量
	spring[0].setRadius(ball[0].getRadius()/4);						// 彈簧半徑

	ball[1].mass = ball[0].mass / 10;
	ball[1].mg = ball[1].mass*9.8;
	ball[1].setRadius(ball[0].getRadius());
	spring[1].setRadius(spring[0].getRadius());

	ball[0].position.y = floor.position.y+floor.getHeight()/2+
			ball[0].getRadius();									// 把球提升到地板上
	spring[0].position.y = ball[0].position.y;            			// 把彈簧接到球心
	ball[0].r0 = spring[0].axis.clone().add(spring[0].position);	// 紀錄彈簧活動端的初始位置

	spring[1].position.copy(ball[0].position);
	ball[1].position.copy(spring[1].position).add(spring[1].getAxis());
	ball[1].position.y = floor.position.y+floor.getHeight()/2+
			ball[1].getRadius();
	spring[1].position.y = ball[1].position.y;
	ball[1].r0 = spring[1].axis.clone().add(spring[1].position);

	ball[0].position.x = spring[0].position.x+spring[0].L0*1.5;		// 水平拉長/壓縮彈簧
	spring[0].setLength(spring[0].L0*1.5);							// 調整彈簧長度

	ball[0].velocity = vector(0,0,0);                       		// 設定初始速度
	ball[0].acceleration = vector();								// 計算初始加速度
	ball[0].clearTrail();
	ball[0].make_trail = true;                              		// 開始顯示軌跡

	ball[1].velocity = vector();
	ball[1].acceleration = vector();
	ball[1].clearTrail();
	ball[1].make_trail = true;

	__m[0] = ball[0].mass;
	__m[1] = ball[1].mass;
	__k[0] = spring[0].k;
	__k[1] = spring[1].k;

	__L0[0] = spring[0].L0;
	__L0[1] = spring[1].L0;

	__r[0] = ball[0].position.clone();
	__r[1] = ball[1].position.clone();
	__r0[0] = ball[0].r0;
	__r0[1] = spring[1].position.clone();

	__v[0] = ball[0].velocity.clone();
	__v[1] = ball[1].velocity.clone();

	__a[0] = ball[0].acceleration.clone();
	__a[1] = ball[1].acceleration.clone();
}
//}}}
/***
!!! 細步計算
彈簧身長或者縮短都會產生回復力 \[\vec F = -k\Delta \vec r,\] 其中 \(\Delta \vec r\) 是彈簧的伸長(縮短)向量。
!!!! 細步計算程式碼
***/
//{{{
let calcA = (r,v,t,a) => {									// 定義計算加速度的函數
	if(!a) a = [];
	for(let n=0,N=r.length; n<N; n++)
		a[n] = vector();

	__tmpV.copy(r[1]).sub(__r0[1]);
	a[1].copy(__tmpV).normalize()
		.multiplyScalar(-__k[1]*__tmpV.length()-__L0[1]);		// 彈簧回復力
	a[0].copy(r[0]).sub(__r0[0]).multiplyScalar(-__k[0]).sub(a[1]);

	if(chkAirDrag.checked){
		a[0].add($tw.physics.airDragSphere(v[0],ball[0].getRadius(),__tmpV));		// 空氣阻力
		a[1].add($tw.physics.airDragSphere(v[1],ball[1].getRadius(),__tmpV));
	}
	if(chkFriction.checked){
		a[0].add(__tmpV.copy(v[0]).normalize()						// 球與桌面的摩擦力
			.multiplyScalar(-muk*__m[0]*9.8)
		);
		a[1].add(__tmpV.copy(v[1]).normalize()
			.multiplyScalar(-muk*__m[1]*9.8)
		);
	}
	a[0].multiplyScalar(1/__m[0]);						// 除以球的質量
	a[1].multiplyScalar(1/__m[1]);
	return a;
};
//}}}
/***
!!!! Update
***/
//{{{
scene.update = (t_cur,dt) => {
	// 以 四階 Runge-Kutta 方法計算下一個時間點的位置、速度,以及加速度。
    $tw.numeric.ODE.nextValue(
		__r,													// 現在位置
		__v,													// 現在速度
		calcA,													// 計算加速度的函數
		t_cur,dt,												// 現在時間,時間間隔
		__a														// 現在加速度
	);

	ball[0].position.copy(__r[0]);
	ball[1].position.copy(__r[1]);

	ball[0].velocity.copy(__v[0]);
	ball[1].velocity.copy(__v[1]);

	ball[0].acceleration.copy(__a[0]);
	ball[1].acceleration.copy(__a[1]);

    spring[0].setAxis(
		__tmpV.copy(ball[0].position).sub(spring[0].position)
	);															// 調整【畫面上】彈簧的長度與方向
																// 畫面上的彈簧並不會自動調整,所以要在這裡手動調整
	__r0[1].copy(spring[1].position.copy(ball[0].position));
	spring[1].setAxis(
		__tmpV.copy(ball[1].position).sub(spring[1].position)
	);
	recordData(
		scene.currentTime(),null,
		[
			ball[0].position.x,
			ball[1].position.x,
			ball[0].position.x
		]
	);
};
//}}}
!! Spring Control
Spring \(M\) (kg): <html><input type="number" title="Spring mass." id="txtSpringMass" min="0" max="2" step="0.001" value="0.1" style="width:55px"></html> / \(L_0\) (m): <html><input type="number" title="Spring length." id="txtSpringL0" min="0" max="2" step="0.01" value="0.1" style="width:50px"></html> / \(k\) (N/m): <html><input type="number" title="Spring constant." id="txtSpringK" min="0" max="100000" step="1" value="5" style="width:60px"></html>
!! Ball Control
Ball \(M\) (kg): <html><input type="number" title="Ball mass." id="txtBallMass" min="0" max="10" step="0.2" value="0.5" style="width:50px"></html> / \(R\) (m): <html><input type="number" title="Ball radius." id="txtBallRadius" min="0" max="1" step="0.01" value="0.05" style="width:50px"></html>
|Undertone Sound Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">>|<<tiddler "Undertone Sound Panel##Spring Control">>|
|<<tiddler "Undertone Sound Panel##Ball Control">>|<<tw3DCommonPanel "Friction Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Undertone Sound Initial]] [[Undertone Sound JS Codes]]>>|
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
/***
! Definition Section
<<<
Definition for the light, screen, space, etc.
<<<
!!! UI Widgets
***/
//{{{
let txtMaxIndex = document.getElementById('txtMaxIndex'),
	txtNx = document.getElementById('txtNx'),
//}}}
/***
!! The Screen
<<<
The screen in the experiment to show the resultant image.
* The screen is divided into many small pieces, each of which corresponds to a single pixel and receives light rays from all possible directions.
* In __Python with visual__ (~VPython) this shall be an array of colors, with the length matches the number of pixels on the screen.
** Each pixel color is itself an array of 4 numbers (//r//, //g//, //b//, //a//), namely //red//, //green//, //blue//, //alpha// (transparency).
* In Javascript (with THREE.js) this could be a canvas that has the same dimensions (in pixels) as the screen.
<<<
***/
//{{{
	Nx = (txtNx ? Math.pow(2,txtNx.value*1) : 8), Ny = Nx,
	canvas, screen, texture, scnplane,
	scnint = null, dnx = null, dny = null;

const createScreen = () => {
	if(!canvas) canvas = document.createElement('canvas');
	canvas.width = Nx;
	canvas.height = Ny;
	screen = canvas.getContext('2d');
	//screen.fillStyle = '#0';
	//screen.fillRect(0,0,Nx,Ny);
	texture = new $tw.threeD.THREE.Texture(canvas);
	if(scnplane) scene.remove(scnplane);
	scnplane = plane({
		width: Nx,
		height: Ny,
		map: texture
	});

	scnint = $tw.data.TypedArray.Float32Array2D(Nx,Ny);
	scnint.init = function(){
		screen.fillStyle = '#0';
		screen.fillRect(0,0,Nx,Ny);
		scnint.fill(0.5);
		texture.needsUpdate = true;
		dnx.fill(0);
		dny.fill(0);
	};

	dnx = $tw.data.TypedArray.Int8Array2D(Nx,Ny);
	dny = $tw.data.TypedArray.Int8Array2D(Nx,Ny);
	scnint.init();
}
createScreen();
scene.createTrackballControl();
cartesian.show(false);
chkBox.checked = true;
chkRayPath.checked = true;
scene.camera.position.z = 1200*(txtNx.value*1+1);
//}}}
/***
!!! The Light Source
<<<
The light source in the experiment.
* The light source could be either a point source or a plane source.
<<<
***/
//{{{
let light = sphere({
	R: 0.1,
	visible: false
});
light.position.z = Nx;
//}}}
/***
!!! The Space
<<<
The space between the screen and the light source, mainly the test area of the experiment.
* The space is divided into many small regions.
** Each region could be cubic for simplicity.
* Each of the small region is small enough that it can be reasonably considered to have a uniform density within its small volume.
** Both in Python and Javascript, and many other computer languages as well, this shall be represented as a 3D array.
*** Each element in this array contains the index of refraction in the corresponding small region.
* Each of the small region is also large enough that light rays get deflected traveling through one region with a refraction index different from its neighbors (//"perpendicular"// to the propagation direction.)
** Light rays certainly get deflected //crossing// regions of different refraction indexes. (The Snell's law.)
<<<
***/
//{{{
let space = null, faces = [], gfaces = group(),
	Nz = Nx, nMax,
	Nx2 = Nx/2, Ny2 = Ny/2,
	t1 = performance.now();

const initSpace = () => {
	nMax = (txtMaxIndex ? txtMaxIndex.value*1 : 1.1);
	for(let nx=0; nx<Nx; nx++){
		if(faces[nx]===undefined){
			faces[nx] = [];
		}
		for(let ny=0; ny<Ny; ny++){
			if(faces[nx][ny]===undefined){
				faces[nx][ny] = [];
			}
			for(let nz=0; nz<Nz; nz++){
				if(faces[nx][ny][nz]===undefined){
					gfaces.add(faces[nx][ny][nz] = box({
						width: 1,
						height: 1,
						depth: 1
					},'noadd'));
					faces[nx][ny][nz].position.set(nx-Nx2+0.5,ny-Ny2+0.5,nz+0.5);
				}
				let n = 1+((nx===Nx2&&ny===Ny2&&nz===Nx2)?(nMax-1):0); //(nMax-1)*Math.random();
				space.set(nx,ny,nz,n);
//console.log('space['+nx+']['+ny+']['+nz+']='+space.get(nx,ny,nz));
				faces[nx][ny][nz].setOpacity((n-1)/(nMax-1)*0.2);
			}
		}
	}
}
space = $tw.data.TypedArray.Float32Array3D(Nx,Ny,Nz);
initSpace();
gfaces.visible = chkBox.checked;
scene.add(gfaces);
chkBox.onclick = () => {
	gfaces.visible = chkBox.checked;
}
//}}}
/***
!!! Ray paths
<<<
Lines to represent the light path.
<<<
***/
//{{{
let path = [], gpath = group();

path.clear = () => {
	for(let nx=Nx-1; nx>=0; nx--){
		for(let ny=Ny-1; ny>=0; ny--){
			gpath.remove(path[nx][ny]);
			path[nx][ny] = undefined;
		}
		path[nx] = undefined;
	}
	return path;
}

path.reset = () => {
	for(let nx=0; nx<Nx; nx++){
		if(path[nx]===undefined) path[nx] = [];
		if(nx===0 || nx===Nx-1) continue;
		for(let ny=0; ny<Ny; ny++){
			if(ny===0 || ny===Ny-1) continue;
			if(path[nx][ny]===undefined){
				gpath.add((path[nx][ny] = line({
					color: 0x00ff00,
					opacity: 0.3
				},'noadd')));
			}
			for(let nz=0; nz<=Nz; nz++){
				path[nx][ny].setVertex(nz,vector(nx-Nx2+0.5,ny-Ny2+0.5,nz));
			}
			path[nx][ny].geometry.verticesNeedUpdate = true;
		}
	}
	return path;
};
path.reset();
gpath.visible = chkRayPath.checked;
scene.add(gpath);
//}}}
/***
!! Hit positions
<<<
Dots to represent positions where the light rays hit the boundaries between pieces of air.
<<<
***/
//{{{
let hit = [], ghit = group();
hit.clear = () => {
	for(let nx=Nx-1; nx>=0; nx--){
		for(let ny=Ny-1; ny>=0; ny--){
			ghit.remove(hit[nx][ny]);
			hit[nx][ny] = undefined;
		}
		hit[nx] = undefined;
	}
	return hit;
}

hit.reset = () => {
	for(let nx=0; nx<Nx; nx++){
		if(hit[nx]===undefined) hit[nx] = [];
		for(let ny=0; ny<Ny; ny++){
			if(nx===0 || ny===0 || nx===Nx-1 || ny===Ny-1)
				continue;
			if(hit[nx][ny]===undefined){
				ghit.add(hit[nx][ny] = sphere({
					R: 0.05,
					color: 0xffff00
				},'noadd'));
			}
			hit[nx][ny].position.set(nx-Nx2+0.5,ny-Ny2+0.5,Nz);
		}
	}
	return hit;
};
hit.reset();
ghit.visible = chkRayPath.checked;
scene.add(ghit);
chkRayPath.onclick = () => {
	gpath.visible = ggradn.visible = gdevc.visible = ghit.visible = chkRayPath.checked;
}
//}}}
/***
!!! traceRay(light, space, screen, nx, ny[, p0])
<<<
Forward trace one light ray passing through one small region in space..
* Starts from parallel light rays.
* Light rays get deflected passing through each of the small regions in space.
* The deflection is caused by local variations in the index of refraction (due to variations in air density.)
* In this simulation we calculate the deflection
** while a light ray is traveling through one small region (of constant index),
*** Within this region the light ray gets deflected by the variations in the //x//- and //y//-directions (assuming propagation is mainly in the //z//-direction), \[\epsilon_x = {1 \over n}\int {\partial n \over \partial x}dz, \qquad \epsilon_y = {1 \over n}\int {\partial n \over \partial y}dz.\]
** and crossing regions of different indexes.
*** Here the deflection follows the Snell's law: \[\vec v_\text{reflect} = r\vec I + \left(rc - \sqrt{1-r^2(1-c^2)}\vec n \right).\]
** See [[2017-08 看見密度]] for more details.
<<<
***/
//{{{
const traceRay = (light, space, screen, nx, ny, nz, p0) => {
	if(!p0) p0 = path[nx][ny].getVertex(nz+1);
	let dnxcur = dnx.get(nx,ny), dnycur = dny.get(nx,ny),
//console.log('dnx['+nx+']['+ny+']='+dnxcur+' dny['+nx+']['+ny+']='+dnycur);
		c1 = vector(), c2 = vector(), ci = vector(),
		xaxis = vector(1,0,0), yaxis = vector(0,1,0), zaxis = vector(0,0,1),
		nxaxis = vector(-1,0,0), nyaxis = vector(0,-1,0), nzaxis = vector(0,0,-1),
		gradn = vector(), gradnarr,
		norm = vector(), normarr;
//}}}
/***
!!!! distribute_x_center(sx, sy, dy, intensity)
<<<
Distribute the intensity of a light ray in the horizontally central area.
<<<
***/
//{{{
	const distribute_x_center = (sx, sy, dy, intensity) => {
		// The x-component of the hit position is very close to the center.
		if(dy==='c'){
			// The y-component is also close to the center, let the
			// intensity go fully to the pixel at the hit position.
		}else if(dy==='tt'){
			// The y-component is close to the top edge, let the intensity
			// go 0.5 to this pixel and 0.5 to its top neighbor.
			scnint.add(sx,sy+1,(intensity*=0.5));
		}else if(dy==='bb'){
			// The y-component is close to the bottom edge, let the intensity
			// go 0.5 to this pixel and 0.5 to its bottom neighbor.
			scnint.add(sx,sy-1,(intensity*=0.5));
		}else if(dy==='t'){
			// The y-component is in the upper half of this pixel, let the
			// intensity go 0.7 to this pixel and 0.3 to its top neighbor.
			scnint.add(sx,sy+1,(intensity*0.3));
			intensity *= 0.7;
		}else if(dy==='b'){
			// The y-component is in the lower half of this pixel, let the
			// intensity go 0.7 to this pixel and 0.3 to its bottom neighbor.
			scnint.add(sx,sy-1,(intensity*0.3));
			intensity *= 0.7;
		}
		scnint.add(sx,sy,intensity);
	},
//}}}
/***
!!!! distribute_x_left(sx, sy, dy, intensity)
<<<
Distribute the intensity of a light ray in the horizontally left area.
<<<
***/
//{{{
	distribute_x_left = (sx, sy, dy, intensity) => {
		// The x-component of the hit position is in the left side.
		if(dy==='c'){
			// The y-component is also close to the center, let the
			// intensity go 0.7 to the pixel and 0.3 to its left neighbor.
			scnint.add(sx-1,sy,(intensity*0.3));
			intensity *= 0.7;
		}else if(dy==='tt'){
			// The y-component is close to the top edge, let the intensity
			// go 0.7 to this pixel and its top neighbor (evenly),
			// and 0.3 to its left and top-left neighbors (evenly).
			// left and top-left neighbors (0.3/2=0.15)
			scnint.add(sx-1,sy,(intensity*0.15));
			scnint.add(sx-1,sy+1,(intensity*0.15));
			// top neighbor (0.7/2=0.35) and self (0.7/2=0.35)
			scnint.add(sx,sy+1,(intensity*=0.35));
		}else if(dy==='bb'){
			// The y-component is close to the bottom edge, let the intensity
			// go 0.7 to this pixel and its bottom neighbor (evenly),
			// and 0.3 to its left and bottom-left neighbors (evenly).
			// left and bottom-left neighbors (0.3/2=0.15)
			scnint.add(sx-1,sy,(intensity*0.15));
			scnint.add(sx-1,sy-1,(intensity*0.15));
			// bottom neighbor (0.7/2=0.35) and self (0.7/2=0.35)
			scnint.add(sx,sy-1,(intensity*=0.35));
		}else if(dy==='t'){
			// The y-component is in the upper half of this pixel, let the
			// intensity go 0.7*0.7 to this pixel, 0.7*0.3 to its top neighbor,
			// 0.3*0.7 to its left neighbor, 0.3*0.3 to its top-left neighbor.
			// left (0.3*0.7=0.21) and top-left neighbors (0.3*0.3=0.09)
			scnint.add(sx-1,sy,(intensity*0.21));
			scnint.add(sx-1,sy+1,(intensity*0.09));
			// top neighbor (0.7*0.3=0.21) and self (0.7*0.7=0.49)
			scnint.add(sx,sy+1,(intensity*0.21));
			intensity *= 0.49;
		}else if(dy==='b'){
			// The y-component is in the lower half of this pixel, let the
			// intensity go 0.7*0.7 to this pixel, 0.7*0.3 to its bottom neighbor,
			// 0.3*0.7 to its left neighbor, 0.3*0.3 to its bottom-left neighbor.
			// left (0.3*0.7=0.21) and bottom-left neighbors (0.3*0.3=0.09)
			scnint.add(sx-1,sy,(intensity*0.21));
			scnint.add(sx-1,sy-1,(intensity*0.09));
			// bottom neighbor (0.7*0.3=0.21) and self (0.7*0.7=0.49)
			scnint.add(sx,sy-1,(intensity*0.21));
			intensity *= 0.49;
		}
		scnint.add(sx,sy,intensity);
	},
//}}}
/***
!!!! distribute_x_right(sx, sy, dy, intensity)
<<<
Distribute the intensity of a light ray in the horizontally right area.
<<<
***/
//{{{
	distribute_x_right = (sx, sy, dy, intensity) => {
		// The x-component of the hit position is in the right side.
		if(dy==='c'){
			// The y-component is also close to the center, let the
			// intensity go 0.7 to the pixel and 0.3 to its right neighbor.
			scnint.add(sx+1,sy,(intensity*0.3));
			intensity *= 0.7;
		}else if(dy==='tt'){
			// The y-component is close to the top edge, let the intensity
			// go 0.7 to this pixel and its top neighbor (evenly),
			// and 0.3 to its right and top-right neighbors (evenly).
			// right and top-right neighbors (0.3/2=0.15)
			scnint.add(sx+1,sy,(intensity*0.15));
			scnint.add(sx+1,sy+1,(intensity*0.15));
			// top neighbor (0.7/2=0.35) and self (0.7/2=0.35)
			scnint.add(sx,sy+1,(intensity*=0.35));
		}else if(dy==='bb'){
			// The y-component is close to the bottom edge, let the intensity
			// go 0.7 to this pixel and its bottom neighbor (evenly),
			// and 0.3 to its right and bottom-right neighbors (evenly).
			// right and bottom-right neighbors (0.3/2=0.15)
			scnint.add(sx+1,sy,(intensity*0.15));
			scnint.add(sx+1,sy-1,(intensity*0.15));
			// bottom neighbor (0.7/2=0.35) and self (0.7/2=0.35)
			scnint.add(sx,sy-1,(intensity*=0.35));
		}else if(dy==='t'){
			// The y-component is in the upper half of this pixel, let the
			// intensity go 0.7*0.7 to this pixel, 0.7*0.3 to its top neighbor,
			// 0.3*0.7 to its right neighbor, 0.3*0.3 to its top-right neighbor.
			// right (0.3*0.7=0.21) and top-right neighbors (0.3*0.3=0.09)
			scnint.add(sx+1,sy,(intensity*0.21));
			scnint.add(sx+1,sy+1,(intensity*0.09));
			// top neighbor (0.7*0.3=0.21) and self (0.7*0.7=0.49)
			scnint.add(sx,sy+1,(intensity*0.21));
			intensity *= 0.49;
		}else if(dy==='b'){
			// The y-component is in the lower half of this pixel, let the
			// intensity go 0.7*0.7 to this pixel, 0.7*0.3 to its bottom neighbor,
			// 0.3*0.7 to its right neighbor, 0.3*0.3 to its bottom-right neighbor.
			// right (0.3*0.7=0.21) and bottom-right neighbors (0.3*0.3=0.09)
			scnint.add(sx+1,sy,(intensity*0.21));
			scnint.add(sx+1,sy-1,(intensity*0.09));
			// bottom neighbor (0.7*0.3=0.21) and self (0.7*0.7=0.49)
			scnint.add(sx,sy-1,(intensity*0.21));
			intensity *= 0.49;
		}
		scnint.add(sx,sy,intensity);
	},
//}}}
/***
!!!! distribute_x_leftedge(sx, sy, dy, intensity)
<<<
Distribute the intensity of a light ray close to the left edge.
<<<
***/
//{{{
	distribute_x_leftedge = (sx, sy, dy, intensity) => {
		// The x-component of the hit position is close to the left edge.
		if(dy==='c'){
			// The y-component is also close to the center, let the
			// intensity go 0.5 to the pixel and 0.5 to its left neighbor.
			scnint.add(sx-1,sy,(intensity*=0.5));
		}else if(dy==='tt'){
			// The y-component is close to the top edge, let the intensity
			// go 0.5 to this pixel and its top neighbor (evenly),
			// and 0.5 to its left and top-left neighbors (evenly).
			// left and top-left neighbors (0.5/2=0.25)
			scnint.add(sx-1,sy,(intensity*=0.25));
			scnint.add(sx-1,sy+1,(intensity));
			// top neighbor (0.5/2=0.25) and self (0.2/2=0.25)
			scnint.add(sx,sy+1,(intensity));
		}else if(dy==='bb'){
			// The y-component is close to the bottom edge, let the intensity
			// go 0.5 to this pixel and its bottom neighbor (evenly),
			// and 0.5 to its left and bottom-left neighbors (evenly).
			// left and bottom-left neighbors (0.5/2=0.25)
			scnint.add(sx-1,sy,(intensity*=0.25));
			scnint.add(sx-1,sy-1,(intensity));
			// bottom neighbor (0.5/2=0.25) and self (0.5/2=0.25)
			scnint.add(sx,sy-1,(intensity));
		}else if(dy==='t'){
			// The y-component is in the upper half of this pixel, let the
			// intensity go 0.5*0.7 to this pixel, 0.5*0.3 to its top neighbor,
			// 0.5*0.7 to its left neighbor, 0.5*0.3 to its top-left neighbor.
			// left (0.5*0.7=0.35) and top-left neighbors (0.5*0.3=0.15)
			scnint.add(sx-1,sy,(intensity*0.35));
			scnint.add(sx-1,sy+1,(intensity*0.15));
			// top neighbor (0.5*0.3=0.15) and self (0.5*0.7=0.35)
			scnint.add(sx,sy+1,(intensity*0.15));
			intensity *= 0.35;
		}else if(dy==='b'){
			// The y-component is in the lower half of this pixel, let the
			// intensity go 0.5*0.7 to this pixel, 0.5*0.3 to its bottom neighbor,
			// 0.5*0.7 to its left neighbor, 0.5*0.3 to its bottom-left neighbor.
			// left (0.5*0.7=0.35) and bottom-left neighbors (0.5*0.3=0.15)
			scnint.add(sx-1,sy,(intensity*0.35));
			scnint.add(sx-1,sy-1,(intensity*0.15));
			// bottom neighbor (0.5*0.3=0.15) and self (0.5*0.7=0.35)
			scnint.add(sx,sy-1,(intensity*0.15));
			intensity *= 0.35;
		}
		scnint.add(sx,sy,intensity);
	},
//}}}
/***
!!!! distribute_x_rightedge(sx, sy, dy, intensity)
<<<
Distribute the intensity of a light ray close to the right edge.
<<<
***/
//{{{
	distribute_x_rightedge = (sx, sy, dy, intensity) => {
		// The x-component of the hit position is close to the right edge.
		if(dy==='c'){
			// The y-component is also close to the center, let the
			// intensity go 0.5 to the pixel and 0.5 to its right neighbor.
			scnint.add(sx+1,sy,(intensity*=0.5));
		}else if(dy==='tt'){
			// The y-component is close to the top edge, let the intensity
			// go 0.5 to this pixel and its top neighbor (evenly),
			// and 0.5 to its right and top-right neighbors (evenly).
			// right and top-right neighbors (0.5/2=0.25)
			scnint.add(sx+1,sy,(intensity*=0.25));
			scnint.add(sx+1,sy+1,(intensity));
			// top neighbor (0.5/2=0.25) and self (0.5/2=0.25)
			scnint.add(sx,sy+1,(intensity));
		}else if(dy==='bb'){
			// The y-component is close to the bottom edge, let the intensity
			// go 0.5 to this pixel and its bottom neighbor (evenly),
			// and 0.5 to its right and bottom-right neighbors (evenly).
			// right and bottom-right neighbors (0.5/2=0.25)
			scnint.add(sx+1,sy,(intensity*=0.25));
			scnint.add(sx+1,sy-1,(intensity));
			// bottom neighbor (0.5/2=0.25) and self (0.5/2=0.25)
			scnint.add(sx,sy-1,(intensity));
		}else if(dy==='t'){
			// The y-component is in the upper half of this pixel, let the
			// intensity go 0.5*0.7 to this pixel, 0.5*0.3 to its top neighbor,
			// 0.5*0.7 to its right neighbor, 0.5*0.3 to its top-right neighbor.
			// right (0.5*0.7=0.35) and top-right neighbors (0.5*0.3=0.15)
			scnint.add(sx+1,sy,(intensity*0.35));
			scnint.add(sx+1,sy+1,(intensity*0.15));
			// top neighbor (0.5*0.3=0.15) and self (0.5*0.7=0.35)
			scnint.add(sx,sy+1,(intensity*0.15));
			intensity *= 0.35;
		}else if(dy==='b'){
			// The y-component is in the lower half of this pixel, let the
			// intensity go 0.5*0.7 to this pixel, 0.5*0.3 to its bottom neighbor,
			// 0.5*0.7 to its right neighbor, 0.5*0.3 to its bottom-right neighbor.
			// right (0.5*0.7=0.35) and bottom-right neighbors (0.5*0.3=0.15)
			scnint.add(sx+1,sy,(intensity*0.35));
			scnint.add(sx+1,sy-1,(intensity*0.15));
			// bottom neighbor (0.5*0.3=0.15) and self (0.5*0.7=0.35)
			scnint.add(sx,sy-1,(intensity*0.15));
			intensity *= 0.35;
		}
		scnint.add(sx,sy,intensity);
	},
//}}}
/***
!!!! distribute_intensity(hitpos, intensity)
<<<
Distribute the intensity of a light ray to the pixel at the hit position, and maybe partly to its neighbors.
<<<
***/
//{{{
	distribute_intensity = (hitpos, intensity) => {
		let sx = Math.floor(hitpos.x+Nx2);
		let sy = Math.floor(hitpos.y+Ny2);

		let dx = hitpos.x+Nx2 - sx;
		let dy = hitpos.y+Ny2 - sy;

		if(dx < 0.1) dx = 'll';
		else if(dx <= 0.4) dx = 'l';
		else if(dx < 0.6) dx = 'c';
		else if(dx < 0.9) dx = 'r';
		else dx = 'rr';

		if(dy < 0.1) dy = 'bb';
		else if(dy <= 0.4) dy = 'b';
		else if(dy < 0.6) dy = 'c';
		else if(dy < 0.9) dy = 't';
		else dy = 'tt';

		// and calculate its intensity contribution.
		if(dx==='c'){
			// The x-component of the hit position is very close to the center.
			distribute_x_center(sx,sy,dy,intensity);
		}else if(dx==='l'){
			distribute_x_left(sx,sy,dy,intensity);
		}else if(dx==='ll'){
			distribute_x_leftedge(sx,sy,dy,intensity);
		}else if(dx==='r'){
			distribute_x_right(sx,sy,dy,intensity);
		}else if(dx==='rr'){
			distribute_x_rightedge(sx,sy,dy,intensity);
		}
		//if(faces[sx]!==undefined && faces[sx][sy]!==undefined){
		//	faces[sx][sy][0].setColorHex(0xffff00);
		//	faces[sx][sy][0].setOpacity(0.3);
		//}
	},
//}}}
/***
!!!! goto_next()
<<<
Find the next region the light ray shall enter, apply Snell's law to find its entering direction. When the light ray hits the screen, calculate the contribution to the intensity at the hit position.
* Intensity of a light ray is assumed to have a symmetric Gaussian profile.
* The effective spot size of a light ray is assumed equivalent to that of one small region in space, i.e., with a radius of 0.5.
* The intensity of a light ray shall be distributed mainly to the pixel at the hit position, and partly to its immediate neighbors, according to its distribution profile.
<<<
***/
//{{{
	goto_next = () => {
		// Find the point at which the ray would hit the boundary and go through it
		// into the next piece of air.

		// The current direction of propagation is along c2.
		let x0 = nx+dnxcur-Nx2, y0 = ny+dnycur-Ny2, nr = 1;
		let wall = '';

		// First check if it will hit the front face.
		ci.copy(c2).multiplyScalar((nz-p0.z)/c2.z);
		hit[nx][ny].position.copy(p0).add(ci);
		if(nz===0){
			// The last piece of air, see where the light ray hits the screen
			path[nx][ny].setVertex(nz,hit[nx][ny].position);
			distribute_intensity(
				hit[nx][ny].position,
				Math.pow(ci.normalize().dot(nzaxis),2)
			);
			return;
		}
		if(hit[nx][ny].position.x>x0 && hit[nx][ny].position.x<x0+1 &&
			hit[nx][ny].position.y>y0 && hit[nx][ny].position.y<y0+1){
			// hit the front face
			norm.copy(nzaxis);
			nr = space.get(nx+dnxcur,ny+dnycur,nz)
				/ space.get(nx+dnxcur,ny+dnycur,nz-1);
			wall = 'front';
		}else if(c2.x>0){
			// Bend towards +x dir.
			ci.copy(c2).multiplyScalar((1-(gradnarr.position.x-x0))/c2.x);
			hit[nx][ny].position.copy(p0).add(ci);
//console.log('hit['+nx+']['+ny+'].position.y=',hit[nx][ny].position.y,'y0=',y0);
//console.log('hit['+nx+']['+ny+'].position.z=',hit[nx][ny].position.z,'nz=',nz);
			if(hit[nx][ny].position.y>=y0 && hit[nx][ny].position.y<=y0+1 &&
				hit[nx][ny].position.z>=nz && hit[nx][ny].position.z<nz+1){
				// hit the +x face
				norm.copy(xaxis);
				nr = space.get(nx+dnxcur,ny+dnycur,nz)
					/ space.get(nx+(++dnxcur),ny+dnycur,nz);
				wall = 'right';
				dnx.set(nx,ny,dnxcur);
			}
		}else if(c2.x<0){
			// Bend towards -x dir.
			ci.copy(c2).multiplyScalar((x0-gradnarr.position.x)/c2.x);
			hit[nx][ny].position.copy(p0).add(ci);
			if(hit[nx][ny].position.y>=y0 && hit[nx][ny].position.y<=y0+1 &&
				hit[nx][ny].position.z>=nz && hit[nx][ny].position.z<nz+1){
				// hit the -x face
				norm.copy(nxaxis);
				nr = space.get(nx+dnxcur,ny+dnycur,nz)
					/ space.get(nx+(--dnxcur),ny+dnycur,nz);
				wall = 'left';
				dnx.set(nx,ny,dnxcur);
			}
		}

		if(!wall){
			// Did not hit the front face, nor the right and left faces,
			// check the top and bottom faces.
			if(c2.y>0){
				// Bend towards +y dir.
				ci.copy(c2).multiplyScalar((1-(gradnarr.position.y-y0))/c2.y);
				hit[nx][ny].position.copy(p0).add(ci);
				if(hit[nx][ny].position.x>=x0 && hit[nx][ny].position.x<=x0+1 &&
					hit[nx][ny].position.z>=nz && hit[nx][ny].position.z<nz+1){
					// hit the +y face
					norm.copy(yaxis);
					nr = space.get(nx+dnxcur,ny+dnycur,nz)
						/ space.get(nx+dnxcur,ny+(++dnycur),nz);
					wall = 'top';
					dny.set(nx,ny,dnycur);
				}
			}else if(c2.y<0){
				// Bend towards -y dir.
				ci.copy(c2).multiplyScalar((y0-gradnarr.position.y)/c2.y);
				hit[nx][ny].position.copy(p0).add(ci);
				if(hit[nx][ny].position.x>=x0 && hit[nx][ny].position.x<=x0+1 &&
					hit[nx][ny].position.z>=nz && hit[nx][ny].position.z<nz+1){
					// hit the -y face
					norm.copy(nyaxis);
					nr = space.get(nx+dnxcur,ny+dnycur,nz)
						/ space.get(nx+dnxcur,ny+(--dnycur),nz);
					wall = 'bottom';
					dny.set(nx,ny,dnycur);
				}
			}
		}

		if(wall){
//if(nz===zend)console.log('path['+nx+']['+ny+'] hits the '+wall);
			// Apply Snell's law to calculate the propagation direction in the
			// piece of air.
			ci.copy(c2);
			let cos = ci.dot(norm);
			c2.copy(norm).multiplyScalar(-cos+Math.sqrt(1/(nr*nr)-(1-cos*cos)))
				.add(ci).multiplyScalar(nr);
			let p = hit[nx][ny].position.clone();
			if(wall !== 'front'){
				// Hit the side walls, extend the ray to the front face of the
				// the neighboring cell.
				path[nx][ny].setVertex(nz,p.add(
					ci.copy(c2).multiplyScalar((nz-p.z)/c2.z)
				));
				//hit[nx][ny].position.copy(p);
				c2.add(traceRay(
					light,space,screen,nx,ny,nz,p.copy(hit[nx][ny].position)
				));
			}

			// Set the propagation direction in the next piece of air.
			path[nx][ny].setVertex(nz,p.copy(hit[nx][ny].position));
			if(nz>=1) path[nx][ny].setVertex(nz-1,p.add(
				ci.copy(c2).multiplyScalar((nz-1-p.z)/c2.z)
			));
		}
	}
//}}}
/***
!!!! the main loop of traceRay()
***/
//{{{
	// Calculate the deflection angle while the light ray is passing through
	// this small piece of air, with p0 the entering position.

	// Find the "gradient" of refraction index "perpendicular" to the direction of
	// propagation.
	gradn.x = (
		(nx+dnxcur+1) < Nx
			? space.get(nx+dnxcur+1,ny+dnycur,nz) : 1
	) - (
		(nx+dnxcur-1) >= 0
			? space.get(nx+dnxcur-1,ny+dnycur,nz) : 1
	);

	gradn.y = (
		(ny+dnycur+1) < Ny
			? space.get(nx+dnxcur,ny+dnycur+1,nz) : 1
	) - (
		(ny+dnycur-1) >= 0
			? space.get(nx+dnxcur,ny+dnycur-1,nz) : 1
	);

	gradn.z = 0;
//console.log('gradn['+(nx+dnxcur)+']['+(ny+dnxcur)+']['+nz+']=',gradn);
	ggradn.add(gradnarr = arrow({
		axis: gradn,
		color: 0xff0000
	},'noadd'));
	gradnarr.position.copy(p0);

	// c1 is the direction upon entering this small piece of air.
	path[nx][ny].getVertex(nz,c1).sub(p0);

	// Sets the direction of propagation upon leaving this piece of air.
	let f = c1.length()/space.get(nx+dnxcur,ny+dnxcur,nz);
	c2.copy(c1.normalize()).applyAxisAngle(xaxis,gradn.y*f)
		.applyAxisAngle(yaxis,-gradn.x*f).normalize();

	// Check the angle between the gradient and the change in direction.
	/*
	let tmp = c1.clone();
	let dc = c2.clone().sub(tmp);
	let da = Math.acos(dc.normalize().dot(tmp.copy(gradn).normalize()));
	console.log('f=',f,'da=',da,'(',da/Math.PI*180,')');
	*/

	// Go to the next piece of air.
	goto_next();

	// Show the total change of propagation direction, including the change
	// by passing through this region and that by entering into the next.
	gdevc.add(arrow({
		pos: p0,
		axis: c2.sub(c1),
		color: 0xffff00
	},'noadd'));
	path[nx][ny].geometry.verticesNeedUpdate = true;
	return c2;
},
//}}}
/***
!!! mapIntensity(space, screen, nx, ny)
<<<
Map the intensity of one pixel onto the screen.
<<<
***/
//{{{
mapIntensity = (scnint,screen,nx,ny,f) => {
	let v = f*scnint.get(nx,ny);
	if(v > 255) v = 255;
	let c = Math.round(v).toString(16);
	screen.fillStyle = '#'+c+c+c;
	screen.fillRect(nx,Ny-1-ny,1,1);
},
//}}}
/***
!!! mapAllIntensities(light, space, screen)
<<<
Map the intensities of all pixels onto the screen.
<<<
***/
//{{{
mapAllIntensities = (scnint,screen) => {
	let t1 = performance.now();
	let f = 32;
	for(let nx=0; nx<Nx; nx++){
		for(let ny=0; ny<Ny; ny++){
			mapIntensity(scnint,screen,nx,ny,f);
		}
	}
	//console.log('['+Nx+']x['+Ny+'] intensities calculated in '+(t1=performance.now()-t1)+' ms');
}
//}}}
/***
!!! Reset function
<<<
Reset everything and start over.
<<<
***/
//{{{
let ggradn = group(), gdevc = group();
ggradn.visible = gdevc.visible = chkRayPath.checked;
scene.add(ggradn).add(gdevc);
const reset = () => {
	let NNx = Math.pow(2,txtNx.value*1);
	if(NNx > 0 && NNx !== Nx){
		path.clear();
		hit.clear();
		Nx = Ny = Nz = NNx;
		Nx2 = Nx/2; Ny2 = Ny/2;
		createScreen();
		space = $tw.data.TypedArray.Float32Array3D(Nx,Ny,Nz);
		//dt_cells = $tw.data.TypedArray.Float32Array2D(Nx,Ny);
		dt_planes = new Float32Array(Nz);
	}else
		scnint.init();

	//dt_cells.fill(0);
	dt_planes.fill(0);
	scene.remove(ggradn);
	scene.remove(gdevc);
	for(let n=0,N=ggradn.children.length; n<N; n++){
		ggradn.remove(ggradn.children[0]);
		gdevc.remove(gdevc.children[0]);
	}
	scene.add(ggradn).add(gdevc);
	//light.position.z = Nx;

	clearMessage();
	initSpace();
	path.reset();
	hit.reset();
	nz = Nz-1;
}
if(txtMaxIndex){
	//txtMaxIndex.onclick = reset;
	txtMaxIndex.onchange = reset;
}
if(txtNx){
	//txtNx.onclick = reset;
	txtNx.onchange = reset;
}
//}}}
/***
!!! Update Function
<<<
Update calculation status.
<<<
***/
//{{{
let nx0 = 1, ny0 = nx0, N0 = 8,
	//dt_cells = $tw.data.TypedArray.Float32Array2D(Nx,Ny),
	dt_planes = new Float32Array(Nz),
	nz = Nz-1, zend = 0;
const update = () => {
	if(nz >= zend){
		for(let n=0; nz>=zend && n<Nz; nz--, n++){
			dt_planes[nz] = performance.now();
			for(let nx=nx0; nx<Nx-1; nx++){
				for(let ny=ny0; ny<Ny-1; ny++){
					//dt_cells.set(nx,ny,performance.now());
					traceRay(light,space,screen,nx,ny,nz);
					//dt_cells.negadd(nx,ny,performance.now());
				}
			}
			dt_planes[nz] = performance.now()-dt_planes[nz];
		}
		//displayMessage('dt_planes times(ms): '+dt_planes);
		displayMessage('total time(ms): '+dt_planes.reduce(function(sum,val){return sum+val;}));
		let dt_map = performance.now();
		mapAllIntensities(scnint,screen);
		dt_map = performance.now()-dt_map;
		displayMessage('mapping time(ms): '+dt_map);
		texture.needsUpdate = true;
	}
}
//}}}
!! Max Index
<html>//n//~~max~~ <input type="number" title="Maximum Index of Refraction." id="txtMaxIndex" min="1.0" max="1.99" step="0.05" value="1.1" style="width:50px"></html>
!! Visible Control
[ =chkBox] Box / [ =chkRayPath] Path
!! Size Control
<html>//N//~~x~~ : 2^ <input type="number" title="Number fo divisions in x-dir." id="txtNx" min="2" max="10" step="1" value="3" style="width:35px"></html>
|Visualizing Density Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Simulation Time Control">>|
|<<tw3DCommonPanel "Label Statistics">>|<<tiddler "Visualizing Density Panel##Max Index">> <<tiddler "Visualizing Density Panel##Visible Control">> / <<tiddler "Visualizing Density Panel##Size Control">>|
|>|<<tw3DCommonPanel "Message">>|
|>|<<tw3DScene [[Visualizing Density Codes]]>>|
|Weighing Time Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Angular Motion">>|<<tw3DCommonPanel "Trail Control">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> / <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene>>|
/***
! Constants and UI Controls
***/
//{{{
// Motion related
txtBoostLevel.value = 1;
txtBoostLevel.onchange();
let linux_arm = config.userAgent.indexOf('linux arm') > -1;
let txtdT = document.getElementById('txtdT');
txtdT.value = (linux_arm ? 0.001 : 0.0002);
chkAutoCPF.checked = true;
txtTrailInterval.value = 10;
txtTrailLength.value = 65536;

// Spring related
let txtSpringLength = document.getElementById('txtSpringLength'),
	txtSpringRadius = document.getElementById('txtSpringRadius'),
	txtSpringCoils = document.getElementById('txtSpringCoils'),
	txtSpringK = document.getElementById('txtSpringK'),
	txtSpringMass = document.getElementById('txtSpringMass');

txtSpringMass.value = 0.01;
txtSpringKz.value = 10;
txtSpringKphi.value = 0.1;
txtSpringLength.value = 0.15;
txtSpringCoils.value = 50;
txtSpringRadius.value = 0.025;

// Rod related
let txtRodLength = document.getElementById('txtRodLength'),
	txtRodMass = document.getElementById('txtRodMass'),
	txtRodRadius = document.getElementById('txtRodRadius'),
	txtPhi0 = document.getElementById('txtPhi0');

txtRodLength.value = 0.2;
txtRodRadius.value = 0.005;
txtRodMass.value = 0.2;
txtPhi0.value = 0;
//}}}
/***
! Creation and Definitions
<<<
Creation of Wilberforce Pendulum
<<<
!!! Spring
***/
//{{{
let spring = helix({
	axis: vector(0,0,-1),
	color: 0xFED162,
	coils: txtSpringCoils.value*1,
	radius: txtSpringRadius.value*1,
	length: txtSpringLength.value*1,
	opacity: 0.5
});
spring.head = sphere({
	opacity: 0.5,
	color: 0xffff00,
	radius: txtSpringRadius.value*0.2
});
spring.tip = sphere({
	opacity: 0.5,
	color: 0xff0000,
	//make_trail: true,
	radius: txtSpringRadius.value*0.2
});
//}}}
/***
!!! Mass -- rod
***/
//{{{
let rod = cylinder({
	axis: vector(txtRodLength.value*1,0,0),
	//color: 0xFED162,
	radius: txtRodRadius.value*1,
	//make_trail: false,
	opacity: 0.5
});
//}}}
/***
!!! Mass -- nuts
***/
//{{{
let nut = [];
for(let n=0; n<2; n++){
	nut[n] = cylinder({
		axis: vector(0.005,0,0),
		radius: txtRodRadius.value,
		make_trail: false,
		opacity: 0.7
	})
	if(n) nut[n].setColor(0xFFFF00);
}
//}}}
/***
! General
***/
//{{{
chkGravity.checked = chkGravity.disabled = chkCartesianComponents.disabled = true;
chkCartesian.checked = chkCartesianComponents.checked = false;
cartesian.hide();
scene.textBookView();
scene.camera.position.multiplyScalar(txtSpringRadius.value*20);
//}}}
/***
!! scene.init()
***/
//{{{
let __r = [], __v = [], __a = [], __tmpV = vector(),
	qrot = new $tw.threeD.THREE.Quaternion(), qdrot = new $tw.threeD.THREE.Quaternion();
scene.init = () => {
	spring.L0 = txtSpringLength.value*1;
	spring.setLength(spring.L0);
	//spring.getVertex(0,spring.head.position);
	spring.head.position.copy(spring.position);
	spring.getVertexWorld(-1,spring.tip.position);

	spring.position.z = spring.L0*2;
	spring.setRadius(txtSpringRadius.value*1);
	spring.setCoils(txtSpringCoils.value/Math.PI);
	spring.Kz = txtSpringKz.value*1;
	spring.Kphi = txtSpringKphi.value/Math.PI;
	spring.C = 45/180*Math.PI/spring.L0;
	spring.mass = +txtSpringMass.value;
	spring.I = spring.mass*Math.pow(spring.getRadius(),2);

	rod.setLength(txtRodLength.value*1);
	rod.setRadius(txtRodRadius.value*1);
	rod.mass = txtRodMass.value*1;
	rod.I = rod.mass*Math.pow(rod.getLength(),2)/12;

	if(!rod.velocity){
		rod.velocity = vector();
		rod.acceleration = vector();
		__r[0] = vector();
		__v[0] = vector();
		__a[0] = vector();
		rod.angle = vector();
		rod.angular_velocity = vector();
		rod.angular_acceleration = vector();
		__r[1] = vector();
		__v[1] = vector();
		__a[1] = vector();
	}else{
		rod.velocity.set(0,0,0);
		rod.acceleration.set(0,0,0);

		if(rod.angle.z){
			qrot.setFromAxisAngle(scene.zaxis,-rod.angle.z);
			rod.applyQuaternion(qrot);
			for(let n=0,N=nut.length; n<N; n++){
				nut[n].applyQuaternion(qrot);
				nut[n].position.applyQuaternion(qrot);
			}
		}

		rod.angular_velocity.set(0,0,0);
		rod.angular_acceleration.set(0,0,0);
	}

	//__r[0].copy(rod.position);
	__v[0].copy(rod.velocity);
	__a[0].copy(rod.acceleration);

	//__r[1].copy(rod.angle);
	__v[1].copy(rod.angular_velocity);
	__a[1].copy(rod.angular_acceleration);

	nut.mass = 0; nut.I = 0;
	rod.getAxis(__tmpV);
	for(let n=0,N=nut.length; n<N; n++){
		nut[n].setRadius(txtRodRadius.value*2);
		nut[n].position.copy(__tmpV).multiplyScalar(
			(n%2 ? 0.5 : -0.5)
		);
		nut[n].mass = rod.mass*(nut[n].getLength()/rod.getLength())*
						Math.pow(nut[n].getRadius()/rod.getRadius(),2);
		nut[n].I = nut[n].mass*Math.pow(rod.getLength()/2,2);
		nut.mass += nut[n].mass;
		nut.I += nut[n].I;
		if(!nut[n].velocity){
			nut[n].velocity = vector();
			nut[n].acceleration = vector();
			__r[2+n] = vector();
			__v[2+n] = vector();
			__a[2+n] = vector();
		}else{
			nut[n].velocity.set(0,0,0);
			nut[n].acceleration.set(0,0,0);
		}
		__v[2+n].copy(nut[n].velocity);
		__a[2+n].copy(nut[n].acceleration);
	}

	//spring.phi0 = txtPhi0.value*Math.PI/180;
	spring.Leq = spring.L0+(rod.mass+nut.mass)*scene.g.value/spring.Kz;
	spring.phi_eq = spring.C*(spring.Leq-spring.L0);

	rod.position.set(0,0,-spring.L0).add(spring.position);
	__r[0].copy(rod.position);

	rod.angle.set(0,0,spring.C*rod.position.z);
	__r[1].copy(rod.angle);
	if(rod.angle.z){
		qrot.setFromAxisAngle(scene.zaxis,rod.angle.z);
		rod.applyQuaternion(qrot);
	}

	for(let n=0,N=nut.length; n<N; n++){
		nut[n].position.add(rod.position);
		if(rod.angle.z){
			nut[n].applyQuaternion(qrot);
			nut[n].position.applyQuaternion(qrot);
		}
		__r[2+n].copy(nut[n].position);
	}
}
//}}}
/***
!! calcA()
***/
//{{{
let calcA = (r,v,t,a) => {
	a[0].copy(r[0]).sub(spring.position);
	let z = a[0].length() - spring.Leq,
		dphi = r[1].z - spring.phi_eq - spring.C * z,
		F_z = -spring.Kz * z,
		F_phi = spring.Kphi*spring.C*dphi;

	a[0].normalize().multiplyScalar((F_z+F_phi)/(rod.mass+nut.mass+spring.mass/3)).add(scene.g);
	a[1].set(0,0,-spring.Kphi*dphi/(spring.I+rod.I+nut.I));

	for(let n=0,N=nut.length; n<N; n++){
		a[2+n].copy(a[0]);
	}
	return a;
}
//}}}
/***
!! scene.update()
***/
//{{{
scene.update = (t_cur,dt) => {
	let e0 = +txtTolerance.value,
		adaptive = chkAdaptive.checked,
		// calculate next position using 4th order Runge-Kutta mehtod
		delta = $tw.numeric.ODE.nextValue(
			__r,__v,calcA,t_cur,dt,__a,adaptive,e0
		);
	if(adaptive) setdT(delta[2]);

	spring.setLength(__tmpV.copy(__r[0]).sub(spring.position).length());
	spring.getVertexWorld(-1,spring.tip.position);

	rod.position.copy(__r[0]);
	rod.velocity.copy(__v[0]);
	rod.acceleration.copy(__a[0]);

	let dphi = __r[1].z-rod.angle.z;
	if(dphi){
		// The meaning of "angle" in Quaternion for Object3D and Vector3 are different:

		//		Object3D.applyQuaternion() means to "change" the orientation of the Object3D
		// 		from its current orientation by that much angle.

		//		Vector3D.applyQuaternion() means to "set" the orientation of the Vector3D to
		//		that angle.

		// Therefore we need two quaternions, one for Object3D and the other for Vector3.
		//
		//													Vincent Yeh 2020/12/21

		qdrot.setFromAxisAngle(scene.zaxis,dphi);
		qrot.setFromAxisAngle(scene.zaxis,__r[1].z);
		rod.applyQuaternion(qdrot);
	}
	rod.angle.copy(__r[1]);
	rod.angular_velocity.copy(__v[1]);
	rod.angular_acceleration.copy(__a[1]);

	rod.getAxis(__tmpV);
	for(let n=0,N=nut.length; n<N; n++){
		//nut[n].position.copy(__r[2+n]);
		nut[n].position.copy(__tmpV).multiplyScalar(
			(n%2 ? 0.5 : -0.5)
		).add(rod.position);
		if(dphi){
			nut[n].applyQuaternion(qdrot);
			nut[n].position.applyQuaternion(qrot);
		}
		nut[n].velocity.copy(__v[2+n]);
		nut[n].acceleration.copy(__a[2+n]);
	}
}
//}}}
/***
!!!! scene.checkStatus
***/
//{{{
scene.checkStatus = () => {
	if(typeof chkMakeTrail !== 'undefined'){
		//getTrailParam(rod);
		for(let n=0,N=nut.length; n<N; n++){
			getTrailParam(nut[n]);
		}
	}
}
//}}}
!! Spring Length Control
L(m): <html><input type="number" title="Spring length, 0.01 to 0.5, step 0.01" id="txtSpringLength" min="0.01" max="0.5" step="0.01" value="0.15" style="width:50px"></html>
!! Spring Mass Control
\(m_s\)(kg): <html><input type="number" title="Spring mass, 0.001 to 0.5, step 0.001" id="txtSpringMass" min="0.005" max="0.5" step="0.001" value="0.01" style="width:50px"></html>
!! Spring Kz Control
\(k_z\) (N / m): <html><input type="number" title="Spring Kz, 0 to 200 step 1" id="txtSpringKz" min="0" max="200" step="1" value="10" style="width:45px"></html>
!! Spring Kphi Control
\(k_\phi\) (N \(\cdot\) m): <html><input type="number" title="Spring Kphi, 0 to 10 step 0.01" id="txtSpringKphi" min="0" max="10" step="0.01" value="0.2" style="width:55px"></html>\(/\pi\)
!! Spring Coil Control
Coils: <html><input type="number" title="Number of spring coils" id="txtSpringCoils" min="1" max="500" step="1" value="100" style="width:50px"></html>
!! Spring Radius Control
R(m): <html><input type="number" title="Spring radius, 0.005 to 0.1, step 0.001." id="txtSpringRadius" min="0.005" max="0.1" step="0.001" value="0.05" style="width:50px"></html>
|Wilberforce Pendulum Simulation|c
|width:45%;<<tw3DCommonPanel "Flow Control">>|width:45%;<<tw3DCommonPanel "Label Statistics">>|
|<<tw3DCommonPanel "Simulation Time Control">>|<<tw3DCommonPanel "Air Drag">>|
|<<tw3DCommonPanel "Center of Mass Control">> / <<tw3DCommonPanel "Linear Motion">> / <<tw3DCommonPanel "Cartesian System Control">>|<<tw3DCommonPanel "Trail Control">>|
|Spring <<tiddler "Wilberforce Pendulum Panel##Spring Mass Control">> / <<tiddler "Wilberforce Pendulum Panel##Spring Kz Control">> / <<tiddler "Wilberforce Pendulum Panel##Spring Kphi Control">>|Spring <<tiddler "Wilberforce Pendulum Panel##Spring Length Control">> / <<tiddler "Wilberforce Pendulum Panel##Spring Coil Control">> / <<tiddler "Wilberforce Pendulum Panel##Spring Radius Control">>|
|<<tiddler "Pendulum Panel##Rod Control">>|<<tw3DCommonPanel "Initial Phi">>|
|<<tw3DCommonPanel "Plot0 Control">> / <<tw3DCommonPanel "DAQ Control">> / <<tw3DCommonPanel "Gravity Control">><br><<twDataPlot id:dataPlot0>>|<<tw3DCommonPanel "Plot1 Control">> (<<tw3DCommonPanel "Label CPS">>) <<tw3DCommonPanel "Message">><br><<twDataPlot id:dataPlot1>>|
|>|<<tw3DScene [[Wilberforce Pendulum Creation]] [[Wilberforce Pendulum Initialization]] [[Wilberforce Pendulum Iteration]]>>|
/*{{{*/
.axis path,
.axis line {
	fill: none;
	stroke: black;
	shape-rendering: crispEdges;
}
.axis text {
	font-family: sans-serif;
	font-size: 11px;
}
/*}}}*/
{{Title{
單擺運動的一般性數值解法(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^
}}}
{{Affiliation{
# 國立東華大學物理學系
}}}
{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
普通物理教科書在討論單擺運動的時候,通常只討論__擺長及支點接固定__條線下的__平面擺動__以及__圓錐擺動__這兩種圓形軌跡的情況。對於非圓形軌跡的一般情況,我們會寫下它的 Lagrangian 來得到這一個情況下的幾條運動方程,每條方程僅一個變數,然後對這些方程進行求解。通常的情況下這些方程都很複雜,很難手動求解而需要以符號運算或是數值計算的方式來獲得結果。本篇文章以簡易的彈簧模型來計算單擺運動時擺繩的張力以及支點的受力,可以得到從任意初始條件開始之後擺錘的運動軌跡,其結果在平面擺動及圓錐擺動這兩種情況下非常符合已知的理論預期,可以合理相信一般情況下其結果也是正確的。
}}}
{{Section{
前言
}}}
{{Paragraph{
普通物理教科書中談到單擺的時候,基本上都是只談支點固定時的一維擺動,也就是限制在 \(x\)-\(y\) 平面上的情形,頂多談到圓錐擺,這兩種情況都是屬於相對容易處理的圓形軌跡。然而我們如果實際拿一個單擺來做實驗,可以很容易發現,當__初始速度隨意給定__的時候,絕大部分情況下擺錘的__軌跡不會剛好是圓形的__,而如果支點可以不固定,情況就更為複雜。像這樣的一般情況目前我們的做法都是寫下該情況的 Lagrangian 來得到該情況下對應各個變數的運動方程,並嘗試求解。 
}}}
{{Section{
原理
}}}
{{SubSection{
軌跡是圓形的時候
}}}
{{Paragraph{
一般人對於擺的現象並不會陌生,__最簡單的擺動是支點固定不動,擺繩長度不變,且擺動維持在同一平面(\(x\)-\(y\) 平面)__,此時對於擺錘而言,其受力基本上僅需考慮自身重力與擺繩的張力(圖(一)A,空氣阻力在這裡通常可以忽略不計):\[\vec F_\text{bob} = m_\text{bob}\vec g + \vec T,\] 其中擺繩的張力為 \begin{equation}\label{eq-T0}\vec T = \left(mg\cos\theta + m{v^2 \over r}\right)(-\hat r).\end{equation} 上式中 \(\vec r\) 為從支點到擺錘位置向量,\(\hat r\) 則為其單位向量。張力公式 (\ref{eq-T0}) 共有兩項,第一項是為了抵銷擺錘所受重力在擺繩方向的分力,第二項則是提供圓周運動所需的向心力(因為擺繩長度不變,且擺動維持在平面上,所以必然是圓周運動)。
}}}
{{Figure{
|multilined noborder|k
| [img(,20em)[https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Simple_gravity_pendulum.svg/450px-Simple_gravity_pendulum.svg.png]]  [img(,20em)[https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Conical_pendulum.svg/404px-Conical_pendulum.svg.png]] |
|圖一、單擺的兩種簡單情況(擺長維持不變,軌跡為圓形):A(左)平面擺動;B(右)圓錐擺動。(圖片來源 A:[[Wikipedia Pendulum|https://en.wikipedia.org/wiki/Pendulum]],B:[[Wikipedia Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]]。)|
}}}
{{Subparagraph{
__另一種簡單的情況是圓錐擺__(圖(一)B),擺錘的運動軌跡畫出一個正圓,擺繩與鉛錘線的夾角維持不變。此時擺繩張力的大小會同時滿足 \begin{eqnarray}\label{eq-T1-cent}T\sin\theta &=& {mv^2 \over L\sin\theta} \\ \label{eq-T1-mg}T\cos\theta &=& mg\end{eqnarray} 這兩條公式,其中 (\ref{eq-T1-cent}) 式表示擺繩張力的水平分量提供圓周運動所需的向心力,而 (\ref{eq-T1-mg}) 式則表示其垂直分量要與擺錘所受的重力相抵消。這兩條公式合併起來其實就是公式 (\ref{eq-T0}):\[T\sin^2\theta + T\cos^2\theta = {mv^2 \over L} + mg\cos\theta \quad \to \quad T = mg\cos\theta + {mv^2 \over L}.\] 另外,將 (\ref{eq-T1-cent}) 式除以 (\ref{eq-T1-mg}) 式還可以進一步得到擺繩與鉛直線夾角 \(\theta\) 與擺錘速率 \(v\) 之間的關係,\begin{equation}\label{eq-conic-v}\tan\theta = {v^2 \over Lg\sin\theta} \quad \to \quad v = \sqrt{Lg\sin\theta\tan\theta}.\end{equation}
}}}
{{SubSection{
軌跡不是圓形的時候
}}}
{{Paragraph{
在一般情況下只要讓擺錘初速度的 \(z\) 分量具有一個非 0 的值,擺動軌跡就不會僅限制在 \(x\)-\(y\) 平面上,且通常不會是正圓,甚至可以遠比圓形要複雜許多,如圖二便是一例。
}}}
{{Figure{
|multilined  noborder|k
|width:300pt;height:160pt; [img(250pt,150pt)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-x2.JPG]] |
| 圖二、一個具有非正圓軌跡的擺動範例。 |
}}}
{{SubSection{
a. 擺長維持不變
}}}
{{Paragraph{
x
}}}
{{Subparagraph{
x
}}}
{{Subparagraph{
x
}}}
{{Subparagraph{
x
}}}
{{Subparagraph{
<<<
上面的張力計算式是否會給出正確的結果?至少在圓形軌跡的情況下應該是,因為【擺繩長度維持不變】這個前提和【圓形軌跡】並不衝突,只要檢查
* 平面擺動
** 是否符合守恆的要求(軌跡重疊、動能位能)
** 週期是否符合公式 \begin{equation}\label{eq-P0}T_\text{ime} = 2\pi \sqrt{L \over g}\left(1+{1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4+\cdots\right)\end{equation} (參考 [[維基百科 Pendulum|https://en.wikipedia.org/wiki/Pendulum]])
* 圓錐擺動
** 是否符合守恆的要求(軌跡重疊、動能位能)
** 是否符合公式 (\ref{eq-T1-cent}) 和 (\ref{eq-T1-mg} )
** 是否符合週期公式 \begin{equation}\label{eq-P1}T_\text{ime} = 2\pi\sqrt{L\sin\theta \over g\tan\theta} = 2\pi\sqrt{L\cos\theta \over g}\end{equation}(參考 [[維基百科 Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]])
<<<
}}}
{{Subparagraph{
x
}}}
{{SubSection{
b. 擺長可以改變
}}}
{{Paragraph{
如果擺繩的長度會改變呢?...
}}}
{{Subparagraph{
x
}}}
{{Subparagraph{
x
}}}
{{Subparagraph{
x
}}}
{{Subparagraph{
{{{
dlmax = fL / rod.k
if dlmax < dL:
	# We have a hard rod.
	Tension += fL
	rod.length += dlmax
else:
	# We have a soft rod.
	Tension += rod.k*dL
	rod.length += dL
}}}
}}}
{{Section{
結果與討論
}}}
{{Paragraph{
根據上述公式 (\ref{eq-tension0}),(\ref{eq-tension1-1}),(\ref{eq-tension1-2}) 寫出程式碼之後進行測試,並以 \(dt = 0.001\) 秒的時間間隔,採用''[[四階 Runge-Kutta 方法]]求解運動方程'',其結果如下:
# ''平面擺動'' 將初速度設為 \((0,0,0)\),擺動的軌跡就會是平面上的一段圓弧,如圖三及表一所示。
** 圖三顯示的是 50 個週期的軌跡,從右邊的局部放大圖來看,50 個週期的軌跡幾乎都重疊在一起,表示運動方程的解是可靠的,也就是依照公式 (\ref{eq-tension0}) 所計算的張力是可靠的。
** 表一顯示擺動週期隨初始角度的結果,以 10 個週期的平均值和 (\ref{eq-P0}) 式相比,差異均小於 \(0.02 \%\),表示模擬的結果是正確的。
# ''圓錐擺動'' 將初速度的 \(z\) 分量設成如 (\ref{eq-conic-v}) 式的數值,就會是圓錐型擺動,如圖四及表二所示。
** 圖四顯示的是 50 個週期的軌跡,左圖顯示的確是圓錐擺動的情形,而中間及右邊是左邊的局部放大圖。兩張放大圖的差異為:中間是依照本文討論之公式 (\ref{eq-tension0}) 計算張力,而右圖則是依照圓錐擺動公式 (\ref{eq-T1-mg}) 計算張力。兩圖的結果顯示兩種張力公式計算結果並無明顯差異,表示本文所討論的張力計算公式是符合預期的。
** 表二顯示擺動週期隨初始角度的結果,以 10 個週期的平均值和 (\ref{eq-P1}) 式相比,差異均小於 \(0.02 \%\),表示模擬的結果是正確的。
}}}
{{Figure{
|multilined  noborder|k
|height:150pt; [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-planar.JPG]] [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-planar-zoom.JPG]] |
|圖三、初速度為 \((0,0,0)\) 的模擬結果中前 50 個週期之擺錘軌跡軌跡圖,其中右圖為左圖之局部放大,圖中可見全部 50 個週期之軌跡幾乎重疊在一起,顯示運動方程的解是可靠的,也就是依照公式 (\ref{eq-tension0}) 所計算的張力是可靠的。模擬參數:擺長 2.4m,初始角度 10&deg;,時間間隔 \(dt = 0.001\) 秒。|
}}}
{{Figure{
| spreadsheet|k
|表一、平面擺動模擬的前 10 個週期紀錄(單位:sec),其平均值與理論值的差異均小於 \(0.04 \%\)|c
|| 1 | =B0+1 | =C0+1 | =D0+1 | =E0+1 | =F0+1 | =G0+1 | =H0+1 | =I0+1 | =J0+1 | Avg | Theory | Error |h
| 5&deg; | 3.110| 3.111| 3.111| 3.111| 3.111| 3.111| 3.110| 3.111| 3.111| 3.111| ''=round(avg(B1:K1),3)''| 3.110850| ''=round((val(L1)-val(M1))/M1*100,3) %''|
| 10&deg; | 3.115| 3.115| 3.115| 3.116| 3.115| 3.115| 3.116| 3.115| 3.115| 3.116| ''=round(avg(B2:K2),3)''| 3.115291| ''=round((val(L2)-val(M2))/M2*100,3) %''|
| 15&deg; | 3.122| 3.123| 3.123| 3.122| 3.123| 3.123| 3.123| 3.122| 3.123| 3.123| ''=round(avg(B3:K3),3)''| 3.122695| ''=round((val(L3)-val(M3))/M3*100,3) %''|
| 20&deg; | 3.133| 3.133| 3.133| 3.133| 3.134| 3.133| 3.133| 3.133| 3.133| 3.134| ''=round(avg(B4:K4),3)''| 3.133065| ''=round((val(L4)-val(M4))/M4*100,3) %''|
| 25&deg; | 3.146| 3.147| 3.147| 3.147| 3.146| 3.147| 3.147| 3.147| 3.146| 3.147| ''=round(avg(B5:K5),3)''| 3.146406| ''=round((val(L5)-val(M5))/M5*100,3) %''|
| 30&deg; | 3.163| 3.164| 3.163| 3.164| 3.163| 3.164| 3.163| 3.164| 3.163| 3.164| ''=round(avg(B6:K6),3)''| 3.162725| ''=round((val(L6)-val(M6))/M6*100,3) %''|
|>|>|>|>|>|>|>|>|>|>|>|>|>|Theoretical value is calculated with eq( \ref{eq-P0} )|
}}}
{{Figure{
|multilined  noborder|k
|height:300pt; [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical.JPG]] [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-zoom.JPG]]<br>[img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-eq3-zoom.JPG]] [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-eq1-zoom.JPG]] |
|圖四、初速度為 \((0,0,v_z)\),其中 \(v_z\) 代入圓錐擺動公式 (\ref{eq-conic-v}) 所計算的值,預期結果應為圓錐擺動。左圖為模擬結果的前 50 個週期之擺錘軌跡軌跡圖,顯示的確為圓錐擺動的狀況。中右二圖為左圖之局部放大,中間是依照本文討論之公式 (\ref{eq-tension0}) 計算張力,而右圖則是依照圓錐擺動公式 (\ref{eq-T1-mg}) 計算張力。兩張放大圖的結果顯示兩種張力公式計算結果並無明顯差異,表示本文所討論的張力計算公式是符合預期的。模擬參數:擺長 2.4m,初始角度 10&deg;,時間間隔 \(dt = 0.001\) 秒。|
}}}
{{Figure{
| spreadsheet|k
|表二、圓錐擺動模擬的前 10 個週期紀錄(單位:sec),其平均值與理論值的差異均小於 \(0.07 \%\)|c
|| 1 | =B0+1 | =C0+1 | =D0+1 | =E0+1 | =F0+1 | =G0+1 | =H0+1 | =I0+1 | =J0+1 | Avg | Theory | Error |h
| 5&deg; | 3.103| 3.103| 3.104| 3.103| 3.104| 3.103| 3.104| 3.103| 3.104| 3.103| ''=round(mean(B1:K1),3)''| 3.103449| ''=round((val(L1)-val(M1))/M1*100,3) %''|
| 10&deg; | 3.085| 3.085| 3.086| 3.085| 3.086| 3.086| 3.086| 3.086| 3.086| 3.086| ''=round(mean(B2:K2),3)''| 3.085661| ''=round((val(L2)-val(M2))/M2*100,3) %''|
| 15&deg; | 3.055| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| ''=round(mean(B3:K3),3)''| 3.055937| ''=round((val(L3)-val(M3))/M3*100,3) %''|
| 20&deg; | 3.014| 3.014| 3.014| 3.014| 3.014| 3.014| 3.015| 3.014| 3.014| 3.014| ''=round(mean(B4:K4),3)''| 3.014154| ''=round((val(L4)-val(M4))/M4*100,3) %''|
| 25&deg; | 2.96| 2.96| 2.96| 2.96| 2.96| 2.96| 2.96| 2.94| 2.96| 2.96| ''=round(mean(B5:K5),3)''| 2.960127| ''=round((val(L5)-val(M5))/M5*100,3) %''|
| 30&deg; | 2.893| 2.894| 2.893| 2.894| 2.893| 2.894| 2.894| 2.893| 2.894| 2.893| ''=round(mean(B6:K6),3)''| 2.893595| ''=round((val(L6)-val(M6))/M6*100,3) %''|
|>|>|>|>|>|>|>|>|>|>|>|>|>|Theoretical value is calculated with eq( \ref{eq-P1} )|
}}}
{{Paragraph{
如果讓擺繩具有線性回復力 \(f_k = -k\Delta l\),那麼擺錘在半徑方向會有簡諧震盪的行為,週期為 \[T_\text{radial} = {2\pi \over \omega} = {2\pi \sqrt{m \over k}}.\] 如果我們進一步讓這個週期跟擺動的週期重疊,也就是 \[2\pi\sqrt{m \over k} = T_\text{swing} \quad \to \quad k = {4\pi^2 m \over T_\text{swing}^2}.\] 如果是平面擺動,那就是 \[k = {mg \over L}{1 \over \left(1+{1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4+\cdots\right)^2};\] 如果是圓錐擺動則為 \[k = {mg \over L\cos\theta}.\] 這種情況下軌跡會如何呢?
}}}
{{Section{
結論
}}}
{{Paragraph{
我們使用固定大小且等同於慣性趨勢效果的假想力來討論擺繩的張力,可以得到當支點固定且擺繩只有徑向變化時擺繩張力的一般式,在圓形軌跡的情況下(平面擺動以及圓錐擺動)的模擬結果符合預期。此結果可以用來模擬這種條件下任意初始條件的擺動,不只限於圓形軌跡的情況。
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# Wikipedia [[Pendulum|https://en.wikipedia.org/wiki/Pendulum]], [[Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]]
# [[The Not-So-Simple Pendulum|http://aapt.scitation.org/doi/abs/10.1119/1.2344202?journalCode=pte]]
# http://aapt.scitation.org/doi/abs/10.1119/1.2060649
# [[Approximation Solution of ...|https://www.sciencedirect.com/science/article/pii/S089812211200017X]]
# [[A New Twist for Conical Pendulum|http://aapt.scitation.org/doi/abs/10.1119/1.880112?journalCode=pte]]
# http://iopscience.iop.org/article/10.1088/0031-9120/35/6/309/meta
# http://iopscience.iop.org/article/10.1088/0143-0807/30/6/L01/pdf
# https://www.sciencedirect.com/science/article/pii/0020746279900349
# http://aapt.scitation.org/doi/pdf/10.1119/1.1457824
# https://www.sciencedirect.com/science/article/pii/0375960196006196
# [[Effect of the mass of the cord|http://aapt.scitation.org/doi/abs/10.1119/1.10378]]
! 針對一階微分方程的四階 ~Runge-Kutta 方法
在 [[微分方程的數值求解]] 這篇講義中,我們提到關鍵點在於如何估計出「能減少誤差累積的斜率」,目前已知的方法不只一個,其中「四階 ~Runge-Kutta 方法」可能是 __夠實用__ 的方法中 __最簡單__ 的一個,其想法基本上是【''先推測幾個中間位置的斜率,和端點的斜率一起求出某種加權平均值,再用此平均斜率來推測下一個位置的值''】,至於各點斜率的權重則根據泰勒展開式來計算出。以最常用的四階做法來說,取兩個中間位置和兩個端點(現在位置及下一個位置)的斜率來求平均,這樣的做法經常出現兩個中間值一個是高估而另一個是低估的現象,使得誤差不至於快速累積,因而廣為使用。其公式並不困難,若按照 [[維基百科 The Runge-Kutta Method|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]] 網頁的寫法,我們__需要先定義一次微分的函數__以及__初始條件__:\[\begin{aligned}\dot y = f(t, y), \quad y(t_0) = y_0,\end{aligned}\] 並選擇適當的時間間隔 \(dt\) 進行迭代計算,\begin{aligned}y_{n+1} &= y_n + {dt \over 6}(k_1 + 2k_2 + 2k_3 + k_4) \\ t_{n+1} &= t_n + dt,\end{aligned} 其中 \begin{aligned} k_1 &= f(t_n, y_n) & 現在位置的斜率 \\ k_2 &= f\left(t_n+{dt \over 2},y_n+{dt \over 2}k_1\right), & 第一個中間點的斜率 \\ k_3 &= f\left(t_n+{dt \over 2},y_n+{dt \over 2}k_2\right), \quad & 第二個中間點的斜率 \\ k_4 &= f\left(t_n+dt,y_n+dtk_3\right). & 下一個位置的斜率 \end{aligned} 寫成程式碼也並不困難。
> @@color:red;''注意:''@@@@下列程式碼''未''經實際測試,除了最後的【針對二階微分方程的多粒子 1D 及 3D Javascript 版本】。@@
!!! 單粒子版本
對於單一粒子的一維運動,程式碼極為簡短:<<slider 'rk4vp' 'Runge-Kutta 1 Python##Single Particle 1D Python' 'Python codes: Single Particle'>>
!!! 多粒子版本
真正實用上我們經常需要處理多粒子的問題,可以使用陣列(array)來做多粒子的計算:<<slider 'rk4ap' 'Runge-Kutta 1 Python##Many Particles 1D Python' 'Python codes: Many Particles'>>
! 針對二階微分方程的四階 ~Runge-Kutta 方法
上面的範例都是__解一階微分方程的__(上面 (1) 式的定義是一階微分),而''物理上的微分方程經常都是二階的'',如牛頓第二定律、薛丁格方程等,所以要修改迭代過程使得一階微分以及函數本身可以在同一個副程式裡解出。

至於二階微分方程如何求解,有興趣者請參考 [[四階 Runge-Kutta 方法 -- 二階微分方程]]。
!! 參考文獻
# 維基百科 [[Runge-Kutta 網頁|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]]
# [[RK4 for 2nd order ODE|https://www.youtube.com/watch?v=smfX0Jt_f0I&feature=youtu.be&t=325]]
! 針對二階微分方程的四階 ~Runge-Kutta 方法
對二階微分方程求解,我們按照 [[四階 Runge-Kutta 方法]] 裡面求解一階微分方程的過程,''同樣先定義出二階微分的函數''以及''初始條件'':\[\begin{aligned}\ddot y = f(y, \dot y, t), \quad y(t_0) = y_0, \quad \dot y(t_0) = \dot y_0.\end{aligned}\]
>注意上面這個二階微分 \(\ddot y\) 在一般情況會是 \(y\) 及 \(\dot y\) 的函數,對一個力學系統來說,通常我們能夠知道【受力】,而根據牛頓第二定律,知道受力就可以算出【加速度】,而加速度正是位置的二次微分,這樣我們就知道
>>對力學系統而言上面 (2) 式中的 \(y\) 就是位置 \(\vec r\),而
>> \(\dot y\) 就是速度 \(\vec v\),
>>\(\ddot y = f(y,\dot y,t)\) 算出來的就是加速度 \(\vec a\),而加速度會是位置和速度的函數。
>物理上什麼情況下加速度是位置的函數呢?舉一個簡單的例子:彈簧-質量系統,在此系統裡物體的受力為 \(-kx\),其中的 //x// 就是位置(相對於平衡點),也就是說【受力和位置有關】,或者說【受力是位置的函數】,而受力可以讓我們算出加速度,所以理所當然【加速度是位置的函數】。
>
>至於什麼情況下加速度會是速度的函數呢?只要受力是速度的函數就是了,比如說空氣阻力 \(\vec f = -{1 \over 2} \rho v^2 C_{D}A \hat v\) 就是一個典型的例子。

{{FrameHalfRight{[>img(100%,)[image/teaching/RK4 for 2ndODE.JPG]]
來源:由 [[此影片|https://youtu.be/smfX0Jt_f0I?t=325]] 截出
}}}
接著按照右圖的做法寫成程式碼:
!!! 單粒子版本
對【單粒子一維運動】來說程式碼也不算複雜:<<slider 'rk42vp' 'Runge-Kutta 2 Python##Single Particle 3D/1D Python' 'Python codes: Single Particle'>>。
!!! 多粒子版本
@@更貼近實際的話我們應該是需要多粒子的版本:<<slider 'rk42vap' 'Runge-Kutta 2 Python##Many Particles 3D/1D Python' 'Python codes: Many Particles(未測試)'>>。@@
!! 參考文獻
# 維基百科 [[Runge-Kutta 網頁|https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods]]
# [[RK4 for 2nd order ODE|https://www.youtube.com/watch?v=smfX0Jt_f0I&feature=youtu.be&t=325]]
! 微分方程
__物理上的動力學過程經常都是以微分方程來表示的__,例如描述日常生活可見的運動過程之牛頓第二運動定律(Newton's Second Law of Motion) \[f = ma = m{d^2x \over dt^2} \quad \text{(1D)}, \quad or \quad \vec f = m\vec a = m{d^2\vec r \over dt^2} \quad \text{(3D)}.\] 就是許多人學到的第一條微分方程。又比如說描述真空中電磁場狀況的馬克斯威爾方程(Maxwell's Equations) \[\begin{eqnarray*}\vec\nabla & \cdot & \vec E &=& {\rho \over \epsilon_0} \\ \vec\nabla & \times & \vec E &=& -{\partial \vec B \over \partial t} \\ \vec\nabla & \cdot & \vec B &=& 0 \\ \vec\nabla & \times & \vec B &=& \mu_0 \vec j + \mu_0\epsilon_0{\partial \vec E \over \partial t},\end{eqnarray*}\] 是許多理工科系學生都要學會的微分方程。因為如此,我們在解物理問題的時候都是求解相關的微分方程,不論是以解析或是數值方法進行,解出微分方程就是解決了那一個物理問題。

物理上微分方程的變數通常是【位置 \(\vec r\)】或者【時間 \(t\)】,簡單起見我們以時間 \(t\) 為變數來說明。

! 數值方法求解 -- 一階微分方程
以數值方法解一階微分方程的想法其實很簡單,基本上就是
# 要先知道一階微分方程本身 \[{dy \over dt} = f(y,t),\] 其中 \(f(y,t)\) 這個函數的形式必須為已知的。
# 要先知道初始時間 \(t_0\) 的函數值 \(y_0\)。
# 從初始時間開始,一次做一小步(\(t_{n+1} = t_n + dt\))。
# 每一小步都用 \[\begin{equation}y_{n+1} = y_n + \left({dy \over dt}\right)_{t=t_n}dt = y_n + f(y_n,t_n) dt\end{equation}\] 來估計下一步的值。
我們如果一步一步來看,從初始時間開始 \(t_0, y_0\) 為已知,那麼下一步的估計值就是 \[y_1 = y_0 + f(y_0, t_0)dt,\] 再下一步的估計值就是 \[y_2 = y_1 + f(y_1, t_1)dt.\] 這裡應該可以很快看出來,就只是【把現在的 \(y\) 與 \(t\) 代入一階微分的函數 \(f(y,t)\)】去計算出【現在的斜率】,然後用現在的值加上「斜率 &times; 時間間隔」來估計下一步的值。

!! 誤差
這個概念非常簡單而且直接,要寫出程式碼並不困難,問題在於【計算成本】的考量,每一小步的時間 \(dt\) ''不可能''無限小,這樣一來估計出來的下一步值跟實際應該有的值之間就會有所差異。

對每一小步來講,這個差異並不會很大,只要我們選擇的步伐 \(dt\) 仍然夠小就可以。但是數值計算會讓誤差持續累積,每一步的微小差異都會累積到下一步去,計算夠久之後誤差就可能大到離譜的程度!

所以__數值方法求解微分方程的過程中,如何''減少誤差累積''是至關重要的事情__。

仔細看上面的(1)式,不難發現整個過程的''重點就在於【計算出現在的斜率】'',也就是說,如何估計出一個「''能減少誤差累積的斜率''」,是數值計算的關鍵!

目前最普遍採用的方法,可能是「[[四階 Runge-Kutta 方法]]」,它不見得是誤差最小的方法,但應該是誤差夠小的實用方法中最簡單的一個。
在處理力學相關問題時,如果一個系統的運動方程是未知的,則我們通常會使用拉格朗日方法(Lagrange's Method)來獲得,它的 [[背後想法|拉格朗日方法的原理]] 有點抽象,但做法上卻很明確而直接,只有兩個步驟:
# 寫下系統的拉格朗日量(Lagrangian) \[\begin{equation}L(q, \dot q, t) = T(\dot q) - U(q),\end{equation}\] 其中 \(q\) 通常是系統位置相關的變數,例如某個座標軸方向上的位置 \(x\)、\(y\)、\(z\),或者是角度 \(\theta\)、\(\phi\) 等等;\(T\) 為系統的總動能,而 \(U\) 為系統的總位能。
** 通常動能是速度 \(\boxed{\dot q \equiv dq/dt}\) 的函數,而位能是位置變數 \(q\) 的函數,因此拉格朗日量會是速度及位置的函數。
** 當然一般情況下也可以是時間的函數,所以我們把它寫成 \(L = L(q, \dot q, t)\)。
# 系統的運動方程為(如果沒有耗散的話) \[\begin{equation}{d \over dt}{\partial L \over \partial \dot q} - {\partial L \over \partial q} = 0.\end{equation}\]
!! 範例一、彈簧系統
我們先用一個簡單且已知結果的系統,彈簧質量系統,當範例來看看如何使用拉格朗日法,這個系統的運動方程我們已經知道是 \[\begin{equation}f = -kx \quad \to \quad \boxed{m\ddot x = -kx},\end{equation}\] 拿它來做練習可以很容易確定有沒有做對。

我們按照上面的步驟來進行:
# 按照 (1) 式寫下拉格朗日量,這裡位置相關的變數就很簡單只有 \(x\) 而已,\[L = T - U = {1 \over 2}m\dot x^2 - {1 \over 2}kx^2.\]
# 按照 (2) 式寫下運動方程 \[\begin{eqnarray}{d \over dt}{\partial L \over \partial \dot x} - {\partial L \over \partial x} &=& 0 \nonumber \\ {d \over dt}m\dot x - (-kx) &=& 0 \nonumber \\ m\ddot x + kx &=& 0.\end{eqnarray}\]
比較 (4) 式和 (3) 式我們知道沒有做錯。對於彈簧質量系統這個過程很容易,對吧!
!! 範例二、擺
接下來我們再看一個範例:擺,這個系統的運動方程也是已知的 \[\begin{equation}\ddot\theta = -{g \over l}\sin\theta.\end{equation}\] 我們一樣按照上述步驟:
# 按照 (1) 式寫下拉格朗日量,這裡的位置相關變數只有 \(\theta\) 而已,\[L = T - U = {1 \over 2}ml^2\dot\theta^2 - mg(1-\cos\theta).\]
# 按照 (2) 式寫下運動方程 \[\begin{eqnarray}{d \over dt}{\partial L \over \partial \dot\theta} - {\partial L \over \partial \theta} &=& 0 \nonumber \\ {d \over dt}(ml^2\dot\theta) - mgl(-\sin\theta) &=& 0 \nonumber \\ ml^2\ddot\theta + mgl\sin\theta &=& 0 \nonumber \\ \ddot \theta + {g \over l}\sin\theta &=& 0.\end{eqnarray}\]
比較 (6) 式和 (5) 式我們知道沒有做錯。對於擺而言這個過程也是很容易的,對吧!
!! 一般情況
上面兩個簡單的範例應該可以讓我們了解到如何使用拉格朗日法,對於實際上無法或者不容易事先知道運動方程的系統,此方法為目前公認得到運動方程的直接方法,有了運動方程我們就可以對其求解(如 [[微分方程的數值求解]]),進而了解該系統的運動狀況。
參考 [[維基百科拉格朗日力學(簡略)|https://zh.wikipedia.org/wiki/%E6%8B%89%E6%A0%BC%E6%9C%97%E6%97%A5%E5%8A%9B%E5%AD%A6]] 或 [[Wikipedia Lagrangian mechanics page(詳細)|https://en.wikipedia.org/wiki/Lagrangian_mechanics]]
{{Title{
擺繩張力的運動學觀點(撰寫中)
}}}
{{Author{
葉旺奇 ^^1^^
}}}
{{Affiliation{
# 國立東華大學物理學系
}}}
{{Address{
Contact: wcy2@gms.ndhu.edu.tw
}}}
{{Abstract{
普通物理教科書在討論擺動的時候,通常只討論平面擺動以及圓錐擺動兩種情況,因為這兩種情況軌跡都是圓形的,我們對其動力學(kinematics)結果已經瞭若指掌,且這種情況能夠讓我們動手計算。如果我們使用電腦數值模擬來討論擺動的話,可以討論更為一般情況而不僅限於圓形軌跡,不過如果討論的不是圓形軌跡,其動力學結果不像圓形軌跡般已經很清楚,因此本篇文章嘗試從運動學(dynamics)觀點出發,在沒有預設軌跡形狀的前提下,以固定大小(零階)的力模型來討論單擺運動中擺繩張力如何計算,由此可以推論出當支點固定,擺繩長度亦不變的時候擺繩的張力大小為(以支點為原點) \[T = mg\cos\theta + {2m \over dt^2}(|\vec r + \vec vdt|-r),\] 其中 \({2m \over dt^2}(|\vec r + \vec vdt|-r)\) 為擺錘的慣性趨勢對擺繩產生的徑向拉力,其效果等同於 \(mv^2 \over r\)。當擺繩長度可變(但僅包含擺繩縱向變化)且具有線性回復力 \(k\Delta l\) 時,擺繩的張力大小即為擺繩的回復力 \[T = k\Delta l.\]。
}}}
{{Section{
前言
}}}
{{Paragraph{
普通物理教科書中談到單擺的時候,基本上都是只談一維擺動,也就是限制在 \(x\)-\(y\) 平面上的情形,頂多談到圓錐擺,這兩種都是屬於圓形軌跡,因為圓形軌跡相對容易處理。然而我們可以很合理預期,當一個擺從任意初始狀態開始的時候,絕大部分情況下不會剛好是圓形軌跡,此時該如何計算擺繩的張力,是需要仔細討論的。本篇文章即試著討論在【支點固定】以及【擺繩只有徑向改變】的情況下如何模擬擺的任意運動。
}}}
{{Section{
原理
}}}
{{Subsection{
軌跡是圓形的時候
}}}
{{Paragraph{
一般人對於擺的現象並不會陌生,__最簡單的擺動是支點固定不動,擺繩長度不變,且擺動維持在同一平面(\(x\)-\(y\) 平面)__,此時對於擺錘而言,其受力基本上僅需考慮自身重力與擺繩的張力(圖(一)A,暫時先忽略空氣阻力):\[\vec F_\text{bob} = m_\text{bob}\vec g + \vec T,\] 其中擺繩的張力為 \begin{equation}\label{eq-T0}\vec T = \left(mg\cos\theta + m{v^2 \over r}\right)(-\hat r).\end{equation} 上式中 \(\vec r\) 為從支點到擺錘位置向量,\(\hat r\) 則為其單位向量。張力公式 (\ref{eq-T0}) 共有兩項,第一項是為了抵銷擺錘所受重力在擺繩方向的分力,第二項則是提供圓周運動所需的向心力(因為擺繩長度不變,且擺動維持在平面上,所以必然是圓周運動)。
}}}
{{Figure{
|multilined noborder|k
| [img(,20em)[https://upload.wikimedia.org/wikipedia/commons/thumb/b/b2/Simple_gravity_pendulum.svg/450px-Simple_gravity_pendulum.svg.png]]  [img(,20em)[https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Conical_pendulum.svg/404px-Conical_pendulum.svg.png]] |
|圖一、單擺的兩種簡單情況(擺長維持不變,軌跡為圓形):A(左)平面擺動;B(右)圓錐擺動。(圖片來源 A:[[Wikipedia Pendulum|https://en.wikipedia.org/wiki/Pendulum]],B:[[Wikipedia Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]]。)|
}}}
{{Subparagraph{
__另一種簡單的情況是圓錐擺__(圖(一)B),擺錘的運動軌跡畫出一個正圓,擺繩與鉛錘線的夾角維持不變。此時擺繩張力的大小會同時滿足 \begin{eqnarray}\label{eq-T1-cent}T\sin\theta &=& {mv^2 \over L\sin\theta} \\ \label{eq-T1-mg}T\cos\theta &=& mg\end{eqnarray} 這兩條公式,其中 (\ref{eq-T1-cent}) 式表示擺繩張力的水平分量提供圓周運動所需的向心力,而 (\ref{eq-T1-mg}) 式則表示其垂直分量要與擺錘所受的重力相抵消。這兩條公式以 (\ref{eq-T1-cent}) &times; \(\sin\theta\) + (\ref{eq-T1-mg}) &times; \(\cos\theta\) 方式合併起來其實就是公式 (\ref{eq-T0}):\[T\sin^2\theta + T\cos^2\theta = {mv^2 \over L} + mg\cos\theta \quad \to \quad T = mg\cos\theta + {mv^2 \over L}.\]
}}}
{{Subsection{
軌跡不是圓形的時候
}}}
{{Paragraph{
在一般情況下只要讓擺錘初速度的 \(z\) 分量具有一個非 0 的值,擺動軌跡就不會僅限制在 \(x\)-\(y\) 平面上,且通常不會是正圓,甚至可以遠比圓形要複雜許多,如圖二便是一例。這種情況下上述的 (\ref{eq-T0})-(\ref{eq-T1-mg}) 式都無法算出正確的張力,沒有正確的張力就無法正確解出運動方程,因此我們要仔細討論一般情況下擺繩張力該如何計算,底下我們將分別就【擺長維持不變】以及【擺長可以改變】這兩類情況加以討論。
}}}
{{Figure{
|multilined  noborder|k
|width:300pt;height:160pt; [img(250pt,150pt)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-x2.JPG]] |
| 圖二、一個具有非正圓軌跡的擺動範例。 |
}}}
{{Subsection{
a. 擺長維持不變
}}}
{{Paragraph{
擺繩之所以會有張力,是因為擺錘給它拉力,而這拉力的來源有兩種:擺錘的重力以及慣性,可以分開來討論。重力部分在很多普通物理教學內容裡都有討論,這裡不再詳述,僅就慣性部分加以討論。
}}}
{{Subparagraph{
我們想像在某一個瞬間,擺錘正在運動(\(\vec v \ne 0\)),假如這時擺繩突然斷開,則擺錘在接下來一段很短的時間 \(dt\) 之內,由於慣性作用會沿著現在的速度 \(\vec v\) 方向前進,產生一個小小的位移 \[d\vec r = \vec v dt,\] 這個位移會讓擺錘與支點(pivot)的距離改變 \[dL = |(\vec r + d\vec r - \vec r_p)|-|\vec r-\vec r_p|,\] 其中 \(\vec r\) 是擺錘現在的位置,\(\vec r_p\) 是支點的位置,而 \(|\vec r - \vec r_p|\) 是擺繩現在的長度。這個改變可能是拉長(\(dL > 0\))或者是縮短(\(dL < 0\)),不過為了簡便,底下討論多使用【拉長】這個動詞,而擺錘與支點距離是增加或是減少則由 \(dL\) 的正負號來決定。
}}}
{{Subparagraph{
在擺繩沒有斷開的情況下,擺錘當然不會產生這樣的位移,不過上面的討論可以解讀成【擺錘的慣性有將擺繩拉長 \(dL\) 的趨勢】,也就是說,這個慣性趨勢會對擺繩產生徑向拉力 \(f_{L}\),我們將之稱為【慣性拉力】(俗稱離心力)。這個拉力的反作用力,就是擺繩給擺錘的張力(之一部份,另一部份由重力產生)。這個拉力的大小如何估計?我們可以試著從動量變化來看。
}}}
{{Subparagraph{
雖然說擺錘之所以會有將擺繩拉長 \(dL\) 的趨勢,實際上是慣性作用的結果,但要討論力,我們還是借用離心力這個假想力比較容易。假想沒有慣性作用,取而代之是一個離心力 \(f_{L}\) 作用在擺錘上,其效果等同於慣性作用的效果,也就是這個離心力如果單獨存在,將會在 \(dt\) 的時間之內讓擺錘的徑向位移達到 \(dL\) 。另外,由於有作用力(固定大小的離心力),擺錘在 \(dt\) 時間內的徑向速率會從 \(0\) 增加到 \(v_{L}\),且這個末速率應該是平均速率的兩倍,也就是 \(v_{L} = 2dL/dt\),因此這段時間擺錘的動量變化率,也就是受力應為 \[f_{L} = {dp \over dt} = {d \over dt}{2mdL \over dt} = {2mdL \over dt^2}.\] 換句話說,由於擺錘慣性拉力引起的擺繩張力應為 \[f_{L} = {2mdL \over dt^2} = {2m \over dt^2}(|(\vec r + \vec vdt - \vec r_p)|-|\vec r - \vec r_p|),\] 加上重力產生的 \(mg\cos\theta\) 便是擺繩的張力:\begin{equation}\label{eq-tension0}\boxed{T_\text{ension} = mg\cos\theta + {2m \over dt^2}(|(\vec r + \vec vdt - \vec r_p)|-|\vec r - \vec r_p|),}\end{equation} 方向由擺錘指向支點。
}}}
{{Subparagraph{
<<<
上面的張力計算式是否會給出正確的結果?至少在圓形軌跡的情況下應該是,因為【擺繩長度維持不變】這個前提和【圓形軌跡】並不衝突,只要檢查
* 平面擺動
** 是否符合守恆的要求(軌跡重疊、動能位能)
** 週期是否符合公式 \begin{equation}\label{eq-P0}T_\text{ime} = 2\pi \sqrt{L \over g}\left(1+{1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4+\cdots\right)\end{equation} (參考 [[維基百科 Pendulum|https://en.wikipedia.org/wiki/Pendulum]])
* 圓錐擺動
** 是否符合守恆的要求(軌跡重疊、動能位能)
** 是否符合公式 (\ref{eq-T1-cent}) 和 (\ref{eq-T1-mg} )
** 是否符合週期公式 \begin{equation}\label{eq-P1}T_\text{ime} = 2\pi\sqrt{L\sin\theta \over g\tan\theta} = 2\pi\sqrt{L\cos\theta \over g}\end{equation}(參考 [[維基百科 Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]])
<<<
}}}
{{Subparagraph{
如果上述狀況都確認是正確的,那麼其它任何情況(同樣維持支點及擺繩長度不變)也應該是正確的。
}}}
{{Subsection{
b. 擺長可以改變
}}}
{{Paragraph{
如果擺繩的長度會改變呢?首先我們先限制只談在彈性限度內的情況,這樣的話若繩長改變 \(\Delta l\) 就會產生線性回復力 \(k\Delta l\),其中 \(k\) 是彈性係數,這個回復力也會貢獻到張力的一部分。再來我們看上面所討論的慣性拉力 \(f_{L}\),這個拉力應該會真的在 \(dt\) 時間內將擺繩長度改變一些,我們以擺繩會被拉長的情況來討論,如果是會被壓縮的情況,其結果的形式應該是相同的。
}}}
{{Subparagraph{
擺繩被拉長之後產生額外彈性回復力 \(f_\text{extra} = -kdl,\) 其中 \(k\) 是擺繩的彈性係數而 \(dl\) 是擺繩被拉長了多少,將此回復力稱為【額外】是因為矲繩可能本來就已經有伸長 \(\Delta l\),也就是已經有回復力 \(k\Delta l\) 的現象。
}}}
{{Subparagraph{
這裡的 \(dl\) 和前面擺繩長度不變時候的 \(dL\) 可能並不一樣,端看擺繩是否夠【硬】(越難拉長就是越硬,反之則越軟)。
# 如果擺繩夠硬使得慣性拉力 \(f_{L}\) 無法將擺繩額外拉長 \(dL\),那麼
** 實際的拉長就會比較小(\(dl < dL\)),
** 此時擺繩的額外回復力會正好抵消慣性拉力,\(f_\text{extra} = f_{L}\),而擺繩張力為 \begin{equation}\label{eq-tension1-1}\boxed{T_\text{ension} = mg\cos\theta + k\Delta l + {2m \over dt^2}(|\vec r + \vec vdt - \vec r_p|-|\vec r - \vec r_p|),}\end{equation}。
# 相反地,如果擺繩夠軟使得慣性拉力足夠將擺繩拉到更長的狀態,那麼
** 在 \(dt\) 時間內最多也只能拉到 \(dL\) 的長度(\(dl = dL\)),
** 此時擺繩的額外回復力為 \(f_\text{extra} = kdL\),而擺繩張力為 \begin{equation}\label{eq-tension1-2}\boxed{T_\text{ension} = mg\cos\theta + k\Delta l + k(|\vec r + \vec vdt - \vec r_p|-|\vec r - \vec r_p|),}\end{equation}。
*** 這個額外回復力會小於慣性拉力,使得在 \(dt\) 時間之後擺繩仍舊會持續被拉長,但也已經額外提供回拉的力量,降低擺錘往外的速率,也就是會減小後續的慣性拉力,在某個時間之後達到相互抵銷,之後返回變成壓縮的趨勢,過程反覆進行。
}}}
{{Subparagraph{
至於實作上如何判斷擺繩是否夠硬,或者說回復力是否足夠抵銷慣性拉力?首先在沒有時間限制下,計算慣性拉力 \(f_{L}\) 能夠將擺繩額外拉到多長 \[kdl_\text{max} = f_{L} \quad \to \quad dl_\text{max} = {f_{L} \over k}.\]再來將這個長度和有 \(dt\) 時間限制的長度 \(dL\) 做比較,若小,則屬於擺繩較硬(上述 (1))的情況,額外張力為 \(f_{L}\) 且擺繩長度增加 \(dl_\text{max}\);若大,則屬於擺繩較軟(上述 (2))的情況,額外張力為 \(kdL\),繩長增加 \(dL\) 。若寫成 Python 程式碼則大致如下:
}}}
{{Subparagraph{
{{{
dlmax = fL / rod.k
if dlmax < dL:
	# We have a hard rod.
	Tension += fL
	rod.length += dlmax
else:
	# We have a soft rod.
	Tension += rod.k*dL
	rod.length += dL
}}}
}}}
{{Section{
結果與討論
}}}
{{Paragraph{
根據上述公式 (\ref{eq-tension0}),(\ref{eq-tension1-1}),(\ref{eq-tension1-2}) 寫出程式碼之後進行測試,並以 \(dt = 0.001\) 秒的時間間隔,採用''[[四階 Runge-Kutta 方法]]求解運動方程'',其結果如下:
# ''平面擺動'' 將初速度設為 \((0,0,0)\),擺動的軌跡就會是平面上的一段圓弧,如圖三及表一所示。
** 圖三顯示的是 50 個週期的軌跡,從右邊的局部放大圖來看,50 個週期的軌跡幾乎都重疊在一起,表示運動方程的解是可靠的,也就是依照公式 (\ref{eq-tension0}) 所計算的張力是可靠的。
** 表一顯示擺動週期隨初始角度的結果,以 10 個週期的平均值和 (\ref{eq-P0}) 式相比,差異均小於 \(0.02 \%\),表示模擬的結果是正確的。
# ''圓錐擺動'' 將初速度的 \(z\) 分量設成如 (\ref{eq-conic-v}) 式的數值,就會是圓錐型擺動,如圖四及表二所示。
** 圖四顯示的是 50 個週期的軌跡,左圖顯示的確是圓錐擺動的情形,而中間及右邊是左邊的局部放大圖。兩張放大圖的差異為:中間是依照本文討論之公式 (\ref{eq-tension0}) 計算張力,而右圖則是依照圓錐擺動公式 (\ref{eq-T1-mg}) 計算張力。兩圖的結果顯示兩種張力公式計算結果並無明顯差異,表示本文所討論的張力計算公式是符合預期的。
** 表二顯示擺動週期隨初始角度的結果,以 10 個週期的平均值和 (\ref{eq-P1}) 式相比,差異均小於 \(0.02 \%\),表示模擬的結果是正確的。
}}}
{{Figure{
|multilined  noborder|k
|height:150pt; [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-planar.JPG]] [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-planar-zoom.JPG]] |
|圖三、初速度為 \((0,0,0)\) 的模擬結果中前 50 個週期之擺錘軌跡軌跡圖,其中右圖為左圖之局部放大,圖中可見全部 50 個週期之軌跡幾乎重疊在一起,顯示運動方程的解是可靠的,也就是依照公式 (\ref{eq-tension0}) 所計算的張力是可靠的。模擬參數:擺長 2.4m,初始角度 10&deg;,時間間隔 \(dt = 0.001\) 秒。|
}}}
{{Figure{
| spreadsheet|k
|表一、平面擺動模擬的前 10 個週期紀錄(單位:sec),其平均值與理論值的差異均小於 \(0.04 \%\)|c
|| 1 | =B0+1 | =C0+1 | =D0+1 | =E0+1 | =F0+1 | =G0+1 | =H0+1 | =I0+1 | =J0+1 | Avg | Theory | Error |h
| 5&deg; | 3.110| 3.111| 3.111| 3.111| 3.111| 3.111| 3.110| 3.111| 3.111| 3.111| ''=round(avg(B1:K1),3)''| 3.110850| ''=round((val(L1)-val(M1))/M1*100,3) %''|
| 10&deg; | 3.115| 3.115| 3.115| 3.116| 3.115| 3.115| 3.116| 3.115| 3.115| 3.116| ''=round(avg(B2:K2),3)''| 3.115291| ''=round((val(L2)-val(M2))/M2*100,3) %''|
| 15&deg; | 3.122| 3.123| 3.123| 3.122| 3.123| 3.123| 3.123| 3.122| 3.123| 3.123| ''=round(avg(B3:K3),3)''| 3.122695| ''=round((val(L3)-val(M3))/M3*100,3) %''|
| 20&deg; | 3.133| 3.133| 3.133| 3.133| 3.134| 3.133| 3.133| 3.133| 3.133| 3.134| ''=round(avg(B4:K4),3)''| 3.133065| ''=round((val(L4)-val(M4))/M4*100,3) %''|
| 25&deg; | 3.146| 3.147| 3.147| 3.147| 3.146| 3.147| 3.147| 3.147| 3.146| 3.147| ''=round(avg(B5:K5),3)''| 3.146406| ''=round((val(L5)-val(M5))/M5*100,3) %''|
| 30&deg; | 3.163| 3.164| 3.163| 3.164| 3.163| 3.164| 3.163| 3.164| 3.163| 3.164| ''=round(avg(B6:K6),3)''| 3.162725| ''=round((val(L6)-val(M6))/M6*100,3) %''|
|>|>|>|>|>|>|>|>|>|>|>|>|>|Theoretical value is calculated with eq( \ref{eq-P0} )|
}}}
{{Figure{
|multilined  noborder|k
|height:300pt; [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical.JPG]] [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-zoom.JPG]]<br>[img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-eq3-zoom.JPG]] [img(45%,)[image/teaching/Sim-Fig-Pendulum-Trajectory-conical-eq1-zoom.JPG]] |
|圖四、初速度為 \((0,0,v_z)\),其中 \(v_z\) 代入圓錐擺動公式 (\ref{eq-conic-v}) 所計算的值,預期結果應為圓錐擺動。左圖為模擬結果的前 50 個週期之擺錘軌跡軌跡圖,顯示的確為圓錐擺動的狀況。中右二圖為左圖之局部放大,中間是依照本文討論之公式 (\ref{eq-tension0}) 計算張力,而右圖則是依照圓錐擺動公式 (\ref{eq-T1-mg}) 計算張力。兩張放大圖的結果顯示兩種張力公式計算結果並無明顯差異,表示本文所討論的張力計算公式是符合預期的。模擬參數:擺長 2.4m,初始角度 10&deg;,時間間隔 \(dt = 0.001\) 秒。|
}}}
{{Figure{
| spreadsheet|k
|表二、圓錐擺動模擬的前 10 個週期紀錄(單位:sec),其平均值與理論值的差異均小於 \(0.07 \%\)|c
|| 1 | =B0+1 | =C0+1 | =D0+1 | =E0+1 | =F0+1 | =G0+1 | =H0+1 | =I0+1 | =J0+1 | Avg | Theory | Error |h
| 5&deg; | 3.103| 3.103| 3.104| 3.103| 3.104| 3.103| 3.104| 3.103| 3.104| 3.103| ''=round(mean(B1:K1),3)''| 3.103449| ''=round((val(L1)-val(M1))/M1*100,3) %''|
| 10&deg; | 3.085| 3.085| 3.086| 3.085| 3.086| 3.086| 3.086| 3.086| 3.086| 3.086| ''=round(mean(B2:K2),3)''| 3.085661| ''=round((val(L2)-val(M2))/M2*100,3) %''|
| 15&deg; | 3.055| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| 3.056| ''=round(mean(B3:K3),3)''| 3.055937| ''=round((val(L3)-val(M3))/M3*100,3) %''|
| 20&deg; | 3.014| 3.014| 3.014| 3.014| 3.014| 3.014| 3.015| 3.014| 3.014| 3.014| ''=round(mean(B4:K4),3)''| 3.014154| ''=round((val(L4)-val(M4))/M4*100,3) %''|
| 25&deg; | 2.96| 2.96| 2.96| 2.96| 2.96| 2.96| 2.96| 2.94| 2.96| 2.96| ''=round(mean(B5:K5),3)''| 2.960127| ''=round((val(L5)-val(M5))/M5*100,3) %''|
| 30&deg; | 2.893| 2.894| 2.893| 2.894| 2.893| 2.894| 2.894| 2.893| 2.894| 2.893| ''=round(mean(B6:K6),3)''| 2.893595| ''=round((val(L6)-val(M6))/M6*100,3) %''|
|>|>|>|>|>|>|>|>|>|>|>|>|>|Theoretical value is calculated with eq( \ref{eq-P1} )|
}}}
{{Paragraph{
如果讓擺繩具有線性回復力 \(f_k = -k\Delta l\),那麼擺錘在半徑方向會有簡諧震盪的行為,週期為 \[T_\text{radial} = {2\pi \over \omega} = {2\pi \sqrt{m \over k}}.\] 如果我們進一步讓這個週期跟擺動的週期重疊,也就是 \[2\pi\sqrt{m \over k} = T_\text{swing} \quad \to \quad k = {4\pi^2 m \over T_\text{swing}^2}.\] 如果是平面擺動,那就是 \[k = {mg \over L}{1 \over \left(1+{1 \over 16}\theta_0^2 + {11 \over 3072}\theta_0^4+\cdots\right)^2};\] 如果是圓錐擺動則為 \[k = {mg \over L\cos\theta}.\] 這種情況下軌跡會如何呢?
}}}
{{Section{
結論
}}}
{{Paragraph{
我們使用固定大小且等同於慣性趨勢效果的假想力來討論擺繩的張力,可以得到當支點固定且擺繩只有徑向變化時擺繩張力的一般式,在圓形軌跡的情況下(平面擺動以及圓錐擺動)的模擬結果符合預期。此結果可以用來模擬這種條件下任意初始條件的擺動,不只限於圓形軌跡的情況。
}}}
{{Section{
參考文獻
}}}
{{Paragraph{
# Wikipedia [[Pendulum|https://en.wikipedia.org/wiki/Pendulum]], [[Conical Pendulum|https://en.wikipedia.org/wiki/Conical_pendulum]]
# [[The Not-So-Simple Pendulum|http://aapt.scitation.org/doi/abs/10.1119/1.2344202?journalCode=pte]]
# http://aapt.scitation.org/doi/abs/10.1119/1.2060649
# [[Approximation Solution of ...|https://www.sciencedirect.com/science/article/pii/S089812211200017X]]
# [[A New Twist for Conical Pendulum|http://aapt.scitation.org/doi/abs/10.1119/1.880112?journalCode=pte]]
# http://iopscience.iop.org/article/10.1088/0031-9120/35/6/309/meta
# http://iopscience.iop.org/article/10.1088/0143-0807/30/6/L01/pdf
# https://www.sciencedirect.com/science/article/pii/0020746279900349
# http://aapt.scitation.org/doi/pdf/10.1119/1.1457824
# https://www.sciencedirect.com/science/article/pii/0375960196006196
# [[Effect of the mass of the cord|http://aapt.scitation.org/doi/abs/10.1119/1.10378]]
數值模擬(含 3D 圖形輸出)的流程主要有三部分
# 畫出場景
# 給定初始條件
# 計算運動過程
其中計算運動過程
物理問題經常需要做積分
積分的基本概念
# 把物體「切割」成很多很多極小的小塊,
# 把每一個小塊當成一個質點,套用質點的公式直接寫下一個小塊的結果,
# 把每一個小塊的結果加起來。
*[>img(20%,)[image/QRcode-wcy2.SimIYPT.jpg]] @@font-size:2em;這個網頁的 URL:<br><br>http://faculty.ndhu.edu.tw/~wcy2/wcy2.SimIYPT.html@@

*@@font-size:1.5em;color:red;如果__幾秒鐘後仍看不到__下面這幾條看起來顯示得不錯的公式,試著重新載入頁面。@@ \[\large {d^2 \vec r \over dt^2} = {\vec F \over m}, \qquad {\partial L \over \partial q} = {d \over dt}{\partial L \over \partial \dot q}, \qquad \nabla^2 V = 0\]

*@@font-size:1.125em;按一下標題文字,可以展開隱藏內容,再按一下可以收起來。@@
!! 開始之前
* [[VPython 3D Objects|http://vpython.org/contents/docs/primitives.html]] / [[2018 培訓區|https://drive.google.com/drive/u/1/folders/1t3ghookKuwCbYv_5p56mNnUWLwXFu4-f]]
* [[MathJax default.js|https://github.com/mathjax/MathJax/blob/master/config/default.js]]
!!2025
[[2025 Problems|http://typt.phy.ntnu.edu.tw/]] / [[Reference kit|http://typt.phy.ntnu.edu.tw/home/%E7%AC%AC%E5%8D%81%E4%BA%94%E5%B1%86typt%E8%BE%AF%E8%AB%96%E9%A1%8C%E7%9B%AE/%E7%AC%AC%E5%8D%81%E5%85%AD%E5%B1%86iypt%E5%8F%83%E8%80%83%E6%96%87%E7%8D%BB.html]]
|editable multilined|k
| Problem | Simulation | Report | Note |h
|3. Lato Lato|[[Simulation|Lato Lato Simulation]](@@color:red;開發中...@@) / [[Panel|Lato Lato Panel]] / [[Creation|Lato Lato Creation]] / [[Initialization|Lato Lato Initialization]] / [[Iteration|Lato Lato Iteration]]|[[2025-3 Lato Lato]](@@color:red;撰寫中...@@)|*[[Log|Lato Lato Log]]|
|X|[[Simulation|X Simulation]](@@color:red;開發中...@@) / [[Creation|X Creation]] / [[Initialization|X Initialization]] / [[Iteration|X Iteration]]|[[2025-X]](@@color:red;撰寫中...@@)|*[[Log|X Log]]|
!!2024
[[2024 Problems|http://typt.phy.ntnu.edu.tw/]] / [[Reference kit|http://typt.phy.ntnu.edu.tw/home/%E7%AC%AC%E5%8D%81%E4%BA%94%E5%B1%86typt%E8%BE%AF%E8%AB%96%E9%A1%8C%E7%9B%AE/%E7%AC%AC%E5%8D%81%E5%85%AD%E5%B1%86iypt%E5%8F%83%E8%80%83%E6%96%87%E7%8D%BB.html]]
|editable multilined|k
| Problem | Simulation | Report | Note |h
|8. Another Magnetic Levitation|[[Simulation|Another Magnetic Levitation Simulation]](@@color:red;開發中...@@) / [[Creation|Another Magnetic Levitation Creation]] / [[Initialization|Another Magnetic Levitation Initialization]] / [[Iteration|Another Magnetic Levitation Iteration]]<br>[[FEM?|https://www.youtube.com/@JousefM/search?query=finite%20element]]|[[2024-8 Another Magnetic Levitation]](@@color:red;撰寫中...@@)|*[[Log|Another Magnetic Levitation Log]]|
|10. Magnetic Gear|[[Simulation|Magnetic Gear Simulation]](@@color:red;開發中...@@) / [[Creation|Magnetic Gear Creation]] / [[Initialization|Magnetic Gear Initialization]] / [[Iteration|Magnetic Gear Iteration]]<br>[[FEM?|https://www.youtube.com/@JousefM/search?query=finite%20element]]|[[2024-10 Magnetic Gear]](@@color:red;撰寫中...@@)|*[[Log|Magnetic Gear Log]]<br>*[[Video|https://youtu.be/x2ZCxkCAoE4]]<br>*[[鄭大師淺談磁性齒輪|https://www.masters.tw/323814/magnetic-gear]]<br>*[[成大論文|https://ieeexplore.ieee.org/document/7988286]]<br>*[[Magnetic Gears|https://www.youtube.com/watch?v=PyBTE5cjGDY]]|
!! 2023
[[2023 Problems|https://www.iypt.org/problems/problems-for-the-36th-iypt-2023/]] / [[Reference kit]]
|multilined|k
| Problem | Simulation | Report | Note |h
|2. Oscillating Sphere|[[Simulation|Oscillating Sphere Simulation]](@@color:red;開發中...@@) / [[Creation|Oscillating Sphere Creation]] / [[Initialization|Oscillating Sphere Initialization]] / [[Iteration|Oscillating Sphere Iteration]]|[[2023-02 Oscillating Sphere]](@@color:red;撰寫中...@@)|[[Log|Oscillating Sphere Log]]|
|6. Magnetic Mechanical Oscillator|[[Simulation|Magnetic Mechanical Oscillator Simulation]](@@未開始...@@) / [[Creation|Magnetic Mechanical Oscillator Creation]] / [[Initialization|Magnetic Mechanical Oscillator Initialization]] / [[Iteration|Euler's Pendulum Iteration]]|[[2023-06 Magnetic Mechanical Oscillator]](@@color:red;撰寫中...@@)|[[Log|Magnetic Mechanical Oscillator Log]]|
|8. Euler's Pendulum|[[Simulation|Euler's Pendulum Simulation]](@@未開始...@@) / [[Creation|Euler's Pendulum Creation]] / [[Initialization|Euler's Pendulum Initialization]] / [[Iteration|Euler's Pendulum Iteration]]|[[2023-08 Euler's Pendulum]](@@未開始...@@)|[[Log|Euler's Pendulum Log]]|
!! 2022
[[2022 Problems|https://www.iypt.org/problems/problems-for-the-35th-iypt-2022/]] / [[Reference kit]]
| multilined|k
| Problem | Simulation | Report | Note |h
|6. Tennis Ball Tower|[[Simulation|Tennis Ball Tower Simulation]](@@未開始...@@) / [[Creation|Tennis Ball Tower Creation]] / [[Initialization|Tennis Ball Tower Initialization]] / [[Iteration|Tennis Ball Tower Iteration]]|[[2022-06 Tennis Ball Tower]](@@未開始...@@)|[[Log|Tennis Ball Tower Log]]|
|7. Three Sided Dice|[[Simulation|Three Sided Dice Simulation]](@@color:green;初步結果@@) / [[Creation|Three Sided Dice Creation]] / [[Initialization|Three Sided Dice Initialization]] / [[Iteration|Three Sided Dice Iteration]]|[[2022-07 Three Sided Dice]](@@未開始...@@)|[[Log|Three Sided Dice Log]]|
|11. Balls on an Elastic Band|[[Simulation|Balls on an Elastic Band Simulation]](@@color:red;開發中...@@) / [[Creation|Balls on an Elastic Band Creation]] / [[Initialization|Balls on an Elastic Band Initialization]] / [[Iteration|Balls on an Elastic Band Iteration]]|[[2022-11 Balls on an Elastic Band]](@@未開始...@@)|[[Log|Balls on an Elastic Band Log]]|
|14. Ball on Membrane|[[Simulation|Ball on Membrane Simulation]](@@color:red;開發中...@@) / [[Creation|Ball on Membrane Creation]] / [[Initialization|Ball on Membrane Initialization]] / [[Iteration|Ball on Membrane Iteration]]|[[2022-14 Ball on Membrane]](@@未開始...@@)|[[Log|Ball on Membrane Log]]|
|17. Invisibility|[[Simulation|Invisibility Simulation]](@@未開始...@@) / [[Creation|Invisibility Creation]] / [[Initialization|Invisibility Initialization]] / [[Iteration|Invisibility Iteration]]|[[2022-17 Invisibility]](@@未開始...@@)|[[Log|Invisibility Log]]|
!! 2021
[[2021 Problems|https://www.iypt.org/problems/problems-for-the-34th-iypt-2021/]] / [[Reference kit]]
| multilined|k
| Problem | Simulation | Report | Note |h
|2. Circling Magnets|[[Simulation|Circling Magnets Simulation]](@@color:green;初步結果...@@) / [[Creation|Circling Magnets Creation]] / [[Initialization|Circling Magnets Initialization]] / [[Iteration|Circling Magnets Iteration]]|[[2021-02 Circling Magnets]](@@color:green;初步結果...@@)|[[Log|Circling Magnets Log]]|
|7. Bead Dynamics|[[Simulation|Bead Dynamics Simulation]](@@color:red;開發中...@@) / [[Creation|Bead Dynamics Creation]] / [[Initialization|Bead Dynamics Initialization]] / [[Iteration|Bead Dynamics Iteration]]|[[2021-02 Bead Dynamics]](@@未開始...@@)|[[Log|Bead Dynamics Log]]|
|10. Spin Drift|[[Simulation|Spin Drift Simulation]](@@未開始...@@) / [[Creation|Spin Drift Creation]] / [[Initialization|Spin Drift Initialization]] / [[Iteration|Spin Drift Iteration]]|[[2021-10 Spin Drift]](@@未開始...@@)|[[Log|Spin Drift Log]]|
|11. Guitar String|[[Simulation|Guitar String Simulation]](@@未開始...@@) / [[Creation|Guitar String Creation]] / [[Initialization|Guitar String Initialization]] / [[Iteration|Guitar String Iteration]]|[[2021-11 Guitar String]](@@未開始...@@)|[[Log|Guitar String Log]]|
|12. Wilberforce Pendulum|[[Simulation|Wilberforce Pendulum Simulation]](@@color:green;初步結果@@) / [[Creation|Wilberforce Pendulum Creation]] / [[Initialization|Wilberforce Pendulum Initialization]] / [[Iteration|Wilberforce Pendulum Iteration]] / [[Panel|Wilberforce Pendulum Panel]]|[[2021-12 Wilberforce Pendulum]](@@color:green;初步結果...@@)|[[Log|Wilberforce Pendulum Log]]|
|15. Rebounding Capsule|[[Simulation|Rebounding Capsule Simulation]](@@未開始...@@) / [[Creation|Rebounding Capsule Creation]] / [[Initialization|Rebounding Capsule Initialization]] / [[Iteration|Rebounding Capsule Iteration]]|[[2021-15 Rebounding Capsule]](@@未開始...@@)|[[Log|Rebounding Capsule Log]]|
!! 2020
[[2020 Problems|image/YPT/Problems-2020.jpg]] / [[Reference kit]]
| multilined|k
| Problem | Simulation | Report | Note |h
|5. Sweet Mirage|[[Sweet Mirage Simulation]](@@未開始...@@) / [[Panel|Sweet Mirage Panel]] / [[Initial|Sweet Mirage Initial]] / [[Code|Sweet Mirage Codes]]|[[2020-05 Sweet Mirage]](@@未開始...@@)||
|7. Balls on a String|[[Balls on a String Simulation]](@@color:green;初步結果@@) / [[Panel|Balls on a String Panel]] / [[Initial|Balls on a String Initial]] / [[Code|Balls on a String Codes]]|[[2020-07 Balls on a String]](@@color:green;初步結果...@@)||
|9. Magnetic Levitation|[[Magnetic Levitation Simulation]](@@未開始...@@) / [[Panel|Magnetic Levitation Panel]] / [[Initial|Magnetic Levitation Initial]] / [[Code|Magnetic Levitation JS Codes]]|[[2020-09 Magnetic Levitation]](@@未開始...@@)||
|13. Friction Oscillator|[[Friction Oscillation Simulation]](@@未開始...@@) / [[Panel|Friction Oscillation Panel]] / [[Initial|Friction Oscillation Initial]] / [[Code|Friction Oscillation JS Codes]]|[[2020-13 Friction Oscillation]](@@未開始...@@)||
!! 2019
[[2019 Problems|YPT/2019/problems2019_signed.pdf]] / [[Reference kit|http://kit.ilyam.org/Draft_2019_IYPT_Reference_kit.pdf]]
| multilined|k
| Problem | Simulation | Report | Note |h
|3. Undertone Sound|[[Undertone Sound Simulation]](@@未開始...@@) / [[Panel|Undertone Sound Panel]] / [[Initial|Undertone Sound Initial]] / [[Code|Undertone Sound JS Codes]]|[[2019-03 降頻音叉]](@@未開始...@@)||
|14. Looping Pendulum|[[Looping Pendulum Simulation]](@@color:green;初步結果@@) / [[Panel|Pendulum Looping Panel]] / [[Initial|Looping Pendulum Initial]] / [[Code|Looping Pendulum Codes]]|[[2019-14 迴旋擺]](@@color:green;初步結果...@@)||
|15. Newton's Cradle|[[Newton's Cradle Simulation]](@@color:green;初步結果@@) / [[Panel|Newton's Cradle Panel]] / [[Initial|Newton's Cradle Initial]] / [[Code|Newton's Cradle JS Codes]]|[[2019-15 牛頓擺]](@@color:green;初步結果...@@)||
!! 2018
[[2018 Problems|http://iypt.org/images/9/9f/problems2018_signed.pdf]] / [[Reference kit|http://kit.ilyam.org/Draft_2018_IYPT_Reference_kit.pdf]]
|multilined|k
| Problem | Simulation | Report | Note |h
|06. Ring Oiler|[[Ring Oiler Simulation]](@@未開始...@@) / [[Initial|Ring Oiler Initial]] / [[Code|Ring Oiler Codes]]|[[2018-06 Ring Oiler]](@@未開始...@@)||
|7. Conical Pile|[[Conical Pile Simulation]](@@color:red;除錯中...@@) / [[Panel |Conical Pile Panel]] / [[Initial|Conical Pile Initial]] / [[Codes|Conical Pile Codes]]|[[2018-07 Conical Pile]](@@color:red;撰寫中...@@)||
|11. ~Azimuthal-Radial Pendulum |[[AR Pendulum Simulation]](@@color:green;有結果@@) / [[Panel|Pendulum AR Panel]] / [[Initial|AR Pendulum Initial]] / [[Code|AR Pendulum Codes]]|[[2018-11 前後左右擺]](@@color:green;初步結果...@@)||
|13. Weighing ime|[[Weighing Time]](@@未開始...@@) / [[Code|Hourglass Codes]]|[[2018-13 秤時間]](@@未開始...@@)||
!! 2017
|multilined|k
| Problem | Simulation | Report | Note |h
|8. Visualizing Density |[[Visualizing Density Simulation]](@@color:red;除錯中...@@) / [[Panel|Visualizing Density Panel]] / [[Code|Visualizing Density Codes]]|[[2017-08 看見密度]](@@color:red;撰寫中...@@)|[[Check this|http://www.ianww.com/blog/2012/11/04/optimizing-three-dot-js-performance-simulating-tens-of-thousands-of-independent-moving-objects/]]<br>[[Hamster.js|https://github.com/austinksmith/Hamsters.js]]<br>[[Parallel.js|https://github.com/parallel-js/parallel.js?utm_source=dlvr.it&utm_medium=twitter]]|
|16. Metronome Sync|[[Pendulum Sync Simulation]](@@未開始...@@) / [[Panel|Pendulum Sync Panel]] / [[Init|Pendulum Sync Init]] / [[Code|Pendulum Sync Code]]|[[2017-16 節拍器同步]](@@未開始...@@)||
!! 2016
|multilined|k
| Problem | Simulation | Report | Note |h
|8. Magnetic Train |[[Magnetic Train Simulation]] / [[Panel|Magnetic Train Panel]] / [[Code 04|Magnetic Train Code 04]]<br>* Subtopic<br>## [[Magnetic Field From Magnets]] / [[Code 01-1|Magnetic Train Code 01-1]] / [[Code 01|Magnetic Train Code 01]]<br>## [[Magnetic Field on Wire]] / [[Code 02-1|Magnetic Train Code 02-1]] / [[Code 02|Magnetic Train Code 02]]<br>## [[Magnetic Force on Wire]] / [[Code 03|Magnetic Train Code 03]]|[[2016-08 磁鐵小火車]](@@color:red;撰寫中...@@)||
!! 其它參考
* [[Python multiprocessing|http://sebastianraschka.com/Articles/2014_multiprocessing.html]] / [[Parallel coding with numpy and scipy|http://scipy-cookbook.readthedocs.io/items/ParallelProgramming.html]] / [[Parallel Python|https://www.parallelpython.com/]]
* [[Parallel programming with multiprocessing|https://wltrimbl.github.io/2014-06-10-spelman/intermediate/python/04-multiprocessing.html]] / [[many alternatives|https://wiki.python.org/moin/ParallelProcessing]]
* [[Typed arrays|https://www.html5rocks.com/en/tutorials/webgl/typed_arrays/]]
* [[How to write fast and memory efficient JS codes|https://www.smashingmagazine.com/2012/11/writing-fast-memory-efficient-javascript/]]
* [[Writing efficient JS|https://gamealchemist.wordpress.com/2016/04/15/writing-efficient-javascript-a-few-tips/]]
* [[Get arrays faster|https://www.google.com.tw/amp/s/gamealchemist.wordpress.com/2013/05/01/lets-get-those-javascript-arrays-to-work-fast/amp/]]
* [[GPU comparison|https://www.microway.com/knowledge-center-articles/comparison-of-nvidia-geforce-gpus-and-nvidia-tesla-gpus/]]
* [[Do we need quaternions?|http://www.gamedev.net/page/resources/_/technical/math-and-physics/do-we-really-need-quaternions-r1199]]
!! To do
* quite a lot!
<<foldHeadings>>

簡單的物體假設為【球體】、【方體】、【圓柱體】
複雜的物體假設為【凸多面體】的組合
!! 物體和牆面的碰撞
牆面可以是平面的或是弧線,判斷的想法為:
# 先讓視線沿著物體的速度(或加速度,至少有重力加速度)方向看過去,以 raycaster 找交點
# 如果和牆面有交點(視點 1)
## 取得視點 1 的牆面法向量
## 讓視線平行於此法向量看往牆面,如果有交點(視點 2)
### 取得視點 2 的牆面法向量
### 如果這兩個法向量平行,則假定牆面是平的,以視點 2 來判斷是否碰撞
### 如果這兩個法向量不平行,則可知牆面不是平的,將視線調至兩者中間,取得視點 3,以視點 3 判斷是否碰種
!! 物體和物體的碰撞
物體可以是方塊形的,也可以是球形或圓柱形的,判斷的想法為:
# 讓視線沿著兩物體的連心線,以 raycaster  找出視線和兩物體的交點(視點 1 及 視點 2)
# 取得視點 1 和視點 2 的法向量
# 如果兩個法向量平行,則可以發生正面碰撞(或追撞)
## 計算各個物體質心視點的距離(視點距離 1 和 視點距離 2),兩距離加起來如果大於等於兩物體連心線的距離,則發生碰撞
### 碰撞點先假定就是視點 1 和視點 2
# 如果兩個法向量不平行,則
## 
http://case.ntu.edu.tw/blog/?p=25796
{{MOSTSubSection{
!!!!最簡單的阻尼震盪
}}}
{{MOSTParagraph{
阻尼震盪就是【會有能量損耗的震盪】,一般而言運動中的能量損耗經常有【速率越快,損耗越大】的現象,在古典物理中通常我們將能量損耗寫成【某種損耗力所做的功】,以便寫入運動方程計算其影響。最簡單的阻尼模型為【損耗''力''的大小__正比於__速率】,也就是 \begin{equation}\vec F_\text{損耗} = -\beta\dot{\vec x},\end{equation} 其中 \(\beta > 0\) 為損耗係數。如此則系統的運動方程為 \begin{equation}m \ddot{\vec x} = \vec F_\text{回復} + \vec F_\text{損耗} = -k\vec x -\beta\dot{\vec x},\end{equation} 其中 \(m\) 為運動物體的質量,\(k > 0\) 為造成震盪的回復係數。
}}}
{{MOSTSubParagraph{
上述運動方程式的解的形式,可以從觀察方程式的意義獲得提示:這個函數的二階微分和它自己,以及它的一階微分,都具有一樣的形式(所以相加減之後可以相等)。最簡單具有這種特性的函數便是指數函數,因此我們將解寫成 \begin{equation}x(t) = x_0 e^{\kappa t},\end{equation} 其中係數 \(x_0\) 為【初始位移】。
}}}
{{MOSTSubParagraph{
至於指數中的 \(\kappa\) 如何得知,我們可以將此解代回運動方程: \begin{equation}m x_0 \kappa^2 e^{\kappa t} = -k x_0 e^{\kappa t} - \beta x_0 \kappa e^{\kappa t}.\end{equation} 消去共同項 \(x_0 e^{\kappa t}\) 則得到 \[m\kappa^2 = -k - \beta\kappa,\] 全部移到等號左邊並依 \(\kappa\) 的冪次排列便得到 \begin{equation}m\kappa^2 + \beta \kappa + k = 0,\end{equation} 一個 \(\kappa\) 的二次方程。
}}}
{{MOSTSubParagraph{
套用二次方程解的公式可得 \begin{equation}\kappa = {-\beta\pm\sqrt{\beta^2-4mk} \over 2m} = -{\beta \over 2m} \pm {\sqrt{\beta^2-4mk} \over 2m},\end{equation} 其中第一項 \(-\beta/2m\) 為【負】實數,第二項則可能為實數可能為虛數,端看根號裡面的結果是否為正。

根據上式我們把解寫成 \begin{equation}x(t) = x_0 e^{\kappa t} = x_0 e^{-{\beta \over 2m}t}e^{\pm{\sqrt{\beta^2-4mk}\over 2m}t}.\end{equation}
}}}
{{MOSTSubParagraph{
至此我們看到解分成兩個部份,第一個部分明顯為衰減,我們將之簡化寫成 \begin{equation}e^{-\lambda t}, \quad \lambda = {\beta \over 2m}.\end{equation} 第二部份的行為則由 \(\kappa\) 的第二項決定,如果其為實數(也就是 \(\beta^2 > 4mk\)),\(\kappa\) 就一定為負值(因為第二項即使取正值,其絕對值也是小於第一項),那麼結果 \(e^{\kappa t}\) 就一定是衰減,這運動很快便會停止,這種情況稱為【過阻尼(overdamped)】。如果是虛數(也就是 \(\beta^2 < 4mk\)),那麼便是震盪(這種情況稱為【次阻尼(underdamped)】),我們將之簡化寫成 \begin{equation}e^{\pm i\omega t}, \quad \omega = {\sqrt{4mk - \beta^2} \over 2m} = \sqrt{ {k \over m} - \left(\beta \over 2m\right)^2} = \sqrt{ {k \over m} - \lambda^2},\end{equation} 其中 \(\omega\) 是其震盪頻率。如果沒有損耗,也就是 \(\beta = 0 \quad \to \quad \lambda = 0\) 的話,則頻率變成 \begin{equation}\omega = \omega_0 = \sqrt{k \over m},\end{equation} 就是簡諧震盪的頻率,也稱做此運動系統的自然頻率。
>如果剛好 \(\beta^2 = 4mk\),那就稱為【臨界阻尼(critically damped)】。
}}}
{{MOSTSubParagraph{
另外,\(e^{\pm i\omega t}\) 其實對應到同樣的物理震盪,因為 \[e^{\pm i\omega t} = \cos(\pm\omega t) + i\sin(\pm\omega t),\] 而物理震盪一定是實數(事實上能測量的物理量都是實數),所以這裡我們只保留實部 \(\cos(\pm\omega t)\),而 cosine 函數的參數不論正負都是同樣的震盪。

總和上述討論,在仍有震盪行為的前提,也就是【次阻尼】的情況下,最簡單阻尼震盪的解為 \begin{equation}\boxed{x(t) = x_0 e^{-\lambda t}\cos(\omega t), \quad \lambda = {\beta \over 2m}, \quad \omega = \sqrt{\omega_0^2 - \lambda^2}, \quad \omega_0 = \sqrt{k \over m}.}\end{equation}
}}}
~VPython is Python+visual
THREE.js

現在的軟體工具讓人們可以很容易做出 3D 的內容,這對於物理教材來說是一大利多,因為很多物理概念需要足夠的三維空間想像力才能理解,而這些概念若是在適當的 3D 繪圖呈現之下,會變得很容易理解,

目前個人電腦的硬體都有足夠的 3D 繪圖能力,加上相對容易使用的軟體,

電腦 3D 繪圖的概念跟拍電影很像,基本要素有
* 導演 -- 就是寫程式的人自己
* 場景 -- 畫面中的物體,以及燈光攝影機等。
* 腳本 -- 就是程式碼