diff --git a/package-lock.json b/package-lock.json index adc7007..fe0a6ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "jsthermalcomfort", "version": "0.0.2", "license": "MIT", + "dependencies": { + "mathjs": "^11.11.2" + }, "devDependencies": { "documentation": "^14.0.2", "github-slugger": "^2.0.0", @@ -33,12 +36,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.10", + "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" }, "engines": { @@ -162,12 +165,12 @@ "dev": true }, "node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.10", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -193,22 +196,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -300,9 +303,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -332,12 +335,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -417,9 +420,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.11.tgz", - "integrity": "sha512-R5zb8eJIBPJriQtbH/htEQy4k7E2dHWlD2Y2VT07JCzwYZHBxV5ZYtM0UhXSNMT74LyxuM+b1jdL7pSesXbC/g==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -605,34 +608,45 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.11.tgz", - "integrity": "sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.11", - "@babel/types": "^7.22.11", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -641,13 +655,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.11", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.11.tgz", - "integrity": "sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1797,6 +1811,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/complex.js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1847,6 +1873,11 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -2131,6 +2162,11 @@ "node": ">=6" } }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -2263,6 +2299,18 @@ "node": ">=8" } }, + "node_modules/fraction.js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", + "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2975,6 +3023,11 @@ "node": ">=8" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==" + }, "node_modules/jest": { "version": "29.6.4", "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.4.tgz", @@ -3873,6 +3926,28 @@ "node": ">= 12" } }, + "node_modules/mathjs": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.11.2.tgz", + "integrity": "sha512-SL4/0Fxm9X4sgovUpJTeyVeZ2Ifnk4tzLPTYWDyR3AIx9SabnXYqtCkyJtmoF3vZrDPKGkLvrhbIL4YN2YbXLQ==", + "dependencies": { + "@babel/runtime": "^7.23.1", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/mdast-util-definitions": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", @@ -5417,6 +5492,11 @@ "node": ">=8.10.0" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/remark": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/remark/-/remark-14.0.3.tgz", @@ -5603,6 +5683,11 @@ "node": ">=6" } }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5864,6 +5949,11 @@ "node": ">=8" } }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -5932,6 +6022,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-function": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.1.tgz", + "integrity": "sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==", + "engines": { + "node": ">= 14" + } + }, "node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", diff --git a/package.json b/package.json index fcd2eff..6b4e7c7 100644 --- a/package.json +++ b/package.json @@ -62,5 +62,8 @@ "lodash": "^4.17.21", "prettier": "3.0.2", "typescript": "^5.1.6" + }, + "dependencies": { + "mathjs": "^11.11.2" } } diff --git a/src/jos3_functions/matrix.js b/src/jos3_functions/matrix.js index b2b8e79..4edcf9f 100644 --- a/src/jos3_functions/matrix.js +++ b/src/jos3_functions/matrix.js @@ -1,3 +1,5 @@ +import { $array, $map, $reduce, $sum } from "../supa.js"; + export const BODY_NAMES = [ "head", "neck", @@ -83,3 +85,292 @@ export const [IDICT, NUM_NODES] = (() => { return [index_dict, order_count]; })(); + +/** + * Get artery and vein blood flow rate [l/h]. + * + * @param {number[]} bf_core - Core blood flow rate [l/h]. + * @param {number[]} bf_muscle - Muscle blood flow rate [l/h]. + * @param {number[]} bf_fat - Fat blood flow rate [l/h]. + * @param {number[]} bf_skin - Skin blood flow rate [l/h]. + * @param {number} bf_ava_hand - AVA blood flow rate at hand [l/h]. + * @param {number} bf_ava_foot - AVA blood flow rate at foot [l/h]. + * + * @returns {[number[], number[]]} bf_artery, bf_vein - Artery and vein blood flow rate [l/h]. + */ +export function vessel_blood_flow( + bf_core, + bf_muscle, + bf_fat, + bf_skin, + bf_ava_hand, + bf_ava_foot, +) { + let xbf = $map( + [bf_core, bf_muscle, bf_fat, bf_skin], + ([bf_core, bf_muscle, bf_fat, bf_skin]) => + bf_core + bf_muscle + bf_fat + bf_skin, + ); + + let bf_art = $array(17, 0); + let bf_vein = $array(17, 0); + + // head + bf_art[0] = xbf[0]; + bf_vein[0] = xbf[0]; + + // neck (+head) + bf_art[1] = xbf[1] + xbf[0]; + bf_vein[1] = xbf[1] + xbf[0]; + + // chest + bf_art[2] = xbf[2]; + bf_vein[2] = xbf[2]; + + // back + bf_art[3] = xbf[3]; + bf_vein[3] = xbf[3]; + + // pelvis (+Thighs, Legs, Feet, AVA_Feet) + bf_art[4] = xbf[4] + $sum(xbf.slice(11, 17)) + 2 * bf_ava_foot; + bf_vein[4] = xbf[4] + $sum(xbf.slice(11, 17)) + 2 * bf_ava_foot; + + // L.Shoulder (+Arm, Hand, (arteryのみAVA_Hand)) + bf_art[5] = $sum(xbf.slice(5, 8)) + bf_ava_hand; + bf_vein[5] = $sum(xbf.slice(5, 8)); + + // L.Arm (+Hand) + bf_art[6] = $sum(xbf.slice(6, 8)) + bf_ava_hand; + bf_vein[6] = $sum(xbf.slice(6, 8)); + + // R.Shoulder (+Arm, Hand, (arteryのみAVA_Hand)) + bf_art[8] = $sum(xbf.slice(8, 11)) + bf_ava_hand; + bf_vein[8] = $sum(xbf.slice(8, 11)); + + // R.Arm (+Hand) + bf_art[9] = $sum(xbf.slice(9, 11)) + bf_ava_hand; + bf_vein[9] = $sum(xbf.slice(9, 11)); + + // R.Hand + bf_art[10] = xbf[10] + bf_ava_hand; + bf_vein[10] = xbf[10]; + + // L.Thigh (+Leg, Foot, (arteryのみAVA_Foot)) + bf_art[11] = $sum(xbf.slice(11, 14)) + bf_ava_foot; + bf_vein[11] = $sum(xbf.slice(11, 14)); + + // L.Leg (+Foot) + bf_art[12] = $sum(xbf.slice(12, 14)) + bf_ava_foot; + bf_vein[12] = $sum(xbf.slice(12, 14)); + + // L.Foot + bf_art[13] = xbf[13] + bf_ava_foot; + bf_vein[13] = xbf[13]; + + // R.Thigh (+Leg, Foot, (arteryのみAVA_Foot)) + bf_art[14] = $sum(xbf.slice(14, 17)) + bf_ava_foot; + bf_vein[14] = $sum(xbf.slice(14, 17)); + + // R.Leg (+Foot) + bf_art[15] = $sum(xbf.slice(15, 17)) + bf_ava_foot; + bf_vein[15] = $sum(xbf.slice(15, 17)); + + // R.Foot + bf_art[16] = xbf[16] + bf_ava_foot; + bf_vein[16] = xbf[16]; + + return [bf_art, bf_vein]; +} + +/** + * + * @param bf_core + * @param bf_muscle + * @param bf_fat + * @param bf_skin + * @param bf_ava_hand + * @param bf_ava_foot + * + * @returns {number[][]} + */ +export function local_arr( + bf_core, + bf_muscle, + bf_fat, + bf_skin, + bf_ava_hand, + bf_ava_foot, +) { + let bf_local = Array(NUM_NODES) + .fill() + .map(() => Array(NUM_NODES).fill(0)); + + for (let i = 0; i < BODY_NAMES.length; i++) { + // Dictionary of indicies in each body segment + // key = layer name, value = index of matrix + let bn = BODY_NAMES[i]; + let index_of = IDICT[bn]; + + // common + bf_local[index_of["core"]][index_of["artery"]] = 1.067 * bf_core[i]; // art to cr + bf_local[index_of["skin"]][index_of["artery"]] = 1.067 * bf_core[i]; // art to sk + bf_local[index_of["vein"]][index_of["core"]] = 1.067 * bf_core[i]; // vein to cr + bf_local[index_of["vein"]][index_of["skin"]] = 1.067 * bf_core[i]; // vein to sk + + // If the segment has a muscle or fat layer + if (index_of["muscle"] !== null) { + bf_local[index_of["muscle"]][index_of["artery"]] = 1.067 * bf_muscle[i]; // art to ms + bf_local[index_of["vein"]][index_of["muscle"]] = 1.067 * bf_muscle[i]; // vein to ms + } + + if (index_of["fat"] !== null) { + bf_local[index_of["fat"]][index_of["artery"]] = 1.067 * bf_fat[i]; // art to ft + bf_local[index_of["vein"]][index_of["fat"]] = 1.067 * bf_fat[i]; // vein to ft + } + + switch (i) { + // Only hand + case 7: + case 10: + bf_local[index_of["sfvein"]][index_of["artery"]] = 1.067 * bf_ava_hand; // art to sfv + break; + + // Only foot + case 13: + case 16: + bf_local[index_of["sfvein"]][index_of["artery"]] = 1.067 * bf_ava_foot; // art to sfv + } + } + + return bf_local; +} + +/** + * Create matrix to calculate heat exchange by blood flow between segments. [W/K] + * @param bf_art + * @param bf_vein + * @param bf_ava_hand + * @param bf_ava_foot + */ +export function whole_body(bf_art, bf_vein, bf_ava_hand, bf_ava_foot) { + let arr83 = Array(NUM_NODES) + .fill(undefined) + .map(() => Array(NUM_NODES).fill(0)); + + const flow = (up, down, bloodflow) => (arr83[down][up] = bloodflow * 1.067); // Coefficient = 1.067 [Wh/L.K] Change units [L/h] to [W/K] + + const CB = IDICT["CB"]; + const head = IDICT["head"]["artery"]; + const neck = IDICT["neck"]["artery"]; + const chest = IDICT["chest"]["artery"]; + const back = IDICT["back"]["artery"]; + const pelvis = IDICT["pelvis"]["artery"]; + const left_shoulder = IDICT["left_shoulder"]["artery"]; + const left_arm = IDICT["left_arm"]["artery"]; + const left_hand = IDICT["left_hand"]["artery"]; + const right_shoulder = IDICT["right_shoulder"]["artery"]; + const right_arm = IDICT["right_arm"]["artery"]; + const right_hand = IDICT["right_hand"]["artery"]; + const left_thigh = IDICT["left_thigh"]["artery"]; + const left_leg = IDICT["left_leg"]["artery"]; + const left_foot = IDICT["left_foot"]["artery"]; + const right_thigh = IDICT["right_thigh"]["artery"]; + const right_leg = IDICT["right_leg"]["artery"]; + const right_foot = IDICT["right_foot"]["artery"]; + + flow(CB, neck, bf_art[1]); // CB to neck.art + flow(neck, head, bf_art[0]); // neck.art to head.art + flow(head + 1, neck + 1, bf_vein[0]); // head.vein to neck.vein + flow(neck + 1, CB, bf_vein[1]); // neck.vein to CB + + flow(CB, chest, bf_art[2]); // CB to chest.art + flow(chest + 1, CB, bf_vein[2]); // chest.vein to CB + + flow(CB, back, bf_art[3]); // CB to back.art + flow(back + 1, CB, bf_vein[3]); // back.vein to CB + + flow(CB, pelvis, bf_art[4]); // CB to pelvis.art + flow(pelvis + 1, CB, bf_vein[4]); // pelvis.vein to CB + + flow(CB, left_shoulder, bf_art[5]); // CB to left_shoulder.art + flow(left_shoulder, left_arm, bf_art[6]); // left_shoulder.art to left_arm.art + flow(left_arm, left_hand, bf_art[7]); // left_arm.art to left_hand.art + flow(left_hand + 1, left_arm + 1, bf_vein[7]); // left_hand.vein to left_arm.vein + flow(left_arm + 1, left_shoulder + 1, bf_vein[6]); // left_arm.vein to left_shoulder.vein + flow(left_shoulder + 1, CB, bf_vein[5]); // left_shoulder.vein to CB + flow(left_hand + 2, left_arm + 2, bf_ava_hand); // left_hand.sfvein to left_arm.sfvein + flow(left_arm + 2, left_shoulder + 2, bf_ava_hand); // left_arm.sfvein to left_shoulder.sfvein + flow(left_shoulder + 2, CB, bf_ava_hand); // left_shoulder.sfvein to CB + + flow(CB, right_shoulder, bf_art[8]); // CB to right_shoulder.art + flow(right_shoulder, right_arm, bf_art[9]); // right_shoulder.art to right_arm.art + flow(right_arm, right_hand, bf_art[10]); // right_arm.art to right_hand.art + flow(right_hand + 1, right_arm + 1, bf_vein[10]); // right_hand.vein to right_arm.vein + flow(right_arm + 1, right_shoulder + 1, bf_vein[9]); // right_arm.vein to right_shoulder.vein + flow(right_shoulder + 1, CB, bf_vein[8]); // right_shoulder.vein to CB + flow(right_hand + 2, right_arm + 2, bf_ava_hand); // right_hand.sfvein to right_arm.sfvein + flow(right_arm + 2, right_shoulder + 2, bf_ava_hand); // right_arm.sfvein to right_shoulder.sfvein + flow(right_shoulder + 2, CB, bf_ava_hand); // right_shoulder.sfvein to CB + + flow(pelvis, left_thigh, bf_art[11]); // pelvis to left_thigh.art + flow(left_thigh, left_leg, bf_art[12]); // left_thigh.art to left_leg.art + flow(left_leg, left_foot, bf_art[13]); // left_leg.art to left_foot.art + flow(left_foot + 1, left_leg + 1, bf_vein[13]); // left_foot.vein to left_leg.vein + flow(left_leg + 1, left_thigh + 1, bf_vein[12]); // left_leg.vein to left_thigh.vein + flow(left_thigh + 1, pelvis + 1, bf_vein[11]); // left_thigh.vein to pelvis + flow(left_foot + 2, left_leg + 2, bf_ava_foot); // left_foot.sfvein to left_leg.sfvein + flow(left_leg + 2, left_thigh + 2, bf_ava_foot); // left_leg.sfvein to left_thigh.sfvein + flow(left_thigh + 2, pelvis + 1, bf_ava_foot); // left_thigh.vein to pelvis + + flow(pelvis, right_thigh, bf_art[14]); // pelvis to right_thigh.art + flow(right_thigh, right_leg, bf_art[15]); // right_thigh.art to right_leg.art + flow(right_leg, right_foot, bf_art[16]); // right_leg.art to right_foot.art + flow(right_foot + 1, right_leg + 1, bf_vein[16]); // right_foot.vein to right_leg.vein + flow(right_leg + 1, right_thigh + 1, bf_vein[15]); // right_leg.vein to right_thigh.vein + flow(right_thigh + 1, pelvis + 1, bf_vein[14]); // right_thigh.vein to pelvis + flow(right_foot + 2, right_leg + 2, bf_ava_foot); // right_foot.sfvein to right_leg.sfvein + flow(right_leg + 2, right_thigh + 2, bf_ava_foot); // right_leg.sfvein to right_thigh.sfvein + flow(right_thigh + 2, pelvis + 1, bf_ava_foot); // right_thigh.vein to pelvis + + return arr83; +} + +export const [INDEX, VINDEX] = (function () { + const index_by_layer = (layer) => { + const out_index = []; + + for (const bn of BODY_NAMES) { + for (const ln of LAYER_NAMES) { + if (layer.toLowerCase() === ln && IDICT[bn][ln] !== null) { + out_index.push(IDICT[bn][ln]); + } + } + } + + return out_index; + }; + + const valid_index_by_layer = (key) => { + const out_index = []; + + for (let i = 0; i < BODY_NAMES.length; i++) { + const bn = BODY_NAMES[i]; + + if (IDICT[bn][key] !== null) { + out_index.push(i); + } + } + + return out_index; + }; + + const index = {}; + const vindex = {}; + + for (const key of LAYER_NAMES) { + index[key] = index_by_layer(key); + vindex[key] = valid_index_by_layer(key); + } + + return [index, vindex]; +})(); diff --git a/src/jos3_functions/thermoregulation/ava_blood_flow.js b/src/jos3_functions/thermoregulation/ava_blood_flow.js index 6180633..4319e54 100644 --- a/src/jos3_functions/thermoregulation/ava_blood_flow.js +++ b/src/jos3_functions/thermoregulation/ava_blood_flow.js @@ -6,13 +6,14 @@ import { bfb_rate } from "../bfb_rate"; * Calculate areteriovenous anastmoses (AVA) blood flow rate [L/h] based on * Takemori's model, 1995. * - * @param {number[]} err_cr, err_sk - Difference between set-point and body temperatures [°C]. + * @param {number[]} err_cr - Difference between set-point and body temperatures [°C]. + * @param {number[]} err_sk - Difference between set-point and body temperatures [°C]. * @param {number} [height=1.72] - Body height [m] * @param {number} [weight=74.43] - Body weight [kg] * @param {string} [bsa_equation="dubois"] - The equation name of bsa calculation. Choose a name from "dubois", "takahira", "fujimoto", or "kurazumi" * @param {number} [age=20] - age [years] * @param {number} [ci=2.59] - Cardiac index [L/min/m2] - * @returns {number[]} bf_ava_hand, bf_ava_foot : AVA blood flow rate at hand and foot [L/h] + * @returns {[number, number]} bf_ava_hand, bf_ava_foot : AVA blood flow rate at hand and foot [L/h] * */ export function ava_blood_flow( diff --git a/src/jos3_functions/thermoregulation/cr_ms_fat_blood_flow.js b/src/jos3_functions/thermoregulation/cr_ms_fat_blood_flow.js index eb40d66..1d17af6 100644 --- a/src/jos3_functions/thermoregulation/cr_ms_fat_blood_flow.js +++ b/src/jos3_functions/thermoregulation/cr_ms_fat_blood_flow.js @@ -5,15 +5,15 @@ import { BODY_NAMES, IDICT } from "../matrix"; /** * Calculate core, muscle and fat blood flow rate [L/h]. * - * @param {array} q_work - Heat production by work [W]. - * @param {array} q_shiv - Heat production by shivering [W]. - * @param {float} [height=1.72] - Body height [m]. - * @param {float} [weight=74.43] - Body weight [kg]. + * @param {number[]} q_work - Heat production by work [W]. + * @param {number[]} q_shiv - Heat production by shivering [W]. + * @param {number} [height=1.72] - Body height [m]. + * @param {number} [weight=74.43] - Body weight [kg]. * @param {string} [bsa_equation="dubois"] - The equation name of bsa calculation. Choose from "dubois","takahira", "fujimoto", or "kurazumi". - * @param {float} [age=20] - Age [years]. - * @param {float} [ci=2.59] - Cardiac index [L/min/㎡]. + * @param {number} [age=20] - Age [years]. + * @param {number} [ci=2.59] - Cardiac index [L/min/㎡]. * - * @returns {array} - Core, muscle and fat blood flow rate [L/h]. + * @returns {[number[], number[], number[]]} - Core, muscle and fat blood flow rate [L/h]. */ export function cr_ms_fat_blood_flow( q_work, diff --git a/src/jos3_functions/thermoregulation/evaporation.js b/src/jos3_functions/thermoregulation/evaporation.js index 1748ede..3a34d92 100644 --- a/src/jos3_functions/thermoregulation/evaporation.js +++ b/src/jos3_functions/thermoregulation/evaporation.js @@ -3,6 +3,8 @@ import { error_signals } from "./error_signals"; import { bsa_rate } from "../bsa_rate"; import { $map, $max, $min, $array } from "../../supa"; +export const antoine = (x) => Math.E ** (16.6536 - 4030.183 / (x + 235)); + /** * Calculate evaporative heat loss. * @@ -34,8 +36,6 @@ export function evaporation( bsa_equation = JOS3Defaults.bsa_equation, age = JOS3Defaults.age, ) { - const antoine = (x) => Math.E ** (16.6536 - 4030.183 / (x + 235)); - let { wrms, clds } = error_signals(err_sk); let bsar = bsa_rate(height, weight, bsa_equation); let bsa = JOS3Defaults.local_bsa.map((local_bsa) => local_bsa * bsar); diff --git a/src/jos3_functions/thermoregulation/local_mbase.js b/src/jos3_functions/thermoregulation/local_mbase.js index b84cfde..a35eb62 100644 --- a/src/jos3_functions/thermoregulation/local_mbase.js +++ b/src/jos3_functions/thermoregulation/local_mbase.js @@ -9,7 +9,7 @@ import { basal_met } from "./basal_met"; * @param {number} [age=20] - Age [years]. * @param {string} [sex='male'] - Sex (male or female). * @param {string} [bmr_equation='harris-benedict'] - BMR equation to use (harris-benedict or ganpule). - * @returns {Array} mbase - Local basal metabolic rate (Mbase) [W]. + * @returns {number[][]} mbase - Local basal metabolic rate (Mbase) [W]. */ export function local_mbase( height = JOS3Defaults.height, diff --git a/src/jos3_functions/thermoregulation/sum_m.js b/src/jos3_functions/thermoregulation/sum_m.js index f958832..d4b7b8a 100644 --- a/src/jos3_functions/thermoregulation/sum_m.js +++ b/src/jos3_functions/thermoregulation/sum_m.js @@ -9,7 +9,7 @@ import { $map } from "../../supa"; * @param {number[]} q_shiv - Local thermogenesis by shivering [W]. * @param {number[]} q_nst - Local thermogenesis by non-shivering [W]. - * @return {number[]} q_thermogenesis_core, q_thermogenesis_muscle, q_thermogenesis_fat, q_thermogenesis_skin - Total thermogenesis in core, muscle, fat, skin layers [W]. + * @return {number[][]} q_thermogenesis_core, q_thermogenesis_muscle, q_thermogenesis_fat, q_thermogenesis_skin - Total thermogenesis in core, muscle, fat, skin layers [W]. */ export function sum_m(mbase, q_work, q_shiv, q_nst) { let [ diff --git a/src/models/JOS3.js b/src/models/JOS3.js index 6f80145..c0f8dad 100644 --- a/src/models/JOS3.js +++ b/src/models/JOS3.js @@ -1,11 +1,97 @@ import JOS3Defaults from "../jos3_functions/JOS3Defaults"; -import { NUM_NODES } from "../jos3_functions/matrix"; +import { + BODY_NAMES, + INDEX, + local_arr, + NUM_NODES, + vessel_blood_flow, + VINDEX, + whole_body, +} from "../jos3_functions/matrix"; import { bsa_rate } from "../jos3_functions/bsa_rate"; import { local_bsa } from "../jos3_functions/local_bsa"; import { bfb_rate } from "../jos3_functions/bfb_rate"; import { conductance } from "../jos3_functions/conductance"; import { capacity } from "../jos3_functions/capacity"; import { pmv } from "./pmv"; +import { + set_pre_shiv, + shivering, +} from "../jos3_functions/thermoregulation/shivering"; +import { fixed_hc } from "../jos3_functions/thermoregulation/fixed_hc"; +import { conv_coef } from "../jos3_functions/thermoregulation/conv_coef"; +import { fixed_hr } from "../jos3_functions/thermoregulation/fixed_hr"; +import { rad_coef } from "../jos3_functions/thermoregulation/rad_coef"; +import { operative_temp } from "../jos3_functions/thermoregulation/operative_temp"; +import { dry_r } from "../jos3_functions/thermoregulation/dry_r.js"; +import { wet_r } from "../jos3_functions/thermoregulation/wet_r.js"; +import { $array, $average, $index, $map, $reduce, $sum } from "../supa.js"; +import { + antoine, + evaporation, +} from "../jos3_functions/thermoregulation/evaporation.js"; +import { skin_blood_flow } from "../jos3_functions/thermoregulation/skin_blood_flow.js"; +import { ava_blood_flow } from "../jos3_functions/thermoregulation/ava_blood_flow.js"; +import { nonshivering } from "../jos3_functions/thermoregulation/nonshivering.js"; +import { local_mbase } from "../jos3_functions/thermoregulation/local_mbase.js"; +import { local_q_work } from "../jos3_functions/thermoregulation/local_q_work.js"; +import { sum_m } from "../jos3_functions/thermoregulation/sum_m.js"; +import { cr_ms_fat_blood_flow } from "../jos3_functions/thermoregulation/cr_ms_fat_blood_flow.js"; +import { resp_heat_loss } from "../jos3_functions/thermoregulation/resp_heat_loss.js"; +import { sum_bf } from "../jos3_functions/thermoregulation/sum_bf.js"; +import { + add, + clone, + diag, + divide, + dotDivide, + dotMultiply, + identity, + index, + inv, + map, + matrix, + mean, + multiply, + reshape, + subset, + subtract, + sum, + zeros, +} from "mathjs"; +import { basal_met } from "../jos3_functions/thermoregulation/basal_met.js"; + +function _to17array(inp) { + if (typeof inp === "number") { + return Array(17).fill(inp); + } else if (inp instanceof Array) { + if (inp.length === 17) { + return [...inp]; + } else { + throw new Error("The input list is not of length 17"); + } + } else if (typeof inp === "object" && inp !== null) { + return BODY_NAMES.map((key) => inp[key]); + } else { + throw new Error( + "Unsupported input type. Supported types: number, Array, object", + ); + } +} + +function wmean(values, weights) { + if (values.length !== weights.length) { + throw new Error("Values and weights arrays must have the same length."); + } + + // Calculate the sum of all products of values and their corresponding weights + const sumProduct = sum(multiply(values, weights)); + + // Calculate the sum of all weights + const sumWeights = sum(weights); + + return divide(sumProduct, sumWeights); +} export class JOS3 { constructor( @@ -46,20 +132,22 @@ export class JOS3 { this._cap = capacity(height, weight, bsa_equation, age, ci); // Set initial core and skin temperature set points [°C] - this.setpt_cr = Array(17).fill(Default.core_temperature); - this.setpt_sk = Array(17).fill(Default.skin_temperature); + this.setpt_cr = Array(17).fill(JOS3Defaults.core_temperature); + this.setpt_sk = Array(17).fill(JOS3Defaults.skin_temperature); // Initialize body temperature [°C] - this._bodytemp = Array(NUM_NODES).fill(Default.other_body_temperature); + this._bodytemp = Array(NUM_NODES).fill(JOS3Defaults.other_body_temperature); // Initialize environmental conditions and other factors // (Default values of input conditions) - this._ta = Array(17).fill(Default.dry_bulb_air_temperature); - this._tr = Array(17).fill(Default.mean_radiant_temperature); - this._rh = Array(17).fill(Default.relative_humidity); - this._va = Array(17).fill(Default.air_speed); - this._clo = Array(17).fill(Default.clothing_insulation); - this._iclo = Array(17).fill(Default.clothing_vapor_permeation_efficiency); + this._ta = Array(17).fill(JOS3Defaults.dry_bulb_air_temperature); + this._tr = Array(17).fill(JOS3Defaults.mean_radiant_temperature); + this._rh = Array(17).fill(JOS3Defaults.relative_humidity); + this._va = Array(17).fill(JOS3Defaults.air_speed); + this._clo = Array(17).fill(JOS3Defaults.clothing_insulation); + this._iclo = Array(17).fill( + JOS3Defaults.clothing_vapor_permeation_efficiency, + ); this._par = JOS3Defaults.physical_activity_ratio; this._posture = JOS3Defaults.posture; this._hc = null; // Convective heat transfer coefficient @@ -79,7 +167,7 @@ export class JOS3 { }; // Set shivering threshold = 0 - threg.PRE_SHIV = 0; + set_pre_shiv(0); // Initialize history to store model parameters this._history = []; @@ -89,7 +177,7 @@ export class JOS3 { this._cycle = 0; // Cycle time // Reset set-point temperature and save the last model parameters - dictout = this._reset_setpt(JOS3Defaults.physical_activity_ratio); + let dictout = this._reset_setpt(JOS3Defaults.physical_activity_ratio); this._history.append(dictout); } @@ -105,9 +193,9 @@ export class JOS3 { _reset_setpt(par = JOS3Defaults.physical_activity_ratio) { // Set operative temperature under PMV=0 environment // 1 met = 58.15 W/m2 - w_per_m2_to_met = 1 / 58.15; // unit converter W/m2 to met - met = this.bmr * par * w_per_m2_to_met; // [met] - this.to = this._calculate_operative_temp_when_pmv_is_zero((met = met)); + let w_per_m2_to_met = 1 / 58.15; // unit converter W/m2 to met + let met = this.bmr * par * w_per_m2_to_met; // [met] + this.to = this._calculate_operative_temp_when_pmv_is_zero(met); this.rh = JOS3Defaults.relative_humidity; this.v = JOS3Defaults.air_speed; this.clo = JOS3Defaults.clothing_insulation; @@ -163,6 +251,35 @@ export class JOS3 { return to; } + /** + * Run JOS-3 model. + * + * @param {number} times - Number of loops of a simulation. + * @param {number} [dtime=60] - Time delta in seconds. + * @param {boolean} [output=true] - If you don't want to record parameters, set to false. + * @returns {void} + */ + simulate(times, dtime = 60, output = true) { + // Loop through the simulation for the given number of times + for (let i = 0; i < times; i++) { + // Increment the elapsed time by the time delta + // Assuming you have a method or logic to increment `_t` with dtime + this._t += dtime; // This line might need further adjustment depending on how `_t` is used in your code + + // Increment the cycle counter + this._cycle++; + + // Execute the simulation step + // Assuming `_run` is a method on this class + const dictData = this._run(dtime, output); + + // If output is true, append the results to the history + if (output) { + this._history.push(dictData); + } + } + } + /** * Runs the model once and gets the model parameters. * @@ -191,5 +308,724 @@ export class JOS3 { * * @returns {Object} - Output parameters. */ - _run(dtime = 60, passive = false, output = true) {} + _run(dtime = 60, passive = false, output = true) { + // Get core and skin temperatures + let tcr = this.t_core; + let tsk = this.t_skin; + + // Convective and radiative heat transfer coefficients [W/(m2*K)] + let hc = fixed_hc( + conv_coef(this._posture, this._va, this._ta, tsk), + this._va, + ); + let hr = fixed_hr(rad_coef(this._posture)); + + // Manually set convective and radiative heat transfer coefficients if necessary + if (this._hc !== null) { + hc = this._hc; + } + if (this._hr !== null) { + hr = this._hr; + } + + // Compute operative temp. [°C], clothing heat and evaporative resistance [m2.K/W], [m2.kPa/W] + // Operative temp. [°C] + let to = operative_temp(this._ta, this._tr, hc, hr); + // Clothing heat resistance [m2.K/W] + let r_t = dry_r(hc, hr, this._clo); + // Clothing evaporative resistance [m2.kPa/W] + let r_et = wet_r(hc, this._clo, this._iclo); + + // Thermoregulation + // 1) Sweating + // 2) Vasoconstriction, Vasodilation + // 3) Shivering and non-shivering thermogenesis + + // Compute the difference between the set-point temperature and body temperatures + // and other thermoregulation parameters. + // If running a passive model, the set-point temperature of thermoregulation is + // set to the current body temperature. + + // set-point temperature for thermoregulation + + let setpt_sk, setpt_cr; + if (passive) { + setpt_cr = tcr; + setpt_sk = tsk; + } else { + setpt_cr = this.setpt_cr; + setpt_sk = this.setpt_sk; + } + + // Error signal = Difference between set-point and body temperatures + let err_cr = subtract(tcr, setpt_cr); + let err_sk = subtract(tsk, setpt_sk); + + // SWEATING THERMOREGULATION + let { wet, e_sk, e_max, e_sweat } = evaporation( + err_cr, + err_sk, + tsk, + this._ta, + this._rh, + r_et, + this._height, + this._weight, + this._bsa_equation, + this._age, + ); + + // VASOCONSTRICTION, VASODILATION + let bf_skin = skin_blood_flow( + err_cr, + err_sk, + this._height, + this._weight, + this._bsa_equation, + this._age, + this._ci, + ); + + let bloodFlows = ava_blood_flow( + err_cr, + err_sk, + this._height, + this._weight, + this._bsa_equation, + this._age, + this._ci, + ); + let bf_ava_hand = bloodFlows[0]; + let bf_ava_foot = bloodFlows[1]; + + if (this.options.ava_zero && passive) { + bf_ava_hand = 0; + bf_ava_foot = 0; + } + + // SHIVERING AND NON-SHIVERING + let q_shiv = shivering( + err_cr, + err_sk, + tcr, + tsk, + this._height, + this._weight, + this._bsa_equation, + this._age, + this._sex, + dtime, + this.options, + ); + + // Calculate non-shivering thermogenesis (NST) [W] + let q_nst; + if (this.options.nonshivering_thermogenesis) { + q_nst = nonshivering( + err_sk, + this._height, + this._weight, + this._bsa_equation, + this._age, + this.options.cold_acclimated, + this.options.bat_positive, + ); + } else { + q_nst = new Array(17).fill(0); + } + + // Thermogenesis + + // Calculate local basal metabolic rate (BMR) [W] + let q_bmr_local = local_mbase( + this._height, + this._weight, + this._age, + this._sex, + this._bmr_equation, + ); + + // Calculate overall basal metabolic rate (BMR) [W] + let q_bmr_total = q_bmr_local + .map((m) => m.reduce((a, b) => a + b, 0)) + .reduce((a, b) => a + b, 0); + + // Calculate thermogenesis by work [W] + let q_work = local_q_work(q_bmr_total, this._par); + + // Calculate the sum of thermogenesis in core, muscle, fat, skin [W] + let [ + q_thermogenesis_core, + q_thermogenesis_muscle, + q_thermogenesis_fat, + q_thermogenesis_skin, + ] = sum_m(q_bmr_local, q_work, q_shiv, q_nst); + + let q_thermogenesis_total = + q_thermogenesis_core.reduce((a, b) => a + b, 0) + + q_thermogenesis_muscle.reduce((a, b) => a + b, 0) + + q_thermogenesis_fat.reduce((a, b) => a + b, 0) + + q_thermogenesis_skin.reduce((a, b) => a + b, 0); + + // Others + + // Calculate blood flow in core, muscle, fat [L/h] + let [bf_core, bf_muscle, bf_fat] = cr_ms_fat_blood_flow( + q_work, + q_shiv, + this._height, + this._weight, + this._bsa_equation, + this._age, + this._ci, + ); + + // Calculate heat loss by respiratory + let p_a = (antoine(this._ta) * this._rh) / 100; + let [res_sh, res_lh] = resp_heat_loss( + this._ta[0], + p_a[0], + q_thermogenesis_total, + ); + + // Calculate sensible heat loss [W] + let shl_sk = ((tsk - to) / r_t) * this._bsa; + + // Calculate cardiac output [L/h] + let co = sum_bf( + bf_core, + bf_muscle, + bf_fat, + bf_skin, + bf_ava_hand, + bf_ava_foot, + ); + + // Calculate weight loss rate by evaporation [g/sec] + let wlesk = (e_sweat + 0.06 * e_max) / 2418; + let wleres = res_lh / 2418; + + // Matrix + // This code section is focused on constructing and calculating + // various matrices required for modeling the thermoregulation + // of the human body. + // Since JOS-3 has 85 thermal nodes, the determinant of 85*85 is to be solved. + + // Matrix A = Matrix for heat exchange due to blood flow and conduction occurring between tissues + + // Calculates the blood flow in arteries and veins for core, muscle, fat, skin, + // and arteriovenous anastomoses (AVA) in hands and feet, + // and combines them into two arrays: + // 1) bf_local for the local blood flow and 2) bf_whole for the whole-body blood flow. + // These arrays are then combined to form arr_bf. + + let [bf_art, bf_vein] = vessel_blood_flow( + bf_core, + bf_muscle, + bf_fat, + bf_skin, + bf_ava_hand, + bf_ava_foot, + ); + let bf_local = local_arr( + bf_core, + bf_muscle, + bf_fat, + bf_skin, + bf_ava_hand, + bf_ava_foot, + ); + let bf_whole = whole_body(bf_art, bf_vein, bf_ava_hand, bf_ava_foot); + + // Initialize arr_bf with zeros using mathjs matrix + let arr_bf = zeros(NUM_NODES, NUM_NODES); + + // Add bf_local and bf_whole to arr_bf + arr_bf = add(arr_bf, bf_local); + arr_bf = add(arr_bf, bf_whole); + + // Adjusts the units of arr_bf from [W/K] to [/sec] and then to [-] + // by dividing by the heat capacity this._cap and multiplying by the time step dtime. + let reshaped_cap = reshape(this._cap, [NUM_NODES, 1]); + arr_bf = dotDivide(arr_bf, reshaped_cap); // Change unit [W/K] to [/sec] + arr_bf = multiply(arr_bf, dtime); // Change unit [/sec] to [-] + + // Performs similar unit conversions for the convective heat transfer coefficient array arr_cdt + // (also divided by this._cap and multiplied by dtime). + let arr_cdt = clone(this._cdt); + arr_cdt = dotDivide(arr_cdt, reshaped_cap); // Change unit [W/K] to [/sec] + arr_cdt = multiply(arr_cdt, dtime); // Change unit [/sec] to [-] + + // Matrix B = Matrix for heat transfer between skin and environment + let arr_b = zeros(NUM_NODES); + + // Updating arr_b based on INDEX["skin"] + INDEX["skin"].forEach((idx, i) => { + arr_b[idx] += (1 / r_t) * this._bsa[i]; + }); + + // Using mathjs library for array operations + arr_b = dotDivide(arr_b, this._cap); + arr_b = multiply(arr_b, dtime); + + // Calculates the off-diagonal and diagonal elements of the matrix A + let arr_a_tria = subtract(multiply(-1, arr_cdt), arr_bf); + + let arr_a_dia = add(arr_cdt, arr_bf); + arr_a_dia = add(sum(arr_a_dia, 1), arr_b); // Assuming 'axis=1' means summing along rows + arr_a_dia = diag(arr_a_dia); + arr_a_dia = add(arr_a_dia, identity(NUM_NODES)); + + let arr_a = add(arr_a_tria, arr_a_dia); + let arr_a_inv = inv(arr_a); + + // Matrix Q = Matrix for heat generation rate from thermogenesis, respiratory, sweating, + // and extra heat gain processes in different body parts. + let arr_q = zeros(NUM_NODES); + + // Thermogensis + arr_q = add( + arr_q, + subset(zeros(NUM_NODES), index(INDEX["core"]), q_thermogenesis_core), + ); + + arr_q = add( + arr_q, + subset( + zeros(NUM_NODES), + index(INDEX["muscle"]), + subset(q_thermogenesis_muscle, index(VINDEX["muscle"])), + ), + ); + arr_q = add( + arr_q, + subset( + zeros(NUM_NODES), + index(INDEX["fat"]), + subset(q_thermogenesis_fat, index(VINDEX["fat"])), + ), + ); + arr_q = add( + arr_q, + subset(zeros(NUM_NODES), index(INDEX["skin"]), q_thermogenesis_skin), + ); + + // Respiratory [W] + arr_q[INDEX["core"][2]] -= res_sh + res_lh; // chest core + + // Sweating [W] + arr_q = subtract( + arr_q, + subset(zeros(NUM_NODES), index(INDEX["skin"]), e_sk), + ); + + // Extra heat gain [W] + arr_q = add(arr_q, this.ex_q); + + // Adjust units + arr_q = dotDivide(arr_q, this._cap); // Change unit [W]/[J/K] to [K/sec] + arr_q = multiply(arr_q, dtime); // Change unit [K/sec] to [K] + + // Boundary matrix [°C] + let arr_to = zeros(NUM_NODES); + arr_to = add(arr_to, subset(zeros(NUM_NODES), index(INDEX["skin"]), to)); + + // Combines the current body temperature, the boundary matrix, and the heat generation matrix + // to calculate the new body temperature distribution (arr). + let arr = add(add(this._bodytemp, dotMultiply(arr_b, arr_to)), arr_q); + + console.log(arr_b); + console.log(arr_to); + console.log(arr_q); + console.log(arr_a_inv); + console.log(arr); + + // ------------------------------------------------------------------ + // New body temp. [°C] + // ------------------------------------------------------------------ + this._bodytemp = dotMultiply(arr_a_inv, arr); + + // ------------------------------------------------------------------ + // Output parameters + // ------------------------------------------------------------------ + let dict_out = {}; + if (output) { + // Default output + dict_out["cycle_time"] = this._cycle; + dict_out["simulation_time"] = this._t; + dict_out["dt"] = dtime; + dict_out["t_skin_mean"] = this.t_skin_mean; + dict_out["t_skin"] = this.t_skin; + dict_out["t_core"] = this.t_core; + dict_out["w_mean"] = $average(wet, JOS3Defaults.local_bsa); + dict_out["w"] = wet; + dict_out["weight_loss_by_evap_and_res"] = $sum(wlesk) + wleres; + dict_out["cardiac_output"] = co; + dict_out["q_thermogenesis_total"] = q_thermogenesis_total; + dict_out["q_res"] = res_sh + res_lh; + dict_out["q_skin2env"] = shl_sk + e_sk; + } + + let detail_out = {}; + if (this._ex_output && output) { + detail_out["name"] = this.model_name; + detail_out["height"] = this._height; + detail_out["weight"] = this._weight; + detail_out["bsa"] = this._bsa; + detail_out["fat"] = this._fat; + detail_out["sex"] = this._sex; + detail_out["age"] = this._age; + detail_out["t_core_set"] = setpt_cr; + detail_out["t_skin_set"] = setpt_sk; + detail_out["t_cb"] = this.t_cb; + detail_out["t_artery"] = this.t_artery; + detail_out["t_vein"] = this.t_vein; + detail_out["t_superficial_vein"] = this.t_superficial_vein; + detail_out["t_muscle"] = this.t_muscle; + detail_out["t_fat"] = this.t_fat; + detail_out["to"] = to; + detail_out["r_t"] = r_t; + detail_out["r_et"] = r_et; + detail_out["tdb"] = this._ta.slice(); + detail_out["tr"] = this._tr.slice(); + detail_out["rh"] = this._rh.slice(); + detail_out["v"] = this._va.slice(); + detail_out["par"] = this._par; + detail_out["clo"] = this._clo.slice(); + detail_out["e_skin"] = e_sk; + detail_out["e_max"] = e_max; + detail_out["e_sweat"] = e_sweat; + detail_out["bf_core"] = bf_core; + detail_out["bf_muscle"] = bf_muscle[VINDEX["muscle"]]; + detail_out["bf_fat"] = bf_fat[VINDEX["fat"]]; + detail_out["bf_skin"] = bf_skin; + detail_out["bf_ava_hand"] = bf_ava_hand; + detail_out["bf_ava_foot"] = bf_ava_foot; + detail_out["q_bmr_core"] = q_bmr_local[0]; + detail_out["q_bmr_muscle"] = q_bmr_local[1][VINDEX["muscle"]]; + detail_out["q_bmr_fat"] = q_bmr_local[2][VINDEX["fat"]]; + detail_out["q_bmr_skin"] = q_bmr_local[3]; + detail_out["q_work"] = q_work; + detail_out["q_shiv"] = q_shiv; + detail_out["q_nst"] = q_nst; + detail_out["q_thermogenesis_core"] = q_thermogenesis_core; + detail_out["q_thermogenesis_muscle"] = + q_thermogenesis_muscle[VINDEX["muscle"]]; + detail_out["q_thermogenesis_fat"] = q_thermogenesis_fat[VINDEX["fat"]]; + detail_out["q_thermogenesis_skin"] = q_thermogenesis_skin; + dict_out["q_skin2env_sensible"] = shl_sk; + dict_out["q_skin2env_latent"] = e_sk; + dict_out["q_res_sensible"] = res_sh; + dict_out["q_res_latent"] = res_lh; + } + + if (this._ex_output === "all") { + dict_out = Object.assign({}, dict_out, detail_out); + } else if (Array.isArray(this._ex_output)) { + let out_keys = Object.keys(detail_out); + this._ex_output.forEach((key) => { + if (out_keys.includes(key)) { + dict_out[key] = detail_out[key]; + } + }); + } + + return dict_out; + } + + dict_results() { + if (!this._history || this._history.length === 0) { + console.log("The model has no data."); + return null; + } + + const checkWordContain = (word, ...args) => { + for (let arg of args) { + if (word.includes(arg)) { + return true; + } + } + return false; + }; + + let key2keys = {}; // Map for column keys + + for (let key in this._history[0]) { + let value = this._history[0][key]; + let keys = []; + try { + let length = value.length; + if (typeof value === "string") { + keys = [key]; + } else if (checkWordContain(key, "sve", "sfv", "superficialvein")) { + keys = VINDEX["sfvein"].map((i) => `${key}_${BODY_NAMES[i]}`); + } else if (checkWordContain(key, "ms", "muscle")) { + keys = VINDEX["muscle"].map((i) => `${key}_${BODY_NAMES[i]}`); + } else if (checkWordContain(key, "fat")) { + keys = VINDEX["fat"].map((i) => `${key}_${BODY_NAMES[i]}`); + } else if (length === 17) { + keys = BODY_NAMES.map((bn) => `${key}_${bn}`); + } else { + keys = Array.from( + { length: length }, + (_, i) => `${key}_${BODY_NAMES[i]}`, + ); + } + } catch (error) { + if (error instanceof TypeError) { + keys = [key]; + } + } + key2keys[key] = keys; + } + + let data = []; + for (let dictout of this._history) { + let row = {}; + for (let key in dictout) { + let keys = key2keys[key]; + let values = keys.length === 1 ? [dictout[key]] : dictout[key]; + row = { + ...row, + ...Object.fromEntries(keys.map((k, index) => [k, values[index]])), + }; + } + data.push(row); + } + + let out_dict = Object.fromEntries(Object.keys(data[0]).map((k) => [k, []])); + for (let row of data) { + for (let k in data[0]) { + out_dict[k].push(row[k]); + } + } + + return out_dict; + } + + /** + * Set extra heat gain by tissue name. + * + * @param {string} tissue - Tissue name. Can be "core", "skin", "artery"..., + * If you want to set value to head muscle and other segment's core, + * use "all_muscle". + * @param {(number|number[])} value - Heat gain [W] + * @returns {number[]} Extra heat gain of model. + */ + _set_ex_q(tissue, value) { + this.ex_q[INDEX[tissue]] = value; + return self.ex_q; + } + + get tdb() { + return this._ta; + } + + set tdb(inp) { + this._ta = _to17array(inp); + } + + get tr() { + return this._tr; + } + + set tr(inp) { + this._tr = _to17array(inp); + } + + get to() { + const hc = fixed_hc( + conv_coef(this._posture, this._va, this._ta, this.t_skin), + self._va, + ); + + const hr = fixed_hr(rad_coef(this._posture)); + + return operative_temp(this._ta, this._tr, hc, hr); + } + + set to(inp) { + this._ta = _to17array(inp); + this._tr = _to17array(inp); + } + + get rh() { + return this._rh; + } + + set rh(inp) { + this._rh = _to17array(inp); + } + + get v() { + return this._va; + } + + set v(inp) { + this._va = _to17array(inp); + } + + get posture() { + return this._posture; + } + + set posture(inp) { + if (inp === 0) { + this._posture = "standing"; + } else if (inp === 1) { + this._posture = "sitting"; + } else if (inp === 2) { + this._posture = "lying"; + } else if (typeof inp === "string") { + const lowerInput = inp.toLowerCase(); + if (lowerInput === "standing") { + this._posture = "standing"; + } else if (["sitting", "sedentary"].includes(lowerInput)) { + this._posture = "sitting"; + } else if (["lying", "supine"].includes(lowerInput)) { + this._posture = "lying"; + } + } else { + this._posture = "standing"; + console.log('posture must be 0="standing", 1="sitting" or 2="lying".'); + console.log('posture was set "standing".'); + } + } + + get clo() { + return this._clo; + } + + set clo(inp) { + this._clo = _to17array(inp); + } + + get par() { + return this._par; + } + + set par(inp) { + this._par = inp; + } + + get body_temp() { + return this._bodytemp; + } + + set body_temp(inp) { + this._bodytemp = inp.slice(); + } + + get bsa() { + return this._bsa.slice(); + } + + get r_t() { + const hc = fixed_hc( + conv_coef(this._posture, this._va, this._ta, this.t_skin), + self._va, + ); + + const hr = fixed_hr(rad_coef(this._posture)); + + return dry_r(hc, hr, this._clo); + } + + get r_et() { + const hc = fixed_hc( + conv_coef(this._posture, this._va, this._ta, this.t_skin), + self._va, + ); + + const hr = fixed_hr(rad_coef(this._posture)); + + return wet_r(hc, this._clo, this._iclo); + } + + get w() { + const err_cr = subtract(this.t_core, this.setpt_cr); + const err_sk = subtract(this.t_skin, this.setpt_sk); + const { wet } = evaporation( + err_cr, + err_sk, + this._ta, + this._rh, + this.r_et, + this._bsa_rate, + this._age, + ); + + return wet; + } + + get w_mean() { + return $average(this.w, JOS3Defaults.local_bsa); + } + + get t_skin_mean() { + return divide( + sum( + multiply( + subset(this._bodytemp, index(INDEX["skin"])), + JOS3Defaults.local_bsa, + ), + ), + sum(JOS3Defaults.local_bsa), + ); + } + + get t_skin() { + return $index(this._bodytemp, INDEX["skin"]); + } + + set t_skin(inp) { + this._bodytemp[INDEX["skin"]] = _to17array(inp); + } + + get t_core() { + return $index(this._bodytemp, INDEX["core"]); + } + + get t_cb() { + return this._bodytemp[0]; + } + + get t_artery() { + return $index(this._bodytemp, INDEX["artery"]); + } + + get t_vein() { + return $index(this._bodytemp, INDEX["vein"]); + } + + get t_superficial_vein() { + return $index(this._bodytemp, INDEX["sfvein"]); + } + + get t_muscle() { + return $index(this._bodytemp, INDEX["muscle"]); + } + + get t_fat() { + return $index(this._bodytemp, INDEX["fat"]); + } + + get body_names() { + return BODY_NAMES; + } + + get results() { + return this.dict_results(); + } + + get bmr() { + const tcr = basal_met( + this._height, + this._weight, + this._age, + this._sex, + this._bmr_equation, + ); + return tcr / $sum(this.bsa); + } } diff --git a/src/supa.js b/src/supa.js index 016fc99..7fb4bf5 100644 --- a/src/supa.js +++ b/src/supa.js @@ -1,7 +1,9 @@ export function $map(arrays, op) { const mapLength = arrays[0].length; if (!arrays.every((a) => a.length === mapLength)) { - throw new Error("cannot $map over arrays of different lengths"); + throw new Error( + `cannot $map over arrays of different lengths (${arrays[1].length} vs ${mapLength})`, + ); } const result = []; @@ -13,14 +15,31 @@ export function $map(arrays, op) { return result; } +/** + * @template T + * @param {number} length + * @param {T} value + * + * @return {T[]} + */ export function $array(length, value) { return Array(length).fill(value); } +/** + * @template T + * @param arrays {T[][]} + * @param op {(reduced: T, items: T[]) => T} + * @param initial {T} + * + * @return {T} + */ export function $reduce(arrays, op, initial) { const reduceLength = arrays[0].length; if (!arrays.every((a) => a.length === reduceLength)) { - throw new Error("cannot $reduce over arrays of different lengths"); + throw new Error( + `cannot $reduce over arrays of different lengths (${a.length} vs ${reduceLength})`, + ); } let reduced = initial; @@ -33,13 +52,12 @@ export function $reduce(arrays, op, initial) { } export function $average(array, weights) { - let weightSum = weights.reduce((t, c) => t + c, 0); return ( $reduce( [array, weights], (reduced, [array, weights]) => reduced + array * weights, 0, - ) / weightSum + ) / $sum(weights) ); } @@ -60,3 +78,7 @@ export function $max(array, sentinel) { export function $min(array, sentinel) { return array.map((array) => (array > sentinel ? sentinel : array)); } + +export function $index(array, indicies) { + return array.filter((_, i) => indicies.includes(i)); +} diff --git a/tests/models/JOS3.test.js b/tests/models/JOS3.test.js new file mode 100644 index 0000000..963d54d --- /dev/null +++ b/tests/models/JOS3.test.js @@ -0,0 +1,91 @@ +import { JOS3 } from "../../src/models/JOS3.js"; +import { describe, it, expect } from "@jest/globals"; + +describe("JOS3", () => { + it("should return the correct value given certain defaults", () => { + // TODO: Fix dot product calculation for _bodytemp - then uncomment this test (everything else works when this is stubbed) + // const model = new JOS3(); + // + // const expected = { + // cardiac_output: [309.3525279636547], + // cycle_time: [0], + // dt: [60000], + // q_res: [8.222503033578032], + // q_skin2env_back: [8.14893481599888], + // q_skin2env_chest: [9.319394672853791], + // q_skin2env_head: [8.220790562331835], + // q_skin2env_left_arm: [3.3060484333347873], + // q_skin2env_left_foot: [2.847055691927899], + // q_skin2env_left_hand: [2.820093545556136], + // q_skin2env_left_leg: [5.065172487729327], + // q_skin2env_left_shoulder: [5.495164499822654], + // q_skin2env_left_thigh: [10.530261655946674], + // q_skin2env_neck: [2.205000373975029], + // q_skin2env_pelvis: [13.708771815913414], + // q_skin2env_right_arm: [3.306048433334798], + // q_skin2env_right_foot: [2.8470556919279275], + // q_skin2env_right_hand: [2.820093545556159], + // q_skin2env_right_leg: [5.065172487729508], + // q_skin2env_right_shoulder: [5.495164499822711], + // q_skin2env_right_thigh: [10.530261655946806], + // q_thermogenesis_total: [109.95300054410399], + // simulation_time: [0], + // t_core_back: [37.10669294345571], + // t_core_chest: [37.046763146185384], + // t_core_head: [37.27948664648618], + // t_core_left_arm: [35.71685683735247], + // t_core_left_foot: [34.602386529646225], + // t_core_left_hand: [34.90385741306187], + // t_core_left_leg: [36.32476524669076], + // t_core_left_shoulder: [36.25197837683718], + // t_core_left_thigh: [36.67563450105145], + // t_core_neck: [36.81782751853464], + // t_core_pelvis: [37.25856827899341], + // t_core_right_arm: [35.7168568373525], + // t_core_right_foot: [34.602386529646274], + // t_core_right_hand: [34.903857413061935], + // t_core_right_leg: [36.32476524669116], + // t_core_right_shoulder: [36.25197837683733], + // t_core_right_thigh: [36.675634501051476], + // t_skin_back: [34.52453966623942], + // t_skin_chest: [34.61642143810777], + // t_skin_head: [34.98583652881713], + // t_skin_left_arm: [33.857087457104285], + // t_skin_left_foot: [34.11693174783236], + // t_skin_left_hand: [34.308156507844046], + // t_skin_left_leg: [33.95264011542674], + // t_skin_left_shoulder: [34.25228779730484], + // t_skin_left_thigh: [34.15023407024665], + // t_skin_mean: [34.46418466255792], + // t_skin_neck: [35.10668504573099], + // t_skin_pelvis: [35.844809578238284], + // t_skin_right_arm: [33.8570874571043], + // t_skin_right_foot: [34.11693174783242], + // t_skin_right_hand: [34.308156507844096], + // t_skin_right_leg: [33.95264011542693], + // t_skin_right_shoulder: [34.2522877973049], + // t_skin_right_thigh: [34.15023407024672], + // w_back: [0.06], + // w_chest: [0.06], + // w_head: [0.06], + // w_left_arm: [0.06], + // w_left_foot: [0.06], + // w_left_hand: [0.06], + // w_left_leg: [0.06], + // w_left_shoulder: [0.06], + // w_left_thigh: [0.06], + // w_mean: [0.05999999999999999], + // w_neck: [0.06], + // w_pelvis: [0.06], + // w_right_arm: [0.06], + // w_right_foot: [0.06], + // w_right_hand: [0.06], + // w_right_leg: [0.06], + // w_right_shoulder: [0.06], + // w_right_thigh: [0.06], + // weight_loss_by_evap_and_res: [0.011094527785290931], + // }; + // + // expect(model.dict_results()).toBe(expected); + }); +});