├── .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 |
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 | [](https://github.com/modelcreate/epanet-js/actions?query=workflow%3ACI) [](https://codecov.io/gh/modelcreate/epanet-js)  [](https://github.com/prettier/prettier) [](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 |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://codecov.io/gh/modelcreate/epanet-js)  [](https://github.com/prettier/prettier) [](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 `