├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package.json ├── packages ├── epanet-engine │ ├── Dockerfile │ ├── README.md │ ├── build.sh │ ├── generate_exports.sh │ ├── package.json │ ├── tests │ │ ├── benchmarks │ │ │ ├── calls-per-second.js │ │ │ ├── open-large-network.js │ │ │ └── run-long-sim.js │ │ ├── browser │ │ │ ├── benchmark.html │ │ │ └── epanet-worker.js │ │ ├── index.js │ │ ├── my-network.inp │ │ ├── old │ │ │ ├── benchmark.js │ │ │ ├── index.js │ │ │ └── runProject.js │ │ └── tests.md │ └── type-gen │ │ ├── create-enums.js │ │ └── create-types.js └── epanet-js │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── OutputReader │ │ └── index.ts │ ├── Project │ │ └── Project.ts │ ├── Workspace │ │ └── Workspace.ts │ ├── apiDefinitions.ts │ ├── enum │ │ ├── ActionCodeType │ │ │ ├── ActionCodeType.ts │ │ │ └── index.ts │ │ ├── AnalysisStatistic │ │ │ ├── AnalysisStatistic.ts │ │ │ └── index.ts │ │ ├── ControlType │ │ │ ├── ControlType.ts │ │ │ └── index.ts │ │ ├── CountType │ │ │ ├── CountType.ts │ │ │ └── index.ts │ │ ├── CurveType │ │ │ ├── CurveType.ts │ │ │ └── index.ts │ │ ├── DemandModel │ │ │ ├── DemandModel.ts │ │ │ └── index.ts │ │ ├── FlowUnits │ │ │ ├── FlowUnits.ts │ │ │ └── index.ts │ │ ├── HeadLossType │ │ │ ├── HeadLossType.ts │ │ │ └── index.ts │ │ ├── InitHydOption │ │ │ ├── InitHydOption.ts │ │ │ └── index.ts │ │ ├── LinkProperty │ │ │ ├── LinkProperty.ts │ │ │ └── index.ts │ │ ├── LinkStatusType │ │ │ ├── LinkStatusType.ts │ │ │ └── index.ts │ │ ├── LinkType │ │ │ ├── LinkType.ts │ │ │ └── index.ts │ │ ├── MixingModel │ │ │ ├── MixingModel.ts │ │ │ └── index.ts │ │ ├── NodeProperty │ │ │ ├── NodeProperty.ts │ │ │ └── index.ts │ │ ├── NodeType │ │ │ ├── NodeType.ts │ │ │ └── index.ts │ │ ├── ObjectType │ │ │ ├── ObjectType.ts │ │ │ └── index.ts │ │ ├── Option │ │ │ ├── Option.ts │ │ │ └── index.ts │ │ ├── PumpStateType │ │ │ ├── PumpStateType.ts │ │ │ └── index.ts │ │ ├── PumpType │ │ │ ├── PumpType.ts │ │ │ └── index.ts │ │ ├── QualityType │ │ │ ├── QualityType.ts │ │ │ └── index.ts │ │ ├── RuleObject │ │ │ ├── RuleObject.ts │ │ │ └── index.ts │ │ ├── RuleOperator │ │ │ ├── RuleOperator.ts │ │ │ └── index.ts │ │ ├── RuleStatus │ │ │ ├── RuleStatus.ts │ │ │ └── index.ts │ │ ├── RuleVariable │ │ │ ├── RuleVariable.ts │ │ │ └── index.ts │ │ ├── SizeLimits │ │ │ ├── SizeLimits.ts │ │ │ └── index.ts │ │ ├── SourceType │ │ │ ├── SourceType.ts │ │ │ └── index.ts │ │ ├── StatisticType │ │ │ ├── StatisticType.ts │ │ │ └── index.ts │ │ ├── StatusReport │ │ │ ├── StatusReport.ts │ │ │ └── index.ts │ │ ├── TimeParameter │ │ │ ├── TimeParameter.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── index.ts │ └── types.ts │ ├── test │ ├── Project.test.ts │ ├── Project │ │ ├── AnalysisOptionsFunctions.test.ts │ │ ├── DataCurveFunctions.test.ts │ │ ├── HydraulicAnalysisFunctions.test.ts │ │ ├── NetworkLinkFunctions.test.ts │ │ ├── NetworkNodeFunctions.test.ts │ │ ├── NodalDemandFunctions.test.ts │ │ ├── ProjectFunctions.test.ts │ │ ├── ReportingFunctions.test.ts │ │ ├── RuleBasedControlFunctions.test.ts │ │ ├── SimpleControlFunctons.test.ts │ │ ├── TimePatternFunctions.test.ts │ │ └── WaterQualityAnalysisFunctions.test.ts │ ├── Workspace.test.ts │ ├── api-coverage.test.ts │ ├── benchmark │ │ ├── index.js │ │ └── runSim.js │ ├── data │ │ ├── AllElementsSmallNetwork.inp │ │ ├── net1.inp │ │ ├── net1_backup.inp │ │ └── tankTest.inp │ └── version-guard.test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test-lint-build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v1 11 | 12 | - name: Install pnpm 13 | run: npm install -g pnpm 14 | 15 | - name: Cache emscripten build 16 | id: cache-emscripten-build 17 | uses: actions/cache@v4 18 | with: 19 | path: "packages/epanet-engine/dist" 20 | key: emscrip-${{ hashFiles('packages/epanet-engine/Dockerfile')}}-${{ hashFiles('packages/epanet-engine/build.sh')}} 21 | 22 | - name: list files 23 | run: ls -LR packages/epanet-engine 24 | - name: Build emscripten docker container 25 | if: steps.cache-emscripten-build.outputs.cache-hit != 'true' 26 | run: pnpm build:engine 27 | 28 | - name: Get pnpm cache 29 | id: pnpm-cache 30 | run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT 31 | 32 | - uses: actions/cache@v4 33 | with: 34 | path: ${{ steps.pnpm-cache.outputs.dir }} 35 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 36 | restore-keys: | 37 | ${{ runner.os }}-pnpm- 38 | 39 | - name: Install Dependencies 40 | run: pnpm install 41 | - name: Running tests 42 | run: pnpm test 43 | 44 | - name: Running test coverage 45 | run: pnpm test:coverage-ci 46 | - uses: codecov/codecov-action@v5 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | file: "packages/epanet-js/test-report.junit.xml" 50 | fail_ci_if_error: true 51 | 52 | - name: Running build 53 | run: pnpm build 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # Coverage 22 | /packages/epanet-js/coverage 23 | packages/epanet-js/test-report.junit.xml 24 | 25 | # Networks 26 | /packages/epanet-engine/tests/networks 27 | /packages/epanet-js/test/benchmark/networks 28 | 29 | # Typegen 30 | /packages/epanet-engine/type-gen/epanet2_2.d.ts 31 | /packages/epanet-engine/type-gen/epanet2_2.h 32 | /packages/epanet-engine/type-gen/epanet2_enums.h 33 | 34 | /packages/epanet-engine/dist/* 35 | /packages/epanet-engine/build/* 36 | /packages/epanet-js/dist/* 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "functional": "cpp", 4 | "new": "cpp", 5 | "string": "cpp", 6 | "*.tcc": "cpp", 7 | "cctype": "cpp", 8 | "clocale": "cpp", 9 | "cmath": "cpp", 10 | "complex": "cpp", 11 | "cstdarg": "cpp", 12 | "cstddef": "cpp", 13 | "cstdio": "cpp", 14 | "cstdlib": "cpp", 15 | "cstring": "cpp", 16 | "ctime": "cpp", 17 | "cwchar": "cpp", 18 | "cwctype": "cpp", 19 | "deque": "cpp", 20 | "vector": "cpp", 21 | "exception": "cpp", 22 | "fstream": "cpp", 23 | "iosfwd": "cpp", 24 | "iostream": "cpp", 25 | "istream": "cpp", 26 | "limits": "cpp", 27 | "memory": "cpp", 28 | "ostream": "cpp", 29 | "sstream": "cpp", 30 | "stdexcept": "cpp", 31 | "streambuf": "cpp", 32 | "array": "cpp", 33 | "cinttypes": "cpp", 34 | "cstdint": "cpp", 35 | "hashtable": "cpp", 36 | "random": "cpp", 37 | "tuple": "cpp", 38 | "type_traits": "cpp", 39 | "unordered_map": "cpp", 40 | "utility": "cpp", 41 | "typeinfo": "cpp", 42 | "atomic": "cpp", 43 | "algorithm": "cpp", 44 | "iterator": "cpp", 45 | "memory_resource": "cpp", 46 | "numeric": "cpp", 47 | "optional": "cpp", 48 | "string_view": "cpp", 49 | "system_error": "cpp", 50 | "initializer_list": "cpp" 51 | } 52 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luke Butler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💧EPANET-JS 2 | 3 | placeholder 4 | 5 | Water distribution network modelling, either in the browser or with Node. Uses OWA-EPANET v2.2 toolkit compiled to Javascript. 6 | 7 | > **Note**: All version before 1.0.0 should be considered beta with potential breaking changes between releases, use in production with caution. 8 | 9 | [![CI](https://github.com/modelcreate/epanet-js/workflows/CI/badge.svg)](https://github.com/modelcreate/epanet-js/actions?query=workflow%3ACI) [![codecov](https://codecov.io/gh/modelcreate/epanet-js/branch/master/graph/badge.svg)](https://codecov.io/gh/modelcreate/epanet-js) ![npm](https://img.shields.io/npm/v/epanet-js) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 10 | 11 |

12 | Install • 13 | Usage • 14 | About • 15 | Examples • 16 | Featured Apps • 17 | Build • 18 | API • 19 | License 20 |

21 | 22 | ## Install 23 | 24 | Use npm to install the latest stable version 25 | 26 | ``` 27 | $ npm install epanet-js 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Load and run an existing inp File 33 | 34 | [Run this example on CodeSandbox](https://codesandbox.io/embed/musing-chandrasekhar-7tp1y?fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.js&theme=dark) 35 | 36 | ```js 37 | import { Project, Workspace } from "epanet-js"; 38 | import fs from "fs"; 39 | 40 | // Read an existing inp file from your local disk 41 | const net1 = fs.readFileSync("net1.inp"); 42 | 43 | // Initialise a new Workspace and Project object 44 | const ws = new Workspace(); 45 | const model = new Project(ws); 46 | 47 | // Write a copy of the inp file to the virtual workspace 48 | ws.writeFile("net1.inp", net1); 49 | 50 | // Runs toolkit methods: EN_open, EN_solveH & EN_close 51 | model.open("net1.inp", "report.rpt", "out.bin"); 52 | model.solveH(); 53 | model.close(); 54 | ``` 55 | 56 | **_More Examples_** 57 | 58 | - [Step through the hydraulic simulation](https://github.com/modelcreate/epanet-js/wiki/Examples#step-through-the-hydraulic-simulation) 59 | - [New model builder API](https://github.com/modelcreate/epanet-js/wiki/Examples#new-model-builder-api) 60 | - [Fire Flow Analysis using React](https://github.com/modelcreate/epanet-js/wiki/Examples#fire-flow-analysis---react-example) 61 | - [Float valves using React Code (WIP)](https://github.com/modelcreate/epanet-js-float-valve-example) - [Demo](https://modelcreate.github.io/epanet-js-float-valve-example/) 62 | 63 | ## About 64 | 65 | Engineers use hydraulic modelling software to simulate water networks. A model will represent a network consisting of pipes, pumps, valves and storage tanks. The modelling software tracks the flow of water in each pipe, the pressure at each node, the height of water in each tank throughout the network during a multi-period simulation. 66 | 67 | EPANET is an industry-standard program, initially developed by the USEPA, to simulate water distribution networks, its source code was released in the public domain. An open-source fork by the Open Water Analytics (OWA) community maintains and extends its original capabilities. Read more about [EPANET on Wikipedia](https://en.wikipedia.org/wiki/EPANET) and the [OWA community on their website](http://wateranalytics.org/). 68 | 69 | The EPANET Toolkit is an API written in C that allows developers to embed the EPANET's engine in their own applications. 70 | 71 | Epanet-js is a full port of version 2.2 OWA-EPANET Toolkit in Typescript, providing access to all 122 functions within the toolkit. 72 | 73 | The JavaScript library is for engineers, developers and academics to run and share hydraulic analyses or create custom front end or server-side applications. 74 | 75 | ### Roadmap 76 | 77 | Reaching version 1.0.0 is the current focus, the first non-beta version will have API stability, full test coverage and have mirrored functions of each method in the EPANET Toolkit. 78 | 79 | See the remaining task on the [Version 1.0.0 Project](https://github.com/modelcreate/epanet-js/projects/1). 80 | 81 | ### Using EPANET 2.3 with epanet-js 82 | 83 | EPANET 2.3 is currently under development, you can access the latest version of the toolkit within epanet-js by [setting an override](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides) in your package.json. 84 | 85 | Versions of the `epanet-engine` compile against the dev branch of owa-epanet are tagged as next, find the [latest version in npm](https://www.npmjs.com/package/@model-create/epanet-engine). 86 | 87 | ``` 88 | "overrides": { 89 | "epanet-js": { 90 | "@model-create/epanet-engine": "0.6.4-beta.0" 91 | } 92 | } 93 | ``` 94 | 95 | ## Featured Apps 96 | 97 | placeholder 98 | 99 | ### Qatium 100 | 101 | Qatium is an open and collaborative water management platform, allowing users to run operational scenarios and near-real time simulations using their hydraulic models in the browser. 102 | 103 | With an intuitive interface, Qatium provides access to operational hydraulic modelling to those focused on running a water distribution network. 104 | 105 | **Website**: [Qatium](https://qatium.com/) 106 | 107 |
108 | 109 | ## 110 | 111 | placeholder 112 | 113 | ### Watermain Shutdown 114 | 115 | Investigate the impact of shutdowns within a water network. Select a pipe, find the isolation valves, the customers impacted, and any alternative supplies, all with one click. 116 | 117 | Epanet-js is used to confirm the impact on the network and ensuring alternative supplies are adequate. 118 | 119 | Only key information is displayed. Is there low or high pressure, and are there water quality issues to be aware of, such as velocity increases or flow reversals. 120 | 121 | **Website**: [Watermain Shutdown](https://shutdown.modelcreate.com/) 122 | 123 | **Source Code**: [GitHub](https://github.com/modelcreate/watermain-shutdown) 124 | 125 |
126 | 127 | ## 128 | 129 | Model View 130 | 131 | ### Model Calibrate 132 | 133 | Extract subsections of your InfoWorks WS Pro models and run them in your browser. As you make calibration changes such as modifying roughness or restriction valves the application runs an epanet model and compares the simulated results to those observered in the field. 134 | 135 | **Website**: [Model Calibrate](https://calibrate.modelcreate.com/) 136 | 137 | **Source Code**: [GitHub](https://github.com/modelcreate/model-calibrate) 138 | 139 |
140 | 141 | ## 142 | 143 | Model View 144 | 145 | ### Model View 146 | 147 | Display models created in EPANET directly in the browser. No data leaves your computer; all data rendered and processed locally using the epanet-js library. 148 | 149 | **Website**: [Model View](https://view.modelcreate.com/) 150 | 151 | **Source Code**: [GitHub](https://github.com/modelcreate/model-view) 152 | 153 |
154 | 155 | ## Build 156 | 157 | epanet-js is split into two packages, the epanet-engine package which compiles the original C code into WASM using Emscripten. And epanet-js is a TypeScript library which wraps over the generated module from Emscripten and manages memory allocation, error handling and returning of varaible. 158 | 159 | **Building epanet-engine** 160 | 161 | Run the command `pnpm run build` to creates a docker container of Emscripten and the compiled OWA-EPANET source code and generate types. 162 | 163 | ```sh 164 | cd packages/epanet-engine 165 | pnpm run build 166 | ``` 167 | 168 | **Building epanet-js** 169 | 170 | You must first build epanet-engine before you can test or build epanet-js. 171 | 172 | ```sh 173 | cd packages/epanet-js 174 | pnpm run test 175 | pnpm run build 176 | ``` 177 | 178 | **Publishing epanet-js** 179 | 180 | `sh 181 | pnpm publish --recursive 182 | ` 183 | 184 | ## API 185 | 186 | > [Find the full API on the epanet-js website](https://epanetjs.com/api/) 187 | 188 | epanet-js contains two classes, Workspace & Project. A Workspace represents a virtual file system where you can store and read files that are consumed by the tool kit, such as [INP Files](http://wateranalytics.org/EPANET/_inp_file.html) or generated by it, such as [RPT files](http://wateranalytics.org/EPANET/_rpt_file.html) or [OUT files](http://wateranalytics.org/EPANET/_out_file.html). 189 | 190 | A Project is a single instance of the EN_Project wrapper object and a singleton with all 122 toolkit methods attached. A [full list of all methods](https://epanetjs.com/api/project/) can be found on the epanet-js website. All method names have been converted to camelCase to keep with javascript convention. 191 | 192 | Create a `Project` object by instancing the Project class with a Workspace object. 193 | 194 | ```javascript 195 | import { Project, Workspace } from `epanet-js` 196 | 197 | const ws = new Workspace() 198 | const model = new Project(ws) 199 | ``` 200 | 201 | ## License 202 | 203 | Both epanet-js and @model-create/epanet-engine are [MIT licenced](https://github.com/modelcreate/epanet-js/blob/master/LICENSE). 204 | 205 | The hydraulic engine used within the epanet-js library is [OWA-EPANET 2.2](https://github.com/OpenWaterAnalytics/EPANET), which is [MIT licenced](https://github.com/OpenWaterAnalytics/EPANET/blob/dev/LICENSE), with contributions by the following [authors](https://github.com/OpenWaterAnalytics/EPANET/blob/dev/AUTHORS). 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "epanet-js-monorepo", 4 | "scripts": { 5 | "build": "pnpm -r build", 6 | "build:engine": "pnpm --filter @model-create/epanet-engine build", 7 | "build:js": "pnpm --filter epanet-js build", 8 | "test": "pnpm --filter epanet-js test", 9 | "test:coverage-ci": "pnpm --filter epanet-js test:coverage-ci" 10 | }, 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /packages/epanet-engine/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG EMSDK_TAG_SUFFIX="" 2 | 3 | FROM emscripten/emsdk:4.0.6${EMSDK_TAG_SUFFIX} 4 | 5 | RUN apt-get update && \ 6 | apt-get install -qqy git && \ 7 | mkdir -p /opt/epanet/build && \ 8 | git clone --depth 1 https://github.com/ModelCreate/EPANET /opt/epanet/src 9 | RUN cd /opt/epanet/build && \ 10 | emcmake cmake ../src && \ 11 | emmake cmake --build . --config Release 12 | -------------------------------------------------------------------------------- /packages/epanet-engine/README.md: -------------------------------------------------------------------------------- 1 | # 💧@model-create/EPANET-engine 2 | 3 | Internal engine for [epanet-js](https://github.com/modelcreate/epanet-js), C source code for Open Water Analytics EPANET v2.2 toolkit compiled to Javascript. 4 | 5 | > **Note**: All version before 1.0.0 should be considered beta with potential breaking changes between releases, use in production with caution. 6 | 7 | ## Build 8 | 9 | epanet-js is split into two packages, the epanet-engine package which compiles the original C code into WASM using Emscripten. And epanet-js is a TypeScript library which wraps over the generated module from Emscripten and manages memory allocation, error handling and returning of varaible. 10 | 11 | **Building epanet-engine** 12 | 13 | Run the command `pnpm run build` to creates a docker container of Emscripten and the compiled OWA-EPANET source code and generate types. 14 | 15 | ```sh 16 | cd packages/epanet-engine 17 | pnpm run build 18 | ``` 19 | 20 | **Building epanet-js** 21 | 22 | You must first build epanet-engine before you can test or build epanet-js. 23 | 24 | ```sh 25 | cd packages/epanet-js 26 | pnpm run test 27 | pnpm run build 28 | ``` 29 | 30 | ## License 31 | 32 | The epanet-js and @model-create/epanet-engine are [MIT licenced](https://github.com/modelcreate/epanet-js/blob/master/LICENSE). 33 | 34 | The hydraulic engine used within the epanet-js library is [OWA-EPANET](https://github.com/OpenWaterAnalytics/EPANET), which is [MIT licenced](https://github.com/OpenWaterAnalytics/EPANET/blob/dev/LICENSE), with contributed by the following [authors](https://github.com/OpenWaterAnalytics/EPANET/blob/dev/AUTHORS). 35 | -------------------------------------------------------------------------------- /packages/epanet-engine/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "=============================================" 6 | echo "Compiling EPANET to WASM" 7 | echo "=============================================" 8 | ( 9 | mkdir -p dist 10 | mkdir -p type-gen 11 | 12 | # Extract epanet2_2.h from the EPANET repository 13 | echo "Extracting epanet2_2.h..." 14 | cp /opt/epanet/src/include/epanet2_2.h type-gen/ 15 | 16 | echo "Extracting epanet2_enums.h..." 17 | cp /opt/epanet/src/include/epanet2_enums.h type-gen/ 18 | 19 | # Generate exports list 20 | echo "Generating exports list..." 21 | ./generate_exports.sh 22 | 23 | # Read the EPANET functions from the JSON file and add memory management functions 24 | EXPORTED_FUNCTIONS=$(cat build/epanet_exports.json ) 25 | 26 | emcc -O3 /opt/epanet/build/lib/libepanet2.a \ 27 | -o dist/index.js \ 28 | -s WASM=1 \ 29 | -s "EXPORTED_FUNCTIONS=${EXPORTED_FUNCTIONS}" \ 30 | -s MODULARIZE=1 \ 31 | -s EXPORT_ES6=1 \ 32 | -s FORCE_FILESYSTEM=1 \ 33 | -s EXPORTED_RUNTIME_METHODS=['FS','getValue','lengthBytesUTF8','stringToUTF8','stringToNewUTF8','UTF8ToString','stackSave','cwrap','stackRestore','stackAlloc'] \ 34 | -s ASSERTIONS=0 \ 35 | -s ALLOW_MEMORY_GROWTH=1 \ 36 | -s SINGLE_FILE=1 \ 37 | -s ENVIRONMENT=web \ 38 | -msimd128 \ 39 | --closure 0 \ 40 | # -s SAFE_HEAP=0 \ 41 | # -s INITIAL_MEMORY=1024MB \ 42 | 43 | 44 | #-s EXPORT_ALL=1 \ 45 | #-s SINGLE_FILE=1 \ 46 | #-s "EXPORTED_FUNCTIONS=['_getversion', '_open_epanet', '_EN_close']" \ 47 | 48 | 49 | 50 | # We will use this in a switch to allow the slim loader version 51 | # -s SINGLE_FILE=1 embeds the wasm file in the js file 52 | 53 | # Export to ES6 module, you also need MODULARIZE for this to work 54 | # By default these are not enabled 55 | # -s EXPORT_ES6=1 \ 56 | # -s MODULARIZE=1 \ 57 | 58 | # Compile to a wasm file (though this is set by default) 59 | # -s WASM=1 \ 60 | 61 | # FORCE_FILESYSTEM 62 | # Makes full filesystem support be included, even if statically it looks like it is not used. 63 | # For example, if your C code uses no files, but you include some JS that does, you might need this. 64 | 65 | 66 | #EXPORTED_RUNTIME_METHODS 67 | # Blank for now but previously I used 68 | # EXPORTED_RUNTIME_METHODS='["ccall", "getValue", "UTF8ToString", "stringToUTF8", "_free", "intArrayToString","FS"]' 69 | 70 | # ALLOW_MEMORY_GROWTH 71 | # Allow the memory to grow as needed 72 | 73 | 74 | 75 | ## Things to look at later 76 | # WASMFS 77 | # https://emscripten.org/docs/tools_reference/settings_reference.html#wasmfs 78 | 79 | 80 | 81 | #mkdir -p dist 82 | #mv index.js dist 83 | #mv epanet_version.wasm dist 84 | 85 | ) 86 | echo "=============================================" 87 | echo "Compiling wasm bindings done" 88 | echo "=============================================" -------------------------------------------------------------------------------- /packages/epanet-engine/generate_exports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # --- Configuration --- 4 | HEADER_FILE="/opt/epanet/src/include/epanet2_2.h" 5 | OUTPUT_JSON="build/epanet_exports.json" 6 | 7 | mkdir -p build 8 | 9 | # --- Script Logic --- 10 | echo "Scanning '$HEADER_FILE' for DLLEXPORT functions..." 11 | 12 | # Check if header file exists 13 | if [ ! -f "$HEADER_FILE" ]; then 14 | echo "Error: Header file not found at '$HEADER_FILE'" 15 | echo "Please update the HEADER_FILE variable in this script." 16 | exit 1 17 | fi 18 | 19 | # 1. Run the confirmed working pipeline and store the result in a variable 20 | # Using the exact grep pattern you confirmed works. 21 | function_list=$(grep 'DLLEXPORT EN' "$HEADER_FILE" | \ 22 | sed -e 's/.*DLLEXPORT //; s/(.*//; s/^/_/' | \ 23 | sort | \ 24 | uniq) 25 | 26 | # 2. Check if the function list variable is empty 27 | if [ -z "$function_list" ]; then 28 | echo "Warning: No functions found matching the pattern 'int DLLEXPORT ' in '$HEADER_FILE'." 29 | echo "Creating an empty JSON array: [] in $OUTPUT_JSON" 30 | echo "[]" > "$OUTPUT_JSON" 31 | exit 0 32 | fi 33 | 34 | # 3. Use awk to format the captured list (passed via echo) as a JSON array 35 | echo "Formatting function list into JSON..." 36 | echo "$function_list" | awk ' 37 | BEGIN { printf "[\"%s\",\"%s\",", "_malloc", "_free" } # Start with the opening bracket 38 | NR > 1 { printf "," } # Add a comma before every line except the first 39 | { printf "\"%s\"", $0 } # Print the current line (function name) enclosed in quotes 40 | END { print "]" } # End with the closing bracket and a newline 41 | ' > "$OUTPUT_JSON" 42 | 43 | 44 | # --- Completion Message --- 45 | # Verify the output file was created and isn't just "[]" 46 | if [ -s "$OUTPUT_JSON" ] && [ "$(cat "$OUTPUT_JSON")" != "[]" ]; then 47 | # Count lines in the original list variable for accuracy 48 | func_count=$(echo "$function_list" | wc -l | awk '{print $1}') # Get line count 49 | echo "Successfully generated export list in $OUTPUT_JSON" 50 | echo "Found $func_count functions." 51 | else 52 | echo "Error: Failed to generate $OUTPUT_JSON correctly." 53 | echo "The script found functions initially, but the final JSON formatting failed." 54 | echo "Intermediate function list that was processed by awk:" 55 | echo "---" 56 | echo "$function_list" # Print the list that awk should have processed 57 | echo "---" 58 | exit 1 59 | fi 60 | 61 | exit 0 62 | -------------------------------------------------------------------------------- /packages/epanet-engine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@model-create/epanet-engine", 3 | "version": "0.8.0-alpha.5", 4 | "type": "module", 5 | "description": "EPANET WASM engine", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "scripts": { 9 | "build:dockerimage": "docker build -t epanet-js-engine .", 10 | "build:dockerimage-arm64": "docker build --build-arg EMSDK_TAG_SUFFIX='-arm64' -t epanet-js-engine .", 11 | "build:dockerimage-noCache": "docker build --no-cache --build-arg EMSDK_TAG_SUFFIX='-arm64' -t epanet-js-engine .", 12 | "build:emscripten": "docker run --rm -v \"$(pwd):/src\" epanet-js-engine ./build.sh", 13 | "build:types": "node type-gen/create-types.js", 14 | "build": "npm run build:dockerimage && npm run build:emscripten && npm run build:types", 15 | "build:arm64": "npm run build:dockerimage-arm64 && npm run build:emscripten && npm run build:types", 16 | "prepublishOnly": "npm run build:dockerimage-noCache && npm run build:emscripten && npm run build:types" 17 | }, 18 | "keywords": [], 19 | "files": [ 20 | "dist" 21 | ], 22 | "author": "", 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /packages/epanet-engine/tests/benchmarks/calls-per-second.js: -------------------------------------------------------------------------------- 1 | //const epanetEngine = require("../dist/epanet_version.js"); 2 | import epanetEngine from "../../dist/epanet_version.js"; 3 | 4 | import { Project, Workspace, NodeType, FlowUnits, HeadLossType } from "epanet-js"; 5 | 6 | 7 | // Helper function to get node index with pre-allocated memory 8 | function getNodeIndexFast(engine, projectHandle, nodeId, ptrToIndexHandlePtr) { 9 | const ptrNodeId = engine.stringToNewUTF8(nodeId) 10 | const errorCode = engine._EN_getnodeindex(projectHandle, ptrNodeId, ptrToIndexHandlePtr); 11 | const value = engine.getValue(ptrToIndexHandlePtr, 'i32'); 12 | engine._free(ptrNodeId); 13 | return value; 14 | } 15 | 16 | 17 | function getNodeIndexFastStack(engine, projectHandle, nodeId, ptrToIndexHandlePtr) { 18 | const stack = engine.stackSave(); // 1. Save stack pointer 19 | try { 20 | const requiredBytes = engine.lengthBytesUTF8(nodeId) + 1; // 2. Calculate size 21 | const ptrNodeId = engine.stackAlloc(requiredBytes); // 3. Allocate on stack 22 | engine.stringToUTF8(nodeId, ptrNodeId, requiredBytes); // 4. Copy JS string to stack memory 23 | 24 | // 5. Call C function with stack pointer 25 | const errorCode = engine._EN_getnodeindex(projectHandle, ptrNodeId, ptrToIndexHandlePtr); 26 | // Handle errorCode if necessary 27 | 28 | // 6. Result is read from the pre-allocated ptrToIndexHandlePtr 29 | return engine.getValue(ptrToIndexHandlePtr, 'i32'); 30 | 31 | } finally { 32 | engine.stackRestore(stack); // 7. Restore stack pointer (frees stack memory) 33 | } 34 | } 35 | 36 | function getNodeIndexCwarp(engine, fn, projectHandle, nodeId, ptrToIndexHandlePtr) { 37 | const errorCode = fn(projectHandle, nodeId, ptrToIndexHandlePtr); 38 | return engine.getValue(ptrToIndexHandlePtr, 'i32'); 39 | } 40 | 41 | // Benchmark function that returns performance metrics 42 | async function benchmarkNodeIndexCalls(iterations = 1000000) { 43 | const engine = await epanetEngine(); 44 | 45 | //const getNodeIndex = engine.cwrap('EN_getnodeindex', 'number', ['number','string','number']) 46 | 47 | let errorCode; 48 | let projectHandle; 49 | let ptrToProjectHandlePtr; 50 | let ptrRptFile; 51 | let ptrBinFile; 52 | let ptrNodeId; 53 | let ptrToIndexHandlePtr; 54 | let indexOfNode; 55 | 56 | // Create Project 57 | ptrToProjectHandlePtr = engine._malloc(4); 58 | errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 59 | if (errorCode !== 0) throw new Error(`Failed to create project: ${errorCode}`); 60 | projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 61 | engine._free(ptrToProjectHandlePtr); 62 | 63 | // Initialize Project 64 | ptrRptFile = engine.stringToNewUTF8("report.rpt") 65 | ptrBinFile = engine.stringToNewUTF8("out.bin") 66 | 67 | 68 | 69 | errorCode = engine._EN_init(projectHandle, ptrRptFile, ptrBinFile, 1, 1); 70 | if (errorCode !== 0) throw new Error(`Failed to initialize project: ${errorCode}`); 71 | engine._free(ptrRptFile); 72 | engine._free(ptrBinFile); 73 | 74 | // Add Node 75 | ptrNodeId = engine.stringToNewUTF8("J1") 76 | 77 | 78 | ptrToIndexHandlePtr = engine._malloc(4); 79 | errorCode = engine._EN_addnode(projectHandle, ptrNodeId, 0, ptrToIndexHandlePtr); 80 | if (errorCode !== 0) throw new Error(`Failed to add node: ${errorCode}`); 81 | indexOfNode = engine.getValue(ptrToIndexHandlePtr, 'i32'); 82 | engine._free(ptrNodeId); 83 | engine._free(ptrToIndexHandlePtr); 84 | 85 | // Pre-allocate memory buffers for benchmarking 86 | const benchmarkPtrNodeId = engine._malloc(4); 87 | const benchmarkPtrToIndexHandlePtr = engine._malloc(4); 88 | 89 | 90 | 91 | const startTime = performance.now(); 92 | for (let i = 0; i < iterations; i++) { 93 | getNodeIndexFast(engine, projectHandle, "J1", benchmarkPtrToIndexHandlePtr); 94 | //getNodeIndexCwarp(engine, getNodeIndex, projectHandle, "J1", benchmarkPtrToIndexHandlePtr); 95 | //getNodeIndexFastStack(engine, projectHandle, "J1", ptrToIndexHandlePtr) 96 | } 97 | 98 | 99 | // CWRAP TEST 100 | 101 | //const valuePtr = engine._malloc(4); 102 | //const test = getNodeIndex(projectHandle, "J1", valuePtr); 103 | //const value = engine.getValue(valuePtr, 'i32'); 104 | //console.log(`return: ${test} test value: ${value}`); 105 | //engine._free(valuePtr); 106 | //const test2 = getNodeIndexCwarp(engine, getNodeIndex, projectHandle, "J1", valuePtr); 107 | //console.log(`test2: ${test2}`); 108 | 109 | 110 | const endTime = performance.now(); 111 | const durationSeconds = (endTime - startTime) / 1000; 112 | const runsPerSecond = iterations / durationSeconds; 113 | const millionRunsPerSecond = runsPerSecond / 1000000; 114 | 115 | // Clean up pre-allocated memory 116 | engine._free(benchmarkPtrNodeId); 117 | engine._free(benchmarkPtrToIndexHandlePtr); 118 | 119 | // Delete Project 120 | errorCode = engine._EN_deleteproject(projectHandle); 121 | if (errorCode !== 0) throw new Error(`Failed to delete project: ${errorCode}`); 122 | 123 | return { 124 | durationSeconds, 125 | runsPerSecond, 126 | millionRunsPerSecond, 127 | iterations 128 | }; 129 | } 130 | 131 | 132 | function benchmarkNodeIndexCallsEpanetJs(iterations = 1000000) { 133 | 134 | const ws = new Workspace(); 135 | const model = new Project(ws); 136 | 137 | model.init("report.rpt", "out.bin", FlowUnits.CFS, HeadLossType.HW); 138 | model.addNode("J1", NodeType.Junction); 139 | model.getNodeIndex("J1"); 140 | 141 | const startTime = performance.now(); 142 | 143 | for (let i = 0; i < iterations; i++) { 144 | model.getNodeIndex("J1"); 145 | } 146 | 147 | const endTime = performance.now(); 148 | const durationSeconds = (endTime - startTime) / 1000; 149 | const runsPerSecond = iterations / durationSeconds; 150 | const millionRunsPerSecond = runsPerSecond / 1000000; 151 | 152 | return { 153 | durationSeconds, 154 | runsPerSecond, 155 | millionRunsPerSecond, 156 | iterations 157 | }; 158 | } 159 | 160 | 161 | 162 | export { benchmarkNodeIndexCalls, benchmarkNodeIndexCallsEpanetJs }; 163 | 164 | 165 | -------------------------------------------------------------------------------- /packages/epanet-engine/tests/benchmarks/open-large-network.js: -------------------------------------------------------------------------------- 1 | import epanetEngine from "../../dist/epanet_version.js"; 2 | import fs from "fs"; 3 | import { Project, Workspace } from "epanet-js"; 4 | 5 | 6 | const inpFileName = "./tests/networks/horrible.inp"; 7 | const inpText = fs.readFileSync(inpFileName); 8 | 9 | 10 | async function benchmarkOpenLargeNetworkWasm(iterations = 3) { 11 | const engine = await epanetEngine(); 12 | const results = []; 13 | 14 | for (let i = 1; i <= iterations; i++) { 15 | const startTime = performance.now(); 16 | 17 | let errorCode; 18 | let projectHandle; 19 | let ptrToProjectHandlePtr; 20 | let ptrInpFile; 21 | let ptrRptFile; 22 | let ptrBinFile; 23 | let indexOfNode; 24 | 25 | 26 | engine.FS.writeFile("net1.inp", inpText); 27 | 28 | // Create Project 29 | ptrToProjectHandlePtr = engine._malloc(4); 30 | errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 31 | if (errorCode !== 0) throw new Error(`Failed to create project: ${errorCode}`); 32 | projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 33 | engine._free(ptrToProjectHandlePtr); 34 | 35 | 36 | const lenInpFile = engine.lengthBytesUTF8("net1.inp") + 1; 37 | ptrInpFile = engine._malloc(lenInpFile); 38 | engine.stringToUTF8("net1.inp", ptrInpFile, lenInpFile); 39 | 40 | const lenRptFile = engine.lengthBytesUTF8("report.rpt") + 1; 41 | ptrRptFile = engine._malloc(lenRptFile); 42 | engine.stringToUTF8("report.rpt", ptrRptFile, lenRptFile); 43 | 44 | const lenBinFile = engine.lengthBytesUTF8("out.bin") + 1; 45 | ptrBinFile = engine._malloc(lenBinFile); 46 | engine.stringToUTF8("out.bin", ptrBinFile, lenBinFile); 47 | 48 | errorCode = engine._EN_open(projectHandle, ptrInpFile, ptrRptFile, ptrBinFile); 49 | if (errorCode !== 0) throw new Error(`Failed to open project: ${errorCode}`); 50 | engine._free(ptrInpFile); 51 | engine._free(ptrRptFile); 52 | engine._free(ptrBinFile); 53 | 54 | 55 | // Delete Project 56 | errorCode = engine._EN_deleteproject(projectHandle); 57 | if (errorCode !== 0) throw new Error(`Failed to delete project: ${errorCode}`); 58 | 59 | const endTime = performance.now(); 60 | const durationSeconds = (endTime - startTime) / 1000; 61 | results.push({ 62 | iteration: i, 63 | durationSeconds, 64 | nodeIndex: indexOfNode 65 | }); 66 | } 67 | 68 | const totalDuration = results.reduce((sum, r) => sum + r.durationSeconds, 0); 69 | const averageDuration = totalDuration / iterations; 70 | 71 | return { 72 | results, 73 | totalDuration, 74 | averageDuration, 75 | iterations 76 | }; 77 | } 78 | 79 | async function benchmarkOpenLargeNetworkEpanetJs(iterations = 3) { 80 | const results = []; 81 | 82 | for (let i = 1; i <= iterations; i++) { 83 | const startTime = performance.now(); 84 | 85 | const ws = new Workspace(); 86 | const model = new Project(ws); 87 | 88 | ws.writeFile("net1.inp", inpText); 89 | 90 | model.open("net1.inp", "report.rpt", "out.bin"); 91 | 92 | const endTime = performance.now(); 93 | const durationSeconds = (endTime - startTime) / 1000; 94 | results.push({ 95 | iteration: i, 96 | durationSeconds 97 | }); 98 | } 99 | 100 | const totalDuration = results.reduce((sum, r) => sum + r.durationSeconds, 0); 101 | const averageDuration = totalDuration / iterations; 102 | 103 | return { 104 | results, 105 | totalDuration, 106 | averageDuration, 107 | iterations 108 | }; 109 | } 110 | 111 | export { benchmarkOpenLargeNetworkWasm, benchmarkOpenLargeNetworkEpanetJs }; -------------------------------------------------------------------------------- /packages/epanet-engine/tests/benchmarks/run-long-sim.js: -------------------------------------------------------------------------------- 1 | import epanetEngine from "../../dist/epanet_version.js"; 2 | import fs from "fs"; 3 | import { Project, Workspace } from "epanet-js"; 4 | 5 | async function benchmarkRunLongSimWasm(iterations = 3) { 6 | const engine = await epanetEngine(); 7 | const results = []; 8 | 9 | // Load the input file once 10 | const inpFileName = "./tests/networks/sw-network1.inp"; 11 | const inpText = fs.readFileSync(inpFileName); 12 | engine.FS.writeFile("net1.inp", inpText); 13 | 14 | for (let i = 1; i <= iterations; i++) { 15 | 16 | 17 | let errorCode; 18 | let projectHandle; 19 | let ptrToProjectHandlePtr; 20 | let ptrInpFile; 21 | let ptrRptFile; 22 | let ptrBinFile; 23 | 24 | // Create Project 25 | ptrToProjectHandlePtr = engine._malloc(4); 26 | errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 27 | if (errorCode > 100) throw new Error(`Failed to create project: ${errorCode}`); 28 | projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 29 | engine._free(ptrToProjectHandlePtr); 30 | 31 | // Run Project 32 | const lenInpFile = engine.lengthBytesUTF8("net1.inp") + 1; 33 | ptrInpFile = engine._malloc(lenInpFile); 34 | engine.stringToUTF8("net1.inp", ptrInpFile, lenInpFile); 35 | 36 | const lenRptFile = engine.lengthBytesUTF8("report.rpt") + 1; 37 | ptrRptFile = engine._malloc(lenRptFile); 38 | engine.stringToUTF8("report.rpt", ptrRptFile, lenRptFile); 39 | 40 | const lenBinFile = engine.lengthBytesUTF8("out.bin") + 1; 41 | ptrBinFile = engine._malloc(lenBinFile); 42 | engine.stringToUTF8("out.bin", ptrBinFile, lenBinFile); 43 | 44 | errorCode = engine._EN_open(projectHandle, ptrInpFile, ptrRptFile, ptrBinFile); 45 | if (errorCode > 100) throw new Error(`Failed to open project: ${errorCode}`); 46 | engine._free(ptrInpFile); 47 | engine._free(ptrRptFile); 48 | engine._free(ptrBinFile); 49 | 50 | const startTime = performance.now(); 51 | 52 | errorCode = engine._EN_solveH(projectHandle); 53 | if (errorCode > 100) throw new Error(`Failed to solve hydraulics: ${errorCode}`); 54 | 55 | const endTime = performance.now(); 56 | const durationSeconds = (endTime - startTime) / 1000; 57 | results.push({ 58 | iteration: i, 59 | durationSeconds 60 | }); 61 | 62 | // Delete Project 63 | errorCode = engine._EN_deleteproject(projectHandle); 64 | if (errorCode > 100) throw new Error(`Failed to delete project: ${errorCode}`); 65 | } 66 | 67 | const totalDuration = results.reduce((sum, r) => sum + r.durationSeconds, 0); 68 | const averageDuration = totalDuration / iterations; 69 | 70 | return { 71 | results, 72 | totalDuration, 73 | averageDuration, 74 | iterations 75 | }; 76 | } 77 | 78 | async function benchmarkRunLongSimEpanetJs(iterations = 3) { 79 | const results = []; 80 | 81 | // Load the input file once 82 | const inpFileName = "./tests/networks/sw-network1.inp"; 83 | const inpText = fs.readFileSync(inpFileName); 84 | 85 | for (let i = 1; i <= iterations; i++) { 86 | 87 | 88 | const ws = new Workspace(); 89 | const model = new Project(ws); 90 | 91 | ws.writeFile("net1.inp", inpText); 92 | 93 | model.open("net1.inp", "report.rpt", "out.bin"); 94 | 95 | const startTime = performance.now(); 96 | model.solveH(); 97 | const endTime = performance.now(); 98 | 99 | const durationSeconds = (endTime - startTime) / 1000; 100 | results.push({ 101 | iteration: i, 102 | durationSeconds 103 | }); 104 | } 105 | 106 | const totalDuration = results.reduce((sum, r) => sum + r.durationSeconds, 0); 107 | const averageDuration = totalDuration / iterations; 108 | 109 | return { 110 | results, 111 | totalDuration, 112 | averageDuration, 113 | iterations 114 | }; 115 | } 116 | 117 | export { benchmarkRunLongSimWasm, benchmarkRunLongSimEpanetJs }; 118 | 119 | -------------------------------------------------------------------------------- /packages/epanet-engine/tests/browser/epanet-worker.js: -------------------------------------------------------------------------------- 1 | // epanet-worker.js 2 | 3 | // Import the wasm module - path is relative to this worker file 4 | import epanetEngine from '../../dist/epanet_version.js'; 5 | 6 | // Define the log function for the worker to send messages back 7 | function workerLog(message) { 8 | self.postMessage({ type: 'log', payload: message }); 9 | } 10 | 11 | // Listen for messages from the main thread 12 | self.onmessage = async (event) => { 13 | if (event.data.type === 'runSimulation') { 14 | const { inpText, fileName } = event.data.payload; 15 | 16 | try { 17 | workerLog("Worker received task. Initializing EPANET engine..."); 18 | const engine = await epanetEngine(); 19 | workerLog("EPANET engine initialized."); 20 | 21 | 22 | 23 | // Write INP file to virtual filesystem 24 | const inputFilePath = "inputfile.inp"; // Use a consistent name 25 | engine.FS.writeFile(inputFilePath, inpText); 26 | workerLog(`Loaded INP file content for: ${fileName}`); 27 | 28 | // Create Project 29 | const ptrToProjectHandlePtr = engine._malloc(4); 30 | let errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 31 | workerLog(`_EN_createproject: ${errorCode}`); 32 | if (errorCode > 100) throw new Error(`Failed to create project (code: ${errorCode})`); 33 | const projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 34 | engine._free(ptrToProjectHandlePtr); 35 | 36 | // Run Project 37 | const ptrInpFile = engine.allocateUTF8(inputFilePath); 38 | const ptrRptFile = engine.allocateUTF8("report.rpt"); 39 | const ptrBinFile = engine.allocateUTF8("out.bin"); 40 | 41 | 42 | 43 | workerLog("Running simulation..."); 44 | //errorCode = engine._EN_init(projectHandle, ptrInpFile); 45 | errorCode = engine._EN_open(projectHandle, ptrInpFile, ptrRptFile, ptrBinFile); 46 | engine._free(ptrInpFile); 47 | 48 | const startTime = performance.now(); 49 | 50 | errorCode = engine._EN_solveH(projectHandle); 51 | workerLog(`_EN_runproject: ${errorCode}`); 52 | 53 | // Note: Keep report/output files if you might want to read them later 54 | // If not, you can free them here or let wasm memory cleanup handle it on project delete 55 | 56 | const endTime = performance.now(); 57 | const durationSeconds = (endTime - startTime) / 1000; 58 | workerLog(`Worker simulation completed in ${durationSeconds.toFixed(3)} seconds.`); 59 | 60 | if (errorCode > 100) throw new Error(`Failed to run project (code: ${errorCode})`); 61 | 62 | 63 | // --- Optional: Read results from report or binary file if needed --- 64 | // Example: Reading the report file 65 | // try { 66 | // const reportContent = engine.FS.readFile(ptrRptFile, { encoding: 'utf8' }); 67 | // workerLog("--- Report File Content ---"); 68 | // workerLog(reportContent.substring(0, 1000) + (reportContent.length > 1000 ? '...' : '')); // Log beginning 69 | // workerLog("--- End Report ---"); 70 | // } catch (readError) { 71 | // workerLog(`Could not read report file: ${readError}`); 72 | // } 73 | engine._free(ptrRptFile); 74 | engine._free(ptrBinFile); 75 | // ------------------------------------------------------------------- 76 | 77 | 78 | // Delete Project 79 | workerLog("Deleting project..."); 80 | errorCode = engine._EN_deleteproject(projectHandle); 81 | workerLog(`_EN_deleteproject: ${errorCode}`); 82 | if (errorCode > 100) workerLog(`Warning: Failed to delete project cleanly (code: ${errorCode})`); 83 | 84 | 85 | 86 | 87 | // Send the final result back to the main thread 88 | self.postMessage({ type: 'result', payload: durationSeconds }); 89 | 90 | } catch (error) { 91 | workerLog(`Worker error: ${error.message}`); 92 | // Send error back to the main thread 93 | self.postMessage({ type: 'error', payload: error.message || 'An unknown error occurred in the worker.' }); 94 | } 95 | } 96 | }; 97 | 98 | // Signal that the worker is ready (optional but good practice) 99 | self.postMessage({ type: 'ready' }); 100 | workerLog("EPANET worker script loaded and ready."); -------------------------------------------------------------------------------- /packages/epanet-engine/tests/index.js: -------------------------------------------------------------------------------- 1 | import { benchmarkNodeIndexCalls, benchmarkNodeIndexCallsEpanetJs } from './benchmarks/calls-per-second.js'; 2 | import { benchmarkOpenLargeNetworkWasm, benchmarkOpenLargeNetworkEpanetJs } from './benchmarks/open-large-network.js'; 3 | import { benchmarkRunLongSimWasm, benchmarkRunLongSimEpanetJs } from './benchmarks/run-long-sim.js'; 4 | 5 | 6 | // Use it in an async function 7 | async function runBenchmark() { 8 | try { 9 | 10 | 11 | console.log("--------------------------------"); 12 | console.log("Run Long Sim"); 13 | 14 | // Run WASM version 15 | const longsimWasmResults = await benchmarkRunLongSimWasm(1); 16 | console.log('WASM Results:', { 17 | averageDuration: longsimWasmResults.averageDuration, 18 | totalDuration: longsimWasmResults.totalDuration 19 | }); 20 | 21 | // Run epanet-js version 22 | const longsimJsResults = await benchmarkRunLongSimEpanetJs(1); 23 | console.log('epanet-js Results:', { 24 | averageDuration: longsimJsResults.averageDuration, 25 | totalDuration: longsimJsResults.totalDuration 26 | }); 27 | 28 | console.log("--------------------------------"); 29 | console.log("Node Index Calls"); 30 | 31 | const results = await benchmarkNodeIndexCalls(60_000_000); 32 | console.log(`Performance: ${results.millionRunsPerSecond.toFixed(4)} million calls per second`); 33 | 34 | const resultsEpanetJs = await benchmarkNodeIndexCallsEpanetJs(10_000_000); 35 | console.log(`Performance: ${resultsEpanetJs.millionRunsPerSecond.toFixed(4)} million calls per second`); 36 | 37 | console.log("--------------------------------"); 38 | console.log("Open Large Network"); 39 | 40 | // Run WASM version 41 | const wasmResults = await benchmarkOpenLargeNetworkWasm(1); 42 | console.log('WASM Results:', { 43 | averageDuration: wasmResults.averageDuration, 44 | totalDuration: wasmResults.totalDuration 45 | }); 46 | 47 | // Run epanet-js version 48 | const jsResults = await benchmarkOpenLargeNetworkEpanetJs(1); 49 | console.log('epanet-js Results:', { 50 | averageDuration: jsResults.averageDuration, 51 | totalDuration: jsResults.totalDuration 52 | }); 53 | 54 | 55 | } catch (error) { 56 | console.error('Benchmark failed:', error.message); 57 | } 58 | } 59 | 60 | 61 | 62 | runBenchmark(); -------------------------------------------------------------------------------- /packages/epanet-engine/tests/my-network.inp: -------------------------------------------------------------------------------- 1 | [JUNCTIONS] 2 | ;Id Elevation 3 | 4tQyVjy4CVL33HXQyTtRg 56.4 4 | H90FOUFsjKy0LrTMBrzt5 68.7 5 | vLfEJv8pqDKcgWS2GkUmI 61.8 6 | 7 | [RESERVOIRS] 8 | ;Id Head Pattern 9 | ITMN7DinWlnXQJC2SX5PU 82.7 10 | 11 | [PIPES] 12 | ;Id Start End Length Diameter Roughness MinorLoss Status 13 | IJGhUPvOjD27Z3cRpkter H90FOUFsjKy0LrTMBrzt5 4tQyVjy4CVL33HXQyTtRg 135.26 300 130 0 Open 14 | iUIhYX2iDiXCP55I9HixM ITMN7DinWlnXQJC2SX5PU vLfEJv8pqDKcgWS2GkUmI 196.73 300 130 0 Open 15 | 16 | [DEMANDS] 17 | ;Id Demand Pattern Category 18 | 4tQyVjy4CVL33HXQyTtRg 0 19 | H90FOUFsjKy0LrTMBrzt5 0 20 | vLfEJv8pqDKcgWS2GkUmI 0 21 | 22 | [TIMES] 23 | Duration 0 24 | 25 | [REPORT] 26 | Status FULL 27 | 28 | [OPTIONS] 29 | Units LPS 30 | Headloss H-W 31 | 32 | [COORDINATES] 33 | ;Node X-coord Y-coord 34 | 4tQyVjy4CVL33HXQyTtRg -4.3861492 55.9151006 35 | H90FOUFsjKy0LrTMBrzt5 -4.3847988 55.9160529 36 | vLfEJv8pqDKcgWS2GkUmI -4.3829174 55.915024 37 | ITMN7DinWlnXQJC2SX5PU -4.3830084 55.9161634 38 | 39 | [VERTICES] 40 | ;link X-coord Y-coord 41 | iUIhYX2iDiXCP55I9HixM -4.3837519 55.9156618 42 | iUIhYX2iDiXCP55I9HixM -4.3825532 55.9153301 43 | 44 | [END] -------------------------------------------------------------------------------- /packages/epanet-engine/tests/old/benchmark.js: -------------------------------------------------------------------------------- 1 | //const epanetEngine = require("../dist/epanet_version.js"); 2 | import epanetEngine from "../../dist/epanet_version.js"; 3 | const engine = await epanetEngine(); 4 | 5 | let errorCode; 6 | let projectHandle; 7 | let ptrToProjectHandlePtr; 8 | let ptrRptFile; 9 | let ptrBinFile; 10 | let ptrNodeId; 11 | let ptrToIndexHandlePtr; 12 | let indexOfNode; 13 | 14 | 15 | 16 | // Create Project 17 | ptrToProjectHandlePtr = engine._malloc(4); 18 | errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 19 | console.log(`_EN_createproject: ${errorCode}`); 20 | projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 21 | engine._free(ptrToProjectHandlePtr); 22 | 23 | // Initialize Project 24 | ptrRptFile = engine.allocateUTF8("report.rpt"); 25 | ptrBinFile = engine.allocateUTF8("out.bin"); 26 | errorCode = engine._EN_init(projectHandle, ptrRptFile, ptrBinFile, 1, 1); // Units=GPM, Headloss=H-W 27 | console.log(`_EN_init: ${errorCode}`); 28 | engine._free(ptrRptFile); 29 | engine._free(ptrBinFile); 30 | 31 | // Add Node 32 | ptrNodeId = engine.allocateUTF8("J1"); 33 | ptrToIndexHandlePtr = engine._malloc(4); 34 | errorCode = engine._EN_addnode(projectHandle, ptrNodeId, 0 /* JUNCTION */, ptrToIndexHandlePtr); 35 | console.log(`_EN_addnode: ${errorCode}`); 36 | indexOfNode = engine.getValue(ptrToIndexHandlePtr, 'i32'); 37 | console.log(`Node index: ${indexOfNode}`); 38 | engine._free(ptrNodeId); 39 | engine._free(ptrToIndexHandlePtr); 40 | 41 | 42 | // Get Node Index 43 | function getNodeIndex(engine, projectHandle, nodeId, verbose = false) { 44 | const ptrNodeId = engine.allocateUTF8(nodeId); 45 | const ptrToIndexHandlePtr = engine._malloc(4); 46 | const errorCode = engine._EN_getnodeindex(projectHandle, ptrNodeId, ptrToIndexHandlePtr); 47 | if (verbose) console.log(`_EN_getnodeindex: ${errorCode}`); 48 | const indexOfNode = engine.getValue(ptrToIndexHandlePtr, 'i32'); 49 | if (verbose) console.log(`Retrieved node index for ${nodeId}: ${indexOfNode}`); 50 | engine._free(ptrNodeId); 51 | engine._free(ptrToIndexHandlePtr); 52 | return indexOfNode; 53 | } 54 | 55 | // Fast version that reuses pre-allocated memory 56 | function getNodeIndexFast(engine, projectHandle, nodeId, ptrNodeId, ptrToIndexHandlePtr) { 57 | engine.stringToUTF8(nodeId, ptrNodeId, 4); 58 | const errorCode = engine._EN_getnodeindex(projectHandle, ptrNodeId, ptrToIndexHandlePtr); 59 | return engine.getValue(ptrToIndexHandlePtr, 'i32'); 60 | } 61 | 62 | // Call the function with verbose output for the single test 63 | indexOfNode = getNodeIndex(engine, projectHandle, "J1", true); 64 | 65 | // Benchmark getNodeIndex 66 | function benchmarkGetNodeIndex(engine, projectHandle, nodeId, iterations = 1000000) { 67 | console.log(`Starting benchmark for ${iterations} iterations...`); 68 | 69 | // Pre-allocate memory buffers 70 | const ptrNodeId = engine._malloc(4); // Buffer for node ID string 71 | const ptrToIndexHandlePtr = engine._malloc(4); // Buffer for index result 72 | 73 | const startTime = performance.now(); 74 | 75 | for (let i = 0; i < iterations; i++) { 76 | getNodeIndexFast(engine, projectHandle, nodeId, ptrNodeId, ptrToIndexHandlePtr); 77 | } 78 | 79 | const endTime = performance.now(); 80 | const durationSeconds = (endTime - startTime) / 1000; 81 | const runsPerSecond = iterations / durationSeconds; 82 | const millionRunsPerSecond = runsPerSecond / 1000000; 83 | 84 | // Clean up pre-allocated memory 85 | engine._free(ptrNodeId); 86 | engine._free(ptrToIndexHandlePtr); 87 | 88 | console.log(`Benchmark Results:`); 89 | console.log(`Total time: ${durationSeconds.toFixed(2)} seconds`); 90 | console.log(`Runs per second: ${runsPerSecond.toFixed(2)}`); 91 | console.log(`Million runs per second: ${millionRunsPerSecond.toFixed(4)}`); 92 | } 93 | 94 | // Run the benchmark 95 | benchmarkGetNodeIndex(engine, projectHandle, "J1", 60_000_000); 96 | 97 | // Delete Project 98 | errorCode = engine._EN_deleteproject(projectHandle); 99 | console.log(`_EN_deleteproject: ${errorCode}`); 100 | 101 | 102 | 103 | 104 | //console.log("epanetEngine", engine._getversion()); 105 | //console.log("epanetEngine", engine._open_epanet()); 106 | 107 | 108 | // Code to replicate: 109 | //for (let i = 1; i <= 3; i++) { 110 | // console.time("runSimulation"); 111 | // const workspace = new Workspace(); 112 | // const model = new Project(workspace); 113 | // workspace.writeFile("net1.inp", horribleInp); 114 | // model.open("net1.inp", "report.rpt", "out.bin"); 115 | // model.close(); 116 | // console.timeEnd("runSimulation"); 117 | // } 118 | 119 | 120 | // const workspace = new Workspace(); 121 | // this._instance = epanetEngine; 122 | // this._FS = this._instance.FS; 123 | // 124 | // workspace.writeFile("net1.inp", horribleInp); 125 | // writeFile(path: string, data: string | ArrayBufferView) { 126 | // this._FS.writeFile(path, data); 127 | // } 128 | // 129 | // const model = new Project(workspace); 130 | // this._ws = ws; 131 | // this._instance = ws._instance; 132 | // this._EN = new this._ws._instance.Epanet(); 133 | // 134 | -------------------------------------------------------------------------------- /packages/epanet-engine/tests/old/index.js: -------------------------------------------------------------------------------- 1 | //const epanetEngine = require("../dist/epanet_version.js"); 2 | import epanetEngine from "../../dist/epanet_version.js"; 3 | import fs from "fs"; 4 | const engine = await epanetEngine(); 5 | 6 | async function runEpanetTest(iteration) { 7 | console.log(`\nStarting iteration ${iteration}`); 8 | const startTime = performance.now(); 9 | 10 | let errorCode; 11 | let projectHandle; 12 | let ptrToProjectHandlePtr; 13 | let ptrInpFile; 14 | let ptrRptFile; 15 | let ptrBinFile; 16 | let ptrNodeId; 17 | let ptrToIndexHandlePtr; 18 | let indexOfNode; 19 | 20 | const inpFileName = "./tests/networks/horrible.inp"; 21 | const inpText = fs.readFileSync(inpFileName); 22 | engine.FS.writeFile("net1.inp", inpText); 23 | 24 | // Create Project 25 | ptrToProjectHandlePtr = engine._malloc(4); 26 | errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 27 | console.log(`_EN_createproject: ${errorCode}`); 28 | projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 29 | engine._free(ptrToProjectHandlePtr); 30 | 31 | ptrInpFile = engine.allocateUTF8("net1.inp"); 32 | ptrRptFile = engine.allocateUTF8("report.rpt"); 33 | ptrBinFile = engine.allocateUTF8("out.bin"); 34 | 35 | errorCode = engine._EN_open(projectHandle, ptrInpFile, ptrRptFile, ptrBinFile); 36 | console.log(`_EN_init: ${errorCode}`); 37 | engine._free(ptrInpFile); 38 | engine._free(ptrRptFile); 39 | engine._free(ptrBinFile); 40 | 41 | // Get Node Index 42 | function getNodeIndex(engine, projectHandle, nodeId) { 43 | const ptrNodeId = engine.allocateUTF8(nodeId); 44 | const ptrToIndexHandlePtr = engine._malloc(4); 45 | const errorCode = engine._EN_getnodeindex(projectHandle, ptrNodeId, ptrToIndexHandlePtr); 46 | console.log(`_EN_getnodeindex: ${errorCode}`); 47 | const indexOfNode = engine.getValue(ptrToIndexHandlePtr, 'i32'); 48 | console.log(`Retrieved node index for ${nodeId}: ${indexOfNode}`); 49 | engine._free(ptrNodeId); 50 | engine._free(ptrToIndexHandlePtr); 51 | return indexOfNode; 52 | } 53 | 54 | // Call the function with verbose output for the single test 55 | indexOfNode = getNodeIndex(engine, projectHandle, "vLfEJv8pqDKcgWS2GkUmI"); 56 | 57 | // Delete Project 58 | errorCode = engine._EN_deleteproject(projectHandle); 59 | console.log(`_EN_deleteproject: ${errorCode}`); 60 | 61 | const endTime = performance.now(); 62 | const durationSeconds = (endTime - startTime) / 1000; 63 | console.log(`Iteration ${iteration} completed in ${durationSeconds} seconds`); 64 | } 65 | 66 | // Run the test multiple times 67 | const numberOfIterations = 10; 68 | for (let i = 1; i <= numberOfIterations; i++) { 69 | await runEpanetTest(i); 70 | } 71 | 72 | //// Initialize Project 73 | //ptrRptFile = engine.allocateUTF8("report.rpt"); 74 | //ptrBinFile = engine.allocateUTF8("out.bin"); 75 | //errorCode = engine._EN_init(projectHandle, ptrRptFile, ptrBinFile, 1, 1); // Units=GPM, Headloss=H-W 76 | //console.log(`_EN_init: ${errorCode}`); 77 | //engine._free(ptrRptFile); 78 | //engine._free(ptrBinFile); 79 | 80 | // Add Node 81 | //ptrNodeId = engine.allocateUTF8("J1"); 82 | //ptrToIndexHandlePtr = engine._malloc(4); 83 | //errorCode = engine._EN_addnode(projectHandle, ptrNodeId, 0 /* JUNCTION */, ptrToIndexHandlePtr); 84 | //console.log(`_EN_addnode: ${errorCode}`); 85 | //indexOfNode = engine.getValue(ptrToIndexHandlePtr, 'i32'); 86 | //console.log(`Node index: ${indexOfNode}`); 87 | //engine._free(ptrNodeId); 88 | //engine._free(ptrToIndexHandlePtr); 89 | 90 | 91 | //// Get Node Index 92 | //function getNodeIndex(engine, projectHandle, nodeId) { 93 | // const ptrNodeId = engine.allocateUTF8(nodeId); 94 | // const ptrToIndexHandlePtr = engine._malloc(4); 95 | // const errorCode = engine._EN_getnodeindex(projectHandle, ptrNodeId, ptrToIndexHandlePtr); 96 | // console.log(`_EN_getnodeindex: ${errorCode}`); 97 | // const indexOfNode = engine.getValue(ptrToIndexHandlePtr, 'i32'); 98 | // console.log(`Retrieved node index for ${nodeId}: ${indexOfNode}`); 99 | // engine._free(ptrNodeId); 100 | // engine._free(ptrToIndexHandlePtr); 101 | // return indexOfNode; 102 | //} 103 | // 104 | //// Call the function with verbose output for the single test 105 | //indexOfNode = getNodeIndex(engine, projectHandle, "J1"); 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | //console.log("epanetEngine", engine._getversion()); 114 | //console.log("epanetEngine", engine._open_epanet()); 115 | 116 | 117 | // Code to replicate: 118 | //for (let i = 1; i <= 3; i++) { 119 | // console.time("runSimulation"); 120 | // const workspace = new Workspace(); 121 | // const model = new Project(workspace); 122 | // workspace.writeFile("net1.inp", horribleInp); 123 | // model.open("net1.inp", "report.rpt", "out.bin"); 124 | // model.close(); 125 | // console.timeEnd("runSimulation"); 126 | // } 127 | 128 | 129 | // const workspace = new Workspace(); 130 | // this._instance = epanetEngine; 131 | // this._FS = this._instance.FS; 132 | // 133 | // workspace.writeFile("net1.inp", horribleInp); 134 | // writeFile(path: string, data: string | ArrayBufferView) { 135 | // this._FS.writeFile(path, data); 136 | // } 137 | // 138 | // const model = new Project(workspace); 139 | // this._ws = ws; 140 | // this._instance = ws._instance; 141 | // this._EN = new this._ws._instance.Epanet(); 142 | // 143 | -------------------------------------------------------------------------------- /packages/epanet-engine/tests/old/runProject.js: -------------------------------------------------------------------------------- 1 | import epanetEngine from "../../dist/epanet_version.js"; 2 | import fs from "fs"; 3 | const engine = await epanetEngine(); 4 | 5 | 6 | 7 | let errorCode; 8 | let projectHandle; 9 | let ptrToProjectHandlePtr; 10 | let ptrInpFile; 11 | let ptrRptFile; 12 | let ptrBinFile; 13 | let ptrNodeId; 14 | let ptrToIndexHandlePtr; 15 | let indexOfNode; 16 | 17 | const inpFileName = "./tests/networks/sw-network1.inp"; 18 | const inpText = fs.readFileSync(inpFileName); 19 | engine.FS.writeFile("net1.inp", inpText); 20 | 21 | // Create Project 22 | ptrToProjectHandlePtr = engine._malloc(4); 23 | errorCode = engine._EN_createproject(ptrToProjectHandlePtr); 24 | console.log(`_EN_createproject: ${errorCode}`); 25 | projectHandle = engine.getValue(ptrToProjectHandlePtr, 'i32'); 26 | engine._free(ptrToProjectHandlePtr); 27 | 28 | // Run Project 29 | const lenInpFile = engine.lengthBytesUTF8("net1.inp") + 1; 30 | ptrInpFile = engine._malloc(lenInpFile); 31 | engine.stringToUTF8("net1.inp", ptrInpFile, lenInpFile); 32 | 33 | const lenRptFile = engine.lengthBytesUTF8("report.rpt") + 1; 34 | ptrRptFile = engine._malloc(lenRptFile); 35 | engine.stringToUTF8("report.rpt", ptrRptFile, lenRptFile); 36 | 37 | const lenBinFile = engine.lengthBytesUTF8("out.bin") + 1; 38 | ptrBinFile = engine._malloc(lenBinFile); 39 | engine.stringToUTF8("out.bin", ptrBinFile, lenBinFile); 40 | 41 | 42 | 43 | 44 | //errorCode = engine._EN_runproject(projectHandle, ptrInpFile, ptrRptFile, ptrBinFile, 0); 45 | errorCode = engine._EN_open(projectHandle, ptrInpFile, ptrRptFile, ptrBinFile); 46 | console.log(`_EN_init: ${errorCode}`); 47 | engine._free(ptrInpFile); 48 | engine._free(ptrRptFile); 49 | engine._free(ptrBinFile); 50 | 51 | 52 | const startTime = performance.now(); 53 | 54 | errorCode = engine._EN_solveH(projectHandle); 55 | const endTime = performance.now(); 56 | const durationSeconds = (endTime - startTime) / 1000; 57 | console.log(`_EN_solveH: ${errorCode}`); 58 | 59 | 60 | console.log(`Completed in ${durationSeconds} seconds`); 61 | 62 | // Delete Project 63 | errorCode = engine._EN_deleteproject(projectHandle); 64 | console.log(`_EN_deleteproject: ${errorCode}`); 65 | 66 | -------------------------------------------------------------------------------- /packages/epanet-engine/tests/tests.md: -------------------------------------------------------------------------------- 1 | - Opening a large network 2 | - Running a large network 3 | - How many funciton calls per second -------------------------------------------------------------------------------- /packages/epanet-engine/type-gen/create-enums.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | // Configuration 5 | const HEADER_FILE = './epanet2_enums.h'; // Make sure this path is correct 6 | const OUTPUT_DIR = './type-gen/generated-enums'; 7 | const ENUM_PREFIX = 'EN_'; // Assuming EN_ prefix should be removed for TS name 8 | 9 | // Ensure output directory exists 10 | if (!fs.existsSync(OUTPUT_DIR)) { 11 | fs.mkdirSync(OUTPUT_DIR, { recursive: true }); 12 | } 13 | 14 | // Read the header file 15 | console.log(`Reading header file: ${HEADER_FILE}`); 16 | let headerContent; 17 | try { 18 | headerContent = fs.readFileSync(HEADER_FILE, 'utf8'); 19 | } catch (err) { 20 | console.error(`Error reading header file ${HEADER_FILE}:`, err.message); 21 | process.exit(1); // Exit if file can't be read 22 | } 23 | const lines = headerContent.split('\n'); 24 | console.log(`Read ${lines.length} lines from header file`); 25 | 26 | // Parse enum definitions 27 | function parseEnums() { 28 | const enums = []; 29 | let currentEnum = null; 30 | let inEnum = false; 31 | let lineNumber = 0; 32 | 33 | for (let i = 0; i < lines.length; i++) { 34 | lineNumber++; 35 | const line = lines[i].trim(); // Trim upfront 36 | 37 | // Skip empty lines early 38 | if (!line) { 39 | continue; 40 | } 41 | 42 | // Start of enum definition 43 | if (line.startsWith('typedef enum') && line.includes('{')) { // More robust start check 44 | console.log(`Found enum start at line ${lineNumber}: ${lines[i].trim()}`); // Log original line for context 45 | // Handle potential single-line start like "typedef enum {" 46 | inEnum = true; 47 | currentEnum = { 48 | name: '', 49 | entries: [] 50 | }; 51 | // Check if the line *also* contains the end brace 52 | if (line.includes('}')) { 53 | // Handle single-line enums or cases where { and } are on the same line as typedef 54 | console.log(`Potential complex enum definition start/end on line ${lineNumber}. Trying to parse name...`); 55 | handleEnumEnd(line, lineNumber); // Try to parse name immediately 56 | // Note: This simple parser might struggle with very complex C layouts. 57 | } 58 | continue; // Move to next line after handling start 59 | } 60 | 61 | // Inside enum definition 62 | if (inEnum && currentEnum) { // Ensure currentEnum is not null 63 | // End of enum definition 64 | if (line.startsWith('}')) { 65 | console.log(`Found enum end at line ${lineNumber}: ${lines[i].trim()}`); // Log original line 66 | handleEnumEnd(line, lineNumber); // Use helper function 67 | continue; // Move to next line after handling end 68 | } 69 | 70 | // Skip comments inside enums 71 | if (line.startsWith('//') || line.startsWith('/*')) { 72 | continue; 73 | } 74 | 75 | // Parse enum entry - simplified regex allowing optional EN_, handling potential assignment/value variations slightly more flexibly. 76 | // Assumes structure like `(EN_)NAME = VALUE, // comment` or `(EN_)NAME,` (value assigned implicitly) 77 | // This regex still makes assumptions but tries to be a bit more general. It won't parse implicit values correctly yet. 78 | const entryMatch = line.match(/^\s*(?:EN_)?([A-Z0-9_]+)\s*(?:=\s*(-?\d+))?\s*(?:,)?\s*(?:\/\/!?(?:<)?\s*(.*))?/); 79 | if (entryMatch) { 80 | // Note: entryMatch[2] (value) might be undefined if not explicitly assigned. 81 | // A real C parser would track the last assigned value for implicit increments. 82 | // This simple parser will only capture explicitly assigned values. 83 | const [, name, valueStr, comment] = entryMatch; 84 | if (valueStr !== undefined) { // Only add if value is explicit 85 | const entry = { 86 | name: name, 87 | value: parseInt(valueStr), // Use the captured value string 88 | comment: comment ? comment.trim() : '' 89 | }; 90 | console.log(`Found enum entry at line ${lineNumber}: ${entry.name} = ${entry.value}`); 91 | currentEnum.entries.push(entry); 92 | } else { 93 | console.log(`Found enum entry without explicit value at line ${lineNumber}: ${name}. Skipping (implicit values not supported).`); 94 | // To support implicit values, you'd need to track the last value + 1. 95 | } 96 | } else { 97 | console.log(`Warning: Could not parse line ${lineNumber} as enum entry: ${line}`); 98 | } 99 | } 100 | } 101 | 102 | // Helper function to handle extracting name at enum end (without regex) 103 | function handleEnumEnd(line, lineNumber) { 104 | const braceIndex = line.indexOf('}'); 105 | const semicolonIndex = line.lastIndexOf(';'); // Use lastIndexOf 106 | 107 | if (braceIndex === -1 || semicolonIndex === -1 || semicolonIndex <= braceIndex) { 108 | console.log(`Warning: Could not find valid '}' and ';' structure on enum end line ${lineNumber}: ${line}`); 109 | // Decide how to handle: maybe keep 'inEnum' true if this was unexpected? 110 | // For now, we still attempt to close the enum state. 111 | } else { 112 | // Extract substring between } and ; then trim whitespace 113 | const extractedName = line.substring(braceIndex + 1, semicolonIndex).trim(); 114 | 115 | if (extractedName.length > 0 && currentEnum) { 116 | // Validate name (optional, e.g., check for valid C identifier chars) 117 | if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(extractedName)) { 118 | currentEnum.name = extractedName; 119 | console.log(`Found enum name: ${currentEnum.name} with ${currentEnum.entries.length} entries`); 120 | enums.push(currentEnum); 121 | } else { 122 | console.log(`Warning: Extracted name "${extractedName}" seems invalid at line ${lineNumber}. Ignoring enum.`); 123 | } 124 | } else if (currentEnum) { 125 | console.log(`Warning: Could not extract name from enum ending at line ${lineNumber}: ${line}`); 126 | } else { 127 | console.log(`Warning: Found enum end at line ${lineNumber} but no current enum context.`); 128 | } 129 | } 130 | 131 | // Reset state regardless of successful name extraction to avoid getting stuck 132 | inEnum = false; 133 | currentEnum = null; 134 | } 135 | 136 | 137 | return enums; 138 | } 139 | 140 | // Generate TypeScript enum file 141 | function generateEnumFile(enumDef) { 142 | const { name, entries } = enumDef; 143 | // Simple check if name starts with ENUM_PREFIX before replacing 144 | const tsName = name.startsWith(ENUM_PREFIX) ? name.substring(ENUM_PREFIX.length) : name; 145 | 146 | if (!tsName) { 147 | console.warn(`Warning: Generated empty TS name for original name ${name}. Skipping file generation.`); 148 | return null; // Return null if name is invalid/empty 149 | } 150 | 151 | if (entries.length === 0) { 152 | console.warn(`Warning: Enum ${name} has no entries. Generating empty enum ${tsName}.ts`); 153 | } 154 | 155 | // Filter out entries if needed, or handle directly 156 | const content = `// Generated from ${HEADER_FILE} - ${new Date().toISOString()} 157 | // Original C enum name: ${name} 158 | 159 | enum ${tsName} { 160 | ${entries.map(entry => ` /** ${entry.comment || `Original value: ${entry.value}`} */\n ${entry.name} = ${entry.value},`).join('\n')} 161 | } 162 | 163 | export default ${tsName}; 164 | `; 165 | 166 | const outputPath = path.join(OUTPUT_DIR, `${tsName}.ts`); 167 | try { 168 | fs.writeFileSync(outputPath, content); 169 | console.log(`Generated enum file: ${outputPath}`); 170 | return tsName; // Return the generated name for index file 171 | } catch (err) { 172 | console.error(`Error writing enum file ${outputPath}:`, err.message); 173 | return null; // Indicate failure 174 | } 175 | } 176 | 177 | // Generate index file 178 | function generateIndexFile(enumNames) { 179 | // Filter out any nulls from failed generations 180 | const validEnumNames = enumNames.filter(name => name !== null); 181 | if (validEnumNames.length === 0) { 182 | console.log("No valid enums were generated, skipping index.ts generation."); 183 | return; 184 | } 185 | 186 | const content = `// Generated index file - ${new Date().toISOString()} 187 | ${validEnumNames.map(name => `export { default as ${name} } from './${name}';`).join('\n')} 188 | `; 189 | const indexPath = path.join(OUTPUT_DIR, 'index.ts'); 190 | try { 191 | fs.writeFileSync(indexPath, content); 192 | console.log(`Generated index file: ${indexPath}`); 193 | } catch (err) { 194 | console.error(`Error writing index file ${indexPath}:`, err.message); 195 | } 196 | } 197 | 198 | // Main execution 199 | console.log('Starting enum parsing...'); 200 | const enums = parseEnums(); 201 | console.log(`Parsing complete. Found ${enums.length} enums.`); 202 | 203 | if (enums.length === 0) { 204 | console.log('No enums parsed successfully.'); 205 | // Optionally show file snippets again or suggest checking warnings 206 | } 207 | 208 | console.log('Generating TypeScript files...'); 209 | const generatedNames = enums.map(generateEnumFile).filter(name => name !== null); // Get names and filter nulls 210 | 211 | generateIndexFile(generatedNames); 212 | 213 | console.log(`Generation complete. Generated ${generatedNames.length} enum files in ${OUTPUT_DIR}`); -------------------------------------------------------------------------------- /packages/epanet-js/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | coverage 6 | 7 | # API Markdown Folders 8 | temp 9 | etc 10 | input 11 | markdown 12 | -------------------------------------------------------------------------------- /packages/epanet-js/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Luke Butler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/epanet-js/README.md: -------------------------------------------------------------------------------- 1 | # 💧EPANET-JS 2 | 3 | ![](https://github.com/modelcreate/epanet-js/workflows/CI/badge.svg) [![codecov](https://codecov.io/gh/modelcreate/epanet-js/branch/master/graph/badge.svg)](https://codecov.io/gh/modelcreate/epanet-js) ![npm](https://img.shields.io/npm/v/epanet-js) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![tested with jest](https://img.shields.io/badge/tested_with-jest-99424f.svg)](https://github.com/facebook/jest) 4 | 5 | Water distribution network modelling, either in the browser or Node. Uses the Open Water Analytics EPANET v2.2 toolkit compiled to Javascript. 6 | 7 | > **Note**: All version before 1.0.0 should be considered beta with potential breaking changes between releases, use in production with caution. 8 | 9 | ## Install 10 | 11 | To install the stable version with npm: 12 | 13 | ``` 14 | $ npm install epanet-js 15 | ``` 16 | 17 | or with yarn: 18 | 19 | ``` 20 | $ yarn add epanet-js 21 | ``` 22 | 23 | For those without a module bundler, the epanet-js package will soon also be available on unpkg as a precompiled UMD builds. This will allow you to drop a UMD build in a `