diff --git a/.npmrc b/.npmrc index ded82e2f..5ccbdfc6 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ auto-install-peers = true +@jsr:registry=https://npm.jsr.io diff --git a/packages/fast-class/asconfig.json b/packages/fast-class/asconfig.json new file mode 100644 index 00000000..87765978 --- /dev/null +++ b/packages/fast-class/asconfig.json @@ -0,0 +1,22 @@ +{ + "targets": { + "debug": { + "outFile": "build/debug.wasm", + "textFile": "build/debug.wat", + "sourceMap": true, + "debug": true + }, + "release": { + "outFile": "build/release.wasm", + "textFile": "build/release.wat", + "sourceMap": true, + "optimizeLevel": 3, + "shrinkLevel": 0, + "converge": false, + "noAssert": false + } + }, + "options": { + "bindings": "esm" + } +} \ No newline at end of file diff --git a/packages/fast-class/assembly/index.ts b/packages/fast-class/assembly/index.ts new file mode 100644 index 00000000..f77f90c2 --- /dev/null +++ b/packages/fast-class/assembly/index.ts @@ -0,0 +1,221 @@ +// The entry file of your WebAssembly module. + +// Constants +const rho: f32 = 1.2; /** Density of air [kg m-3] */ +const cp: f32 = 1005.0; /** Specific heat of dry air [J kg-1 K-1] */ + +class Config { + /** + * Initial ABL height [m] + */ + h_0: f32 = 200; + /** + * Initial mixed-layer potential temperature [K] + */ + theta_0: f32 = 288; + /** + * Initial temperature jump at h [K] + */ + dtheta_0: f32 = 1; + /** + * Initial mixed-layer specific humidity [kg kg-1] + */ + q_0: f32 = 0.008; + /** + * Initial specific humidity jump at h [kg kg-1] + */ + dq_0: f32 = -0.001; + /** + * Time step [s] + */ + dt: f32 = 60; + /** + * Total run time [s] + */ + runtime: f32 = 43200; + /** + * Surface kinematic heat flux [K m s-1] + */ + wtheta: f32 = 0.1; + /** + * Advection of heat [K s-1] + */ + advtheta: f32 = 0; + /** + * Free atmosphere potential temperature lapse rate [K m-1] + */ + gammatheta: f32 = 0.006; + /** + * Surface kinematic moisture flux [kg kg-1 m s-1] + */ + wq: f32 = 0.0001; + /** + * Advection of moisture [kg kg-1 s-1] + */ + advq: f32 = 0; + /** + * Free atmosphere specific humidity lapse rate [kg kg-1 m-1] + */ + gammaq: f32 = 0; + /** + * Horizontal large-scale divergence of wind [s-1] + */ + divU: f32 =0; + /** + * Entrainment ratio for virtual heat [-] + */ + beta: f32 = 0.2; +} + +/** + * CLASS model definition + * @property _cfg: object containing the model settings + * @property h: ABL height [m] + * @property theta: Mixed-layer potential temperature [K] + * @property dtheta: Temperature jump at h [K] + * @property q: Mixed-layer specific humidity [kg kg-1] + * @property dq: Specific humidity jump at h [kg kg-1] + * @property t: Model time [s] + */ + +class CLASS { + _cfg: Config; + h: f32; + theta: f32; + dtheta: f32; + q: f32; + dq: f32; + t: f32 = 0; + + /** + * Create object and initialize the model state + * @param config Model settings + */ + constructor(config: Config) { + this._cfg = config; + this.h = config.h_0; + this.theta = config.theta_0; + this.dtheta = config.dtheta_0; + this.q = config.q_0; + this.dq = config.dq_0; + } + /** + * Integrate mixed layer + */ + update(): void { + const dt = this._cfg.dt; + this.h += dt * this.htend; + this.theta += dt * this.thetatend; + this.dtheta += dt * this.dthetatend; + this.q += dt * this.qtend; + this.dq += dt * this.dqtend; + this.t += dt; + } + + /** Tendency of CLB [m s-1]*/ + get htend(): f32 { + return this.we + this.ws; + } + + /** Tendency of mixed-layer potential temperature [K s-1] */ + get thetatend(): f32 { + return ( + (this._cfg.wtheta - this.wthetae) / this.h + + this._cfg.advtheta + ); + } + + /** Tendency of potential temperature jump at h [K s-1] */ + get dthetatend(): f32 { + const w_th_ft: f32 = 0.0; // TODO: add free troposphere switch + return this._cfg.gammatheta * this.we - this.thetatend + w_th_ft; + } + + /** Tendency of mixed-layer specific humidity [kg kg-1 s-1] */ + get qtend(): f32 { + return ( + (this._cfg.wq - this.wqe) / this.h + this._cfg.advq + ); + } + + /** Tendency of specific humidity jump at h[kg kg-1 s-1] */ + get dqtend(): f32 { + const w_q_ft: f32 = 0; // TODO: add free troposphere switch + return this._cfg.gammaq - this.qtend + w_q_ft; + } + + /** Entrainment velocity [m s-1]. */ + get we(): f32 { + // TODO add sw_shearwe + let we = -this.wthetave / this.dthetav; + + // Don't allow boundary layer shrinking + if (we < 0) { + we = 0; + } + return we; + } + + /** Large-scale vertical velocity [m s-1]. */ + get ws(): f32 { + return -this._cfg.divU * this.h; + } + + /** Entrainment kinematic heat flux [K m s-1]. */ + get wthetae(): f32 { + return -this.we * this.dtheta; + } + + /** Entrainment moisture flux [kg kg-1 m s-1]. */ + get wqe(): f32 { + return -this.we * this.dq; + } + + /** Entrainment kinematic virtual heat flux [K m s-1]. */ + get wthetave(): f32 { + return -this._cfg.beta * this.wthetav; + } + + /** Virtual temperature jump at h [K]. */ + get dthetav(): f32 { + return ( + (this.theta + this.dtheta) * (1.0 + 0.61 * (this.q + this.dq)) - + this.theta * (1.0 + 0.61 * this.q) + ); + } + + /** Surface kinematic virtual heat flux [K m s-1]. */ + get wthetav(): f32 { + return ( + this._cfg.wtheta + 0.61 * this.theta * this._cfg.wq + ); + } +} + +export function runner(h_0: f32, runtime: f32): Array { + // TODO expose config as argument + // TODO expose output varnames as argument + const config = new Config(); + config.h_0 = h_0; + config.runtime = runtime; + const model = new CLASS(config); + const outputSize: i32 = i32(config.runtime / config.dt); + const times: Float32Array = new Float32Array(outputSize); + const heights: Float32Array = new Float32Array(outputSize); + + let index: i32 = 0; + while (model.t < config.runtime) { + model.update(); + + if (model.t % 60 === 0) { + times[index] = model.t; + heights[index] = model.h; + index++; + } + } + + const output = new Array(2); + output[0] = times; + output[1] = heights; + return output; +} diff --git a/packages/fast-class/assembly/tsconfig.json b/packages/fast-class/assembly/tsconfig.json new file mode 100644 index 00000000..7e390865 --- /dev/null +++ b/packages/fast-class/assembly/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../node_modules/.pnpm/assemblyscript@0.27.30/node_modules/assemblyscript/std/assembly.json", + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/fast-class/package.json b/packages/fast-class/package.json new file mode 100644 index 00000000..c770d75f --- /dev/null +++ b/packages/fast-class/package.json @@ -0,0 +1,26 @@ +{ + "name": "@classmodel/fast-class", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "node tests", + "asbuild:debug": "asc assembly/index.ts --target debug", + "asbuild:release": "asc assembly/index.ts --target release", + "asbuild": "npm run asbuild:debug && npm run asbuild:release", + "start": "npx serve ." + }, + "author": "Stefan Verhoeven", + "license": "GPL-3.0-only", + "description": "", + "devDependencies": { + "@classmodel/class": "npm:@jsr/classmodel__class@0.0.3", + "assemblyscript": "^0.27.30" + }, + "type": "module", + "exports": { + ".": { + "import": "./build/release.js", + "types": "./build/release.d.ts" + } + } +} \ No newline at end of file diff --git a/packages/fast-class/tests/index.js b/packages/fast-class/tests/index.js new file mode 100644 index 00000000..a784e3bd --- /dev/null +++ b/packages/fast-class/tests/index.js @@ -0,0 +1,76 @@ +import { describe, test } from "node:test"; +import assert from "node:assert"; +import { runner as asRunner } from '../build/debug.js'; +import { CLASS } from '@classmodel/class/class'; + +describe('asRunner', () => { + test('should run', () => { + console.time('asRunner'); + const [times, heights] = asRunner(200, 100 * 12 * 3600); + console.timeEnd('asRunner'); + // assert.strictEqual(times.length, 720); + // assert.strictEqual(heights.length, 720); + }); +}) + +function jsRunner(h_0, runtime) { + const config = { + title: "Test", + description: "Test", + initialState: { + h_0: 200, + theta_0: 288, + dtheta_0: 1, + q_0: 0.008, + dq_0: -0.001, + }, + timeControl: { dt: 60, runtime: 43200 }, + mixedLayer: { + wtheta: 0.1, + advtheta: 0, + gammatheta: 0.006, + wq: 0.0001, + advq: 0, + gammaq: 0, + divU: 0, + beta: 0.2, + }, + }; + config.timeControl.h_0 = h_0; + config.timeControl.runtime = runtime; + const model = new CLASS(config); + const outputSize = Math.floor(config.timeControl.runtime / config.timeControl.dt); + const times = new Float32Array(outputSize); + const heights = new Float32Array(outputSize); + + let index = 0; + while (model.t < config.timeControl.runtime) { + model.update(); + if (model.t % 60 === 0) { + times[index] = model.t; + heights[index] = model.h; + index++; + } + } + + const output = new Array(2); + output[0] = times; + output[1] = heights; + return output; +} + +/** + * + * mkdir node_modules/@classmodel + * ln -s ../../../class node_modules/@classmodel/ + */ +describe('jsRunner', () => { + test('should run', () => { + console.time('jsRunner'); + const [times, heights] = jsRunner(200, 100 * 12 * 3600); + console.timeEnd('jsRunner'); + // assert.strictEqual(times.length, 720); + // assert.strictEqual(heights.length, 720); + // console.log(times, heights); + }); +}) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 475ff717..14cd7707 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,6 +107,15 @@ importers: specifier: ^5.3.3 version: 5.3.3 + packages/fast-class: + devDependencies: + '@classmodel/class': + specifier: npm:@jsr/classmodel__class@0.0.3 + version: '@jsr/classmodel__class@0.0.3' + assemblyscript: + specifier: ^0.27.30 + version: 0.27.30 + packages: '@alloc/quick-lru@5.2.0': @@ -618,6 +627,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsr/classmodel__class@0.0.3': + resolution: {integrity: sha512-P9cS2UekzYQI/EqE1OjAhsqiyftMl5ljnwnI3LzN9Z49hovbTEHRFVKfCLzf47nRZMwKcsQGL1zxu7P39TOwhA==, tarball: https://npm.jsr.io/~/11/@jsr/classmodel__class/0.0.3.tgz} + '@kobalte/core@0.13.3': resolution: {integrity: sha512-7ansvAwiIz2EYuifI8jmGj+ZNG/3R4hdkXZkCEFVKsQzq3vZpuSiAXgrdZeQOR4Zby7gxLOzpakjfv7D/3PPGw==} peerDependencies: @@ -1227,6 +1239,11 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assemblyscript@0.27.30: + resolution: {integrity: sha512-tSlwbLEDM1X+w/6/Y2psc3sEg9/7r+m7xv44G6FI2G/w1MNnnulLxcPo7FN0kVIBoD/oxCiRFGaQAanFY0gPhA==} + engines: {node: '>=16', npm: '>=7'} + hasBin: true + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -1278,6 +1295,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binaryen@116.0.0-nightly.20240114: + resolution: {integrity: sha512-0GZrojJnuhoe+hiwji7QFaL3tBlJoA+KFUN7ouYSDGZLSo9CKM8swQX8n/UcbR0d1VuZKU+nhogNzv423JEu5A==} + hasBin: true + bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -2102,6 +2123,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2692,9 +2716,11 @@ packages: shikiji-core@0.9.19: resolution: {integrity: sha512-AFJu/vcNT21t0e6YrfadZ+9q86gvPum6iywRyt1OtIPjPFe25RQnYJyxHQPMLKCCWA992TPxmEmbNcOZCAJclw==} + deprecated: Shikiji is merged back to Shiki v1.0, please migrate over to get the latest updates shikiji@0.9.19: resolution: {integrity: sha512-Kw2NHWktdcdypCj1GkKpXH4o6Vxz8B8TykPlPuLHOGSV8VkhoCLcFOH4k19K4LXAQYRQmxg+0X/eM+m2sLhAkg==} + deprecated: Shikiji is merged back to Shiki v1.0, please migrate over to get the latest updates signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3638,6 +3664,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsr/classmodel__class@0.0.3': + dependencies: + zod: 3.23.8 + zod-to-json-schema: 3.23.1(zod@3.23.8) + '@kobalte/core@0.13.3(solid-js@1.8.18)': dependencies: '@floating-ui/dom': 1.6.7 @@ -4254,6 +4285,11 @@ snapshots: argparse@2.0.1: {} + assemblyscript@0.27.30: + dependencies: + binaryen: 116.0.0-nightly.20240114 + long: 5.2.3 + ast-types@0.16.1: dependencies: tslib: 2.6.3 @@ -4301,6 +4337,8 @@ snapshots: binary-extensions@2.3.0: {} + binaryen@116.0.0-nightly.20240114: {} + bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 @@ -5136,6 +5174,8 @@ snapshots: lodash@4.17.21: {} + long@5.2.3: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: