rust experiment

This commit is contained in:
2024-11-15 22:03:51 -06:00
parent 2fc47b2554
commit 04e5d24cab
17 changed files with 4183 additions and 22 deletions

6
ecgsyn-rs/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
*.log
tmp/
node_modules/
pkg/
target/

8
ecgsyn-rs/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

11
ecgsyn-rs/.idea/ecgsyn-rs.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
ecgsyn-rs/.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ecgsyn-rs.iml" filepath="$PROJECT_DIR$/.idea/ecgsyn-rs.iml" />
</modules>
</component>
</project>

6
ecgsyn-rs/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

466
ecgsyn-rs/Cargo.lock generated Normal file
View File

@@ -0,0 +1,466 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "ecgsyn"
version = "0.1.0"
dependencies = [
"itertools",
"js-sys",
"nalgebra",
"num-traits",
"ode_solvers",
"rand_xoshiro",
"realfft",
"wasm-bindgen",
"wee_alloc",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "js-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.162"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "matrixmultiply"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "nalgebra"
version = "0.33.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26aecdf64b707efd1310e3544d709c5c0ac61c13756046aaaba41be5c4f66a3b"
dependencies = [
"approx",
"matrixmultiply",
"nalgebra-macros",
"num-complex",
"num-rational",
"num-traits",
"simba",
"typenum",
]
[[package]]
name = "nalgebra-macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "ode_solvers"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f899b49b30ecbcfc2db3328b9517741764266e8b42a91cde85573da67790019"
dependencies = [
"nalgebra",
"num-traits",
"simba",
"thiserror",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "primal-check"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "rand_xoshiro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
dependencies = [
"rand_core",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "realfft"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1"
dependencies = [
"rustfft",
]
[[package]]
name = "rustfft"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose",
"version_check",
]
[[package]]
name = "safe_arch"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a"
dependencies = [
"bytemuck",
]
[[package]]
name = "simba"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a386a501cd104797982c15ae17aafe8b9261315b5d07e3ec803f2ea26be0fa"
dependencies = [
"approx",
"num-complex",
"num-traits",
"paste",
"wide",
]
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "transpose"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasm-bindgen"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if 1.0.0",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "wide"
version = "0.7.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b828f995bf1e9622031f8009f8481a85406ce1f4d4588ff746d872043e855690"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

24
ecgsyn-rs/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "ecgsyn"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
itertools = "0.13.0"
js-sys = "0.3.72"
nalgebra = "0.33.2"
num-traits = "0.2.19"
ode_solvers = "0.5.0"
rand_xoshiro = "0.6.0"
realfft = "3.4.0"
wasm-bindgen = "0.2.95"
wee_alloc = "0.4.5"
[profile.release]
overflow-checks = false
opt-level = "s"
lto = true

View File

@@ -0,0 +1,26 @@
{
"name": "ecgsyn-site",
"description": "description",
"version": "1.0.0",
"author": "John Luebs <john@luebs.org>",
"license": "MIT",
"main": "app/main.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"serve": "webpack serve --config webpack.config.js --open --mode development --devtool eval-source-map"
},
"dependencies": {
"ecgsyn": "link:../pkg",
"picnic": "^7.1.0"
},
"devDependencies": {
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
}
}

2844
ecgsyn-rs/site/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
ecgsyn-rs/site/src/custom.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.css" {
const content: any;
export default content;
}

View File

@@ -0,0 +1,80 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>ECGSYN</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Place favicon.ico in the root directory -->
</head>
<body>
<script src="./bundle.js"></script>
<!--[if lt IE 8]>
<p class="browserupgrade">
You are using an <strong>outdated</strong> browser. Please
<a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.
</p>
<![endif]-->
<div class="flex two">
<div class="full">
<canvas height="200"></canvas>
</div>
<div style="width: 5%">
<label class="stack wlabel">
<input name="stack" type="radio" />
<span class="button toggle">P</span>
</label>
<label class="stack wlabel">
<input name="stack" type="radio" />
<span class="button toggle">Q</span>
</label>
<label class="stack wlabel">
<input name="stack" type="radio" />
<span class="button toggle">R</span>
</label>
<label class="stack wlabel">
<input name="stack" type="radio" />
<span class="button toggle">S</span>
</label>
<label class="stack wlabel">
<input name="stack" type="radio" />
<span class="button toggle">T</span>
</label>
</div>
<div style="width: 95%">
<article class="parameter-group">
<div class="slider-group">
<div class="slider-container">
<label>θ (°)</label>
<input type="range" min="-180" max="180" value="0" step="1" />
<input class="value" type="number" min="-180" max="180" value="0" />
</div>
<div class="slider-container">
<label>a</label>
<input type="range" min="-30" max="30" value="0" step="0.1" />
<input class="value" type="number" min="-30" max="30" value="0" />
</div>
<div class="slider-container">
<label>b</label>
<input type="range" min="0" max="1" value="0.5" step="0.01" />
<input class="value" type="number" min="0" max="1" value="0.5" />
</div>
<div class="slider-container">
<label>θrf</label>
<input type="range" min="0" max="0.5" value="0" step="0.01" />
<input class="value" type="number" min="0" max="0.5" value="0" />
</div>
</div>
</article>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
import * as ecg from "ecgsyn";
import "./styles.css";
//ecg.greet("WebAssembly with Penis!");
let tp = new ecg.TimeParameters();
let rrprocess = new ecg.RrProcess(tp);
let rrp = new ecg.RrParameters();
let series = rrprocess.generate_series(rrp);
let params = new ecg.Parameters(-0.4, 1.2, 0.0);
params.push_extremum(new ecg.Extremum(0.0, 30, 0.1, 0.0));
let buffer = new ecg.Buffer();
ecg.ecgsyn(params, series, buffer);
(window as any).ecg = ecg;
(window as any).buffer = buffer;

View File

@@ -0,0 +1,68 @@
@import "picnic/picnic.css";
.wlabel {
text-align: center;
margin-bottom: 0.2em;
}
.control-panel {
max-width: 600px;
margin: 20px auto;
padding: 20px;
}
.wave-selector {
display: flex;
gap: 20px;
align-items: center;
margin-bottom: 15px;
padding: 15px;
background: #f8f8f8;
border-radius: 4px;
}
.wave-selector label {
margin: 0;
}
.wave-buttons {
margin-left: auto;
display: flex;
gap: 10px;
}
.parameter-group {
margin: 0 0;
padding: 15px;
background: #f8f8f8;
border-radius: 4px;
}
.slider-group {
margin: 10px 0;
}
.slider-container {
display: flex;
align-items: center;
gap: 10px;
margin: 5px 0;
}
.slider-container label {
width: 80px;
}
.slider-container input[type="range"] {
flex-grow: 1;
}
.slider-container .value {
width: 8em;
text-align: right;
}
button.small {
padding: 0.3em 0.7em;
font-size: 0.9em;
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES5",
"module": "ES6",
"jsx": "react",
"moduleResolution": "Node",
"outDir": "./dist/",
"strict": true,
"sourceMap": true,
"declaration": true,
"declarationDir": "./dist/types/",
"noImplicitAny": true,
"esModuleInterop": true,
"downlevelIteration": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"isolatedModules": true,
"isolatedDeclarations": true
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
}

View File

@@ -0,0 +1,34 @@
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");
module.exports = {
entry: "./src/index.ts",
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
experiments: {
asyncWebAssembly: true,
},
plugins: [
new CopyPlugin({
patterns: [{ from: "src/index.html" }],
}),
],
};

558
ecgsyn-rs/src/lib.rs Normal file
View File

@@ -0,0 +1,558 @@
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
use itertools::Itertools;
use itertools::MinMaxResult::MinMax;
use js_sys::{Float64Array, WebAssembly};
use nalgebra::{Complex, Vector3};
use num_traits::float::Float;
use ode_solvers::Dopri5;
use rand_xoshiro::{
rand_core::{RngCore, SeedableRng},
Xoshiro256Plus,
};
use realfft::{ComplexToReal, RealFftPlanner};
use std::{f64::consts::TAU, iter::Sum, sync::Arc};
use wasm_bindgen::prelude::*;
trait RoundTies {
fn round_ties_even(self) -> Self;
}
impl RoundTies for f32 {
#[inline(always)]
fn round_ties_even(self) -> Self {
f32::round_ties_even(self)
}
}
impl RoundTies for f64 {
#[inline(always)]
fn round_ties_even(self) -> Self {
f64::round_ties_even(self)
}
}
fn ieee_rem<T: Float + RoundTies>(x: T, y: T) -> T {
let n = (x / y).round_ties_even();
x - n * y
}
trait Rand {
fn rand<R: RngCore>(rng: &mut R) -> Self;
}
impl Rand for f32 {
fn rand<R: RngCore>(rng: &mut R) -> f32 {
f32::from_bits(rng.next_u32() >> 8) * 5.9604645e-8f32
}
}
impl Rand for f64 {
fn rand<R: RngCore>(rng: &mut R) -> f64 {
f64::from_bits(rng.next_u64() >> 11) * 1.1102230246251565e-16
}
}
fn mean<T>(v: &[T]) -> T
where
T: Float + Sum<T>,
{
v.iter().copied().sum::<T>() / T::from(v.len()).unwrap()
}
fn sum_square_deviations<T>(v: &[T], c: Option<T>) -> T
where
T: Float + Sum<T>,
{
let c = c.unwrap_or_else(|| mean(v));
v.iter().map(|x| (*x - c).powi(2)).sum()
}
fn variance<T>(v: &[T], xbar: Option<T>) -> T
where
T: Float + Sum<T>,
{
assert!(v.len() > 1);
let n = T::from(v.len()).unwrap();
let sum = sum_square_deviations(v, xbar);
sum / (n - T::one())
}
fn std_deviation<T>(v: &[T], xbar: Option<T>) -> T
where
T: Float + Sum<T>,
{
variance(v, xbar).sqrt()
}
const FFT_MIN_SIZE: usize = 32;
fn fft_is_valid_size(n: usize) -> bool {
const MIN_SIZE: usize = FFT_MIN_SIZE;
// n must be greater than 32, and have prime factors only of 2, 3, and 5.
let mut n = n;
while n >= 5 * MIN_SIZE && (n % 5) == 0 {
n /= 5;
}
while n >= 3 * MIN_SIZE && (n % 3) == 0 {
n /= 3;
}
while n >= 2 * MIN_SIZE && (n % 2) == 0 {
n /= 2;
}
n == MIN_SIZE
}
fn fft_nearest_valid_size(n: usize, round_up: bool) -> usize {
const MIN_SIZE: usize = FFT_MIN_SIZE;
if n < MIN_SIZE {
return MIN_SIZE;
}
let initial_size = if round_up {
MIN_SIZE * ((n + MIN_SIZE - 1) / MIN_SIZE) // round up
} else {
MIN_SIZE * (n / MIN_SIZE) // round down
};
let step = if round_up {
MIN_SIZE
} else {
-(MIN_SIZE as isize) as usize
};
std::iter::successors(Some(initial_size), |&size| Some(size.wrapping_add(step)))
.find(|&size| fft_is_valid_size(size))
.unwrap()
}
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct TimeParameters {
/// The number of beats to simulate.
pub num_beats: u32,
/// The internal sampling frequency in Hz.
pub sr_internal: u32,
/// Output decimation factor.
pub decimate_factor: u32,
/// The mean heart rate in beats per minute.
pub hr_mean: f64,
/// The standard deviation of the heart rate.
pub hr_std: f64,
/// RNG seed value
pub seed: u64,
}
impl Default for TimeParameters {
fn default() -> Self {
TimeParameters {
num_beats: 12,
sr_internal: 512,
decimate_factor: 1,
hr_mean: 60.0,
hr_std: 1.0,
seed: 8,
}
}
}
#[wasm_bindgen]
impl TimeParameters {
#[wasm_bindgen(constructor)]
pub fn new() -> TimeParameters {
TimeParameters::default()
}
}
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct RrParameters {
/// Mayer wave frequency in Hz.
pub flo: f64,
/// flo standard deviation.
pub flostd: f64,
/// Respiratory frequency in Hz.
pub fhi: f64,
/// fhi standard deviation.
pub fhistd: f64,
/// The ratio of power between low and high frequency components.
pub lfhfratio: f64,
}
impl Default for RrParameters {
fn default() -> Self {
RrParameters {
flo: 0.1,
flostd: 0.01,
fhi: 0.25,
fhistd: 0.01,
lfhfratio: 0.5,
}
}
}
#[wasm_bindgen]
impl RrParameters {
#[wasm_bindgen(constructor)]
pub fn new() -> RrParameters {
RrParameters::default()
}
}
#[wasm_bindgen]
pub struct RrProcess {
time_params: TimeParameters,
ifft: Arc<dyn ComplexToReal<f64>>,
spectrum: Vec<Complex<f64>>,
scratch: Vec<Complex<f64>>,
signal: Vec<f64>,
rng: Xoshiro256Plus,
rr_mean: f64,
rr_std: f64,
}
#[wasm_bindgen]
impl RrProcess {
#[wasm_bindgen(constructor)]
pub fn new(time_params: &TimeParameters) -> RrProcess {
let mut planner = RealFftPlanner::<f64>::new();
let rr_mean = 60.0 / time_params.hr_mean;
let rr_std = 60.0 * time_params.hr_std / time_params.hr_mean.powi(2);
let nrr = fft_nearest_valid_size(
(time_params.num_beats * time_params.sr_internal * (rr_mean.ceil() as u32)) as usize,
true,
);
let ifft = planner.plan_fft_inverse(nrr);
let spectrum = ifft.make_input_vec();
let signal = ifft.make_output_vec();
let scratch = ifft.make_scratch_vec();
let rng = Xoshiro256Plus::seed_from_u64(time_params.seed);
RrProcess {
time_params: time_params.clone(),
ifft,
spectrum,
scratch,
signal,
rng,
rr_mean,
rr_std,
}
}
#[wasm_bindgen]
pub fn generate_signal(&mut self, params: &RrParameters) -> Box<[f64]> {
let w1 = TAU * params.flo;
let w2 = TAU * params.fhi;
let c1 = TAU * params.flostd;
let c2 = TAU * params.fhistd;
let sig2 = 1.0;
let sig1 = params.lfhfratio;
let sr = self.time_params.sr_internal as f64;
let nrr = self.signal.len() as f64;
let dw = sr / nrr * TAU;
self.spectrum.iter_mut().enumerate().for_each(|(i, x)| {
let w = i as f64 * dw;
let dw1 = w - w1;
let dw2 = w - w2;
let hw1 = sig1 * (-dw1.powi(2) / (2.0 * c1.powi(2))).exp() / (TAU * c1.powi(2)).sqrt();
let hw2 = sig2 * (-dw2.powi(2) / (2.0 * c2.powi(2))).exp() / (TAU * c2.powi(2)).sqrt();
let hw = hw1 + hw2;
let sw = (sr / 2.0) * hw.sqrt();
let ph = TAU * f64::rand(&mut self.rng);
x.re = sw * ph.cos();
x.im = sw * ph.sin();
});
self.spectrum[0].im = 0.0;
if let Some(last) = self.spectrum.last_mut() {
last.im = 0.0;
}
let _ =
self.ifft
.process_with_scratch(&mut self.spectrum, &mut self.signal, &mut self.scratch);
self.signal.iter_mut().for_each(|x| *x *= 1.0 / nrr);
let xstd = std_deviation(&self.signal, None);
let ratio = self.rr_std / xstd;
self.signal
.iter()
.map(|x| x * ratio + self.rr_mean)
.collect()
}
#[wasm_bindgen]
pub fn generate_series(&mut self, params: &RrParameters) -> RrSeriesWasm {
let signal = self.generate_signal(params);
self.rng.jump();
RrSeriesWasm(RrSeries::new(
&self.time_params,
params,
self.rng.clone(),
&signal,
))
}
}
struct Segment<T: Float> {
end: T,
value: T,
}
impl<T: Float> Segment<T> {
fn new(end: T, value: T) -> Segment<T> {
Segment { end, value }
}
}
struct RrSeries<T: Float> {
time_params: TimeParameters,
rr_params: RrParameters,
rng: Xoshiro256Plus,
len: usize,
segments: Vec<Segment<T>>,
}
impl<T: Float + RoundTies> RrSeries<T> {
fn new(
time_params: &TimeParameters,
rr_params: &RrParameters,
rng: Xoshiro256Plus,
signal: &[T],
) -> RrSeries<T> {
let sr = T::from(time_params.sr_internal).unwrap();
let mut tecg = T::zero();
let mut i = 0usize;
let mut segments = Vec::<Segment<T>>::new();
while i < signal.len() {
tecg = tecg + signal[i];
segments.push(Segment::new(tecg, signal[i]));
i = (T::one() + (tecg * sr).round_ties_even())
.to_usize()
.unwrap();
}
RrSeries {
time_params: *time_params,
rr_params: *rr_params,
rng,
len: signal.len(),
segments,
}
}
fn val(&self, t: T) -> T {
let len = self.segments.len();
let i = match self.segments.partition_point(|x| t.lt(&x.end)) {
i if i < len => i,
_ => len - 1,
};
self.segments[i].value
}
}
#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct Extremum {
pub theta: f64,
pub a: f64,
pub b: f64,
pub theta_rf: f64,
}
#[wasm_bindgen]
impl Extremum {
#[wasm_bindgen(constructor)]
pub fn new(deg: f64, a: f64, b: f64, theta_rf: f64) -> Extremum {
Extremum {
theta: deg.to_radians(),
a,
b,
theta_rf,
}
}
}
#[wasm_bindgen]
pub struct Buffer(Vec<f64>);
#[wasm_bindgen]
impl Buffer {
#[wasm_bindgen(constructor)]
pub fn new() -> Buffer {
Buffer(Vec::new())
}
#[wasm_bindgen]
pub fn array(&self) -> Float64Array {
let memory_buffer = wasm_bindgen::memory()
.dyn_into::<WebAssembly::Memory>()
.unwrap()
.buffer();
Float64Array::new_with_byte_offset_and_length(
&memory_buffer,
self.0.as_ptr() as u32,
self.0.len() as u32,
)
}
}
#[wasm_bindgen]
pub struct Parameters {
pub range_min: f64,
pub range_max: f64,
pub noise_amplitude: f64,
extrema: Vec<Extremum>,
}
#[wasm_bindgen]
impl Parameters {
#[wasm_bindgen(constructor)]
pub fn new(range_min: f64, range_max: f64, noise_amplitude: f64) -> Parameters {
Parameters {
range_min,
range_max,
noise_amplitude,
extrema: Vec::with_capacity(5),
}
}
#[wasm_bindgen]
pub fn push_extremum(&mut self, extremum: &Extremum) {
self.extrema.push(*extremum);
}
#[wasm_bindgen]
pub fn remove_extremum(&mut self, index: usize) {
self.extrema.remove(index);
}
#[wasm_bindgen]
pub fn set_extremum(&mut self, index: usize, extremum: &Extremum) {
self.extrema[index] = *extremum;
}
}
struct Attractor {
ti: f64,
ai: f64,
bi: f64,
}
struct EcgSystem<'a> {
rr: &'a RrSeries<f64>,
fhi: f64,
attrs: Vec<Attractor>,
}
type State = Vector3<f64>;
impl ode_solvers::System<f64, State> for EcgSystem<'_> {
fn system(&self, t: f64, v: &State, dv: &mut State) {
let ta = v.y.atan2(v.x);
let r0 = 1.0;
let a0 = 1.0 - (v.x.powi(2) + v.y.powi(2)).sqrt() / r0;
let w0 = TAU / self.rr.val(t);
let zbase = 0.005 * (TAU * self.fhi * t).sin();
dv.x = a0 * v.x - w0 * v.y;
dv.y = a0 * v.y + w0 * v.x;
dv.z = 0.0;
for attr in self.attrs.iter() {
let dt = ieee_rem(ta - attr.ti, TAU);
dv.z += -attr.ai * dt * (-0.5 * dt.powi(2) / attr.bi.powi(2)).exp();
}
dv.z += -1.0 * (v.z - zbase);
}
}
#[wasm_bindgen]
pub fn ecgsyn(params: &Parameters, rr_series: &mut RrSeriesWasm, buffer: &mut Buffer) {
let time_params = &rr_series.0.time_params;
let rr_params = &rr_series.0.rr_params;
let hr_sec = time_params.hr_mean / 60.0;
let hr_fact = hr_sec.sqrt();
let system = EcgSystem {
rr: &rr_series.0,
fhi: rr_params.fhi,
attrs: params
.extrema
.iter()
.map(|ex| Attractor {
ti: ex.theta * hr_sec.powf(ex.theta_rf),
ai: ex.a,
bi: ex.b * hr_fact,
})
.collect(),
};
let dt = 1.0 / time_params.sr_internal as f64;
let t_end = rr_series.0.len as f64 * dt;
let y0 = State::new(1.0, 0.0, 0.04);
let mut stepper = Dopri5::new(system, 0.0, t_end, dt, y0, 1.0e-6, 1.0e-6);
let _ = stepper.integrate().unwrap();
let zresult = &mut buffer.0;
let stepper_out = stepper.y_out();
zresult.clear();
stepper_out
.iter()
.step_by(time_params.decimate_factor as usize)
.for_each(|y| {
zresult.push(y.z);
});
let (zmin, zmax) = if let MinMax(zmin, zmax) = zresult.iter().minmax() {
(*zmin, *zmax)
} else {
return;
};
let zrange = zmax - zmin;
zresult.iter_mut().for_each(|z| {
*z = params.range_min
+ (params.range_max - params.range_min) * (*z - zmin) / zrange
+ (2.0 * f64::rand(&mut rr_series.0.rng) - 1.0) * params.noise_amplitude;
});
}
#[wasm_bindgen(js_name = "RrSeries")]
pub struct RrSeriesWasm(RrSeries<f64>);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let time_params = TimeParameters::default();
let rr_params = RrParameters::default();
let mut rr_process = RrProcess::new(&time_params);
let mut rr_series = rr_process.generate_series(&rr_params);
let mut buffer = Buffer::new();
let mut params = Parameters::new(-0.4, 1.2, 0.0);
vec![
Extremum::new(-70.0, 1.2, 0.25, 0.25),
Extremum::new(-15.0, 0.0, 0.0, 0.5),
Extremum::new(0.0, 30.0, 0.1, 0.0),
Extremum::new(15.0, -7.5, 0.1, 0.5),
Extremum::new(100.0, 0.75, 0.04, 0.25),
]
.iter()
.for_each(|ex| {
params.push_extremum(ex);
});
ecgsyn(&params, &mut rr_series, &mut buffer);
}
}

View File

@@ -1,22 +0,0 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Untitled</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<!-- Place favicon.ico in the root directory -->
</head>
<body>
<!--[if lt IE 8]>
<p class="browserupgrade">
You are using an <strong>outdated</strong> browser. Please
<a href="http://browsehappy.com/">upgrade your browser</a> to improve
your experience.
</p>
<![endif]-->
</body>
</html>