├── pnpm-workspace.yaml ├── packages ├── epanet-js │ ├── src │ │ ├── enum │ │ │ ├── Option │ │ │ │ ├── index.ts │ │ │ │ └── Option.ts │ │ │ ├── LinkType │ │ │ │ ├── index.ts │ │ │ │ └── LinkType.ts │ │ │ ├── NodeType │ │ │ │ ├── index.ts │ │ │ │ └── NodeType.ts │ │ │ ├── PumpType │ │ │ │ ├── index.ts │ │ │ │ └── PumpType.ts │ │ │ ├── ControlType │ │ │ │ ├── index.ts │ │ │ │ └── ControlType.ts │ │ │ ├── CountType │ │ │ │ ├── index.ts │ │ │ │ └── CountType.ts │ │ │ ├── CurveType │ │ │ │ ├── index.ts │ │ │ │ └── CurveType.ts │ │ │ ├── DemandModel │ │ │ │ ├── index.ts │ │ │ │ └── DemandModel.ts │ │ │ ├── FlowUnits │ │ │ │ ├── index.ts │ │ │ │ └── FlowUnits.ts │ │ │ ├── MixingModel │ │ │ │ ├── index.ts │ │ │ │ └── MixingModel.ts │ │ │ ├── ObjectType │ │ │ │ ├── index.ts │ │ │ │ └── ObjectType.ts │ │ │ ├── QualityType │ │ │ │ ├── index.ts │ │ │ │ └── QualityType.ts │ │ │ ├── RuleObject │ │ │ │ ├── index.ts │ │ │ │ └── RuleObject.ts │ │ │ ├── RuleStatus │ │ │ │ ├── index.ts │ │ │ │ └── RuleStatus.ts │ │ │ ├── SizeLimits │ │ │ │ ├── index.ts │ │ │ │ └── SizeLimits.ts │ │ │ ├── SourceType │ │ │ │ ├── index.ts │ │ │ │ └── SourceType.ts │ │ │ ├── HeadLossType │ │ │ │ ├── index.ts │ │ │ │ └── HeadLossType.ts │ │ │ ├── InitHydOption │ │ │ │ ├── index.ts │ │ │ │ └── InitHydOption.ts │ │ │ ├── LinkProperty │ │ │ │ ├── index.ts │ │ │ │ └── LinkProperty.ts │ │ │ ├── NodeProperty │ │ │ │ ├── index.ts │ │ │ │ └── NodeProperty.ts │ │ │ ├── PumpStateType │ │ │ │ ├── index.ts │ │ │ │ └── PumpStateType.ts │ │ │ ├── RuleOperator │ │ │ │ ├── index.ts │ │ │ │ └── RuleOperator.ts │ │ │ ├── RuleVariable │ │ │ │ ├── index.ts │ │ │ │ └── RuleVariable.ts │ │ │ ├── StatisticType │ │ │ │ ├── index.ts │ │ │ │ └── StatisticType.ts │ │ │ ├── StatusReport │ │ │ │ ├── index.ts │ │ │ │ └── StatusReport.ts │ │ │ ├── TimeParameter │ │ │ │ ├── index.ts │ │ │ │ └── TimeParameter.ts │ │ │ ├── ActionCodeType │ │ │ │ ├── index.ts │ │ │ │ └── ActionCodeType.ts │ │ │ ├── LinkStatusType │ │ │ │ ├── index.ts │ │ │ │ └── LinkStatusType.ts │ │ │ ├── AnalysisStatistic │ │ │ │ ├── index.ts │ │ │ │ └── AnalysisStatistic.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── Workspace │ │ │ └── Workspace.ts │ │ └── OutputReader │ │ │ └── index.ts │ ├── .gitignore │ ├── vitest.config.ts │ ├── vite.config.ts │ ├── test │ │ ├── data │ │ │ ├── tankTest.inp │ │ │ ├── AllElementsSmallNetwork.inp │ │ │ ├── net1_backup.inp │ │ │ └── net1.inp │ │ ├── Workspace.test.ts │ │ ├── Project.test.ts │ │ ├── benchmark │ │ │ ├── runSim.js │ │ │ └── index.js │ │ ├── version-guard.test.ts │ │ ├── Project │ │ │ ├── WaterQualityAnalysisFunctions.test.ts │ │ │ ├── ProjectFunctions.test.ts │ │ │ ├── DataCurveFunctions.test.ts │ │ │ ├── NodalDemandFunctions.test.ts │ │ │ ├── AnalysisOptionsFunctions.test.ts │ │ │ ├── TimePatternFunctions.test.ts │ │ │ ├── SimpleControlFunctons.test.ts │ │ │ ├── HydraulicAnalysisFunctions.test.ts │ │ │ ├── ReportingFunctions.test.ts │ │ │ └── RuleBasedControlFunctions.test.ts │ │ └── api-coverage.test.ts │ ├── tsconfig.json │ ├── LICENSE │ ├── package.json │ └── README.md └── epanet-engine │ ├── tests │ ├── tests.md │ ├── my-network.inp │ ├── old │ │ ├── runProject.js │ │ ├── benchmark.js │ │ └── index.js │ ├── index.js │ ├── benchmarks │ │ ├── open-large-network.js │ │ ├── run-long-sim.js │ │ └── calls-per-second.js │ └── browser │ │ └── epanet-worker.js │ ├── Dockerfile │ ├── package.json │ ├── README.md │ ├── generate_exports.sh │ ├── build.sh │ └── type-gen │ └── create-enums.js ├── .gitattributes ├── package.json ├── .gitignore ├── LICENSE ├── .vscode └── settings.json ├── .github └── workflows │ └── main.yml └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/Option/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Option'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/LinkType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LinkType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/NodeType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NodeType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/PumpType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PumpType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/ControlType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ControlType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/CountType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CountType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/CurveType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './CurveType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/DemandModel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './DemandModel'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/FlowUnits/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './FlowUnits'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/MixingModel/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './MixingModel'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/ObjectType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ObjectType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/QualityType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './QualityType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleObject/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RuleObject'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleStatus/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RuleStatus'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/SizeLimits/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './SizeLimits'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/SourceType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './SourceType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/HeadLossType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './HeadLossType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/InitHydOption/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './InitHydOption'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/LinkProperty/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LinkProperty'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/NodeProperty/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './NodeProperty'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/PumpStateType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './PumpStateType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleOperator/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RuleOperator'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleVariable/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './RuleVariable'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/StatisticType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './StatisticType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/StatusReport/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './StatusReport'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/TimeParameter/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './TimeParameter'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/ActionCodeType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './ActionCodeType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/LinkStatusType/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LinkStatusType'; 2 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/AnalysisStatistic/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './AnalysisStatistic'; 2 | -------------------------------------------------------------------------------- /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-js/src/enum/LinkStatusType/LinkStatusType.ts: -------------------------------------------------------------------------------- 1 | enum LinkStatusType { 2 | Closed = 0, 3 | Open = 1, 4 | } 5 | 6 | export default LinkStatusType; 7 | -------------------------------------------------------------------------------- /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/src/enum/DemandModel/DemandModel.ts: -------------------------------------------------------------------------------- 1 | enum DemandModel { 2 | DDA = 0, //!< Demand driven analysis 3 | PDA = 1, //!< Pressure driven analysis 4 | } 5 | 6 | export default DemandModel; 7 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/HeadLossType/HeadLossType.ts: -------------------------------------------------------------------------------- 1 | enum HeadLossType { 2 | HW = 0, //!< Hazen-Williams 3 | DW = 1, //!< Darcy-Weisbach 4 | CM = 2, //!< Chezy-Manning 5 | } 6 | export default HeadLossType; 7 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/NodeType/NodeType.ts: -------------------------------------------------------------------------------- 1 | enum NodeType { 2 | Junction = 0, //!< Junction node 3 | Reservoir = 1, //!< Reservoir node 4 | Tank = 2, //!< Storage tank node 5 | } 6 | 7 | export default NodeType; 8 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/SizeLimits/SizeLimits.ts: -------------------------------------------------------------------------------- 1 | enum SizeLimits { 2 | MaxId = 31, //!< Max. # characters in ID name 3 | MaxMsg = 255, //!< Max. # characters in message text 4 | } 5 | 6 | export default SizeLimits; 7 | -------------------------------------------------------------------------------- /packages/epanet-js/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Workspace } from './Workspace/Workspace'; 2 | export { default as Project } from './Project/Project'; 3 | export * from './enum/index'; 4 | export * from './OutputReader/index'; 5 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleStatus/RuleStatus.ts: -------------------------------------------------------------------------------- 1 | enum RuleStatus { 2 | IsOpen = 1, //!< Link is open 3 | IsClosed = 2, //!< Link is closed 4 | IsActive = 3, //!< Control valve is active 5 | } 6 | 7 | export default RuleStatus; 8 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/PumpType/PumpType.ts: -------------------------------------------------------------------------------- 1 | enum PumpType { 2 | ConstHP = 0, //!< Constant horsepower 3 | PowerFunc = 1, //!< Power function 4 | Custom = 2, //!< User-defined custom curve 5 | NoCurve = 3, //!< No curve 6 | } 7 | 8 | export default PumpType; 9 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleObject/RuleObject.ts: -------------------------------------------------------------------------------- 1 | enum RuleObject { 2 | Node = 6, //!< Clause refers to a node 3 | Link = 7, //!< Clause refers to a link 4 | System = 8, //!< Clause refers to a system parameter (e.g., time) 5 | } 6 | 7 | export default RuleObject; 8 | -------------------------------------------------------------------------------- /packages/epanet-js/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "node", 7 | coverage: { 8 | provider: "v8", 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/StatusReport/StatusReport.ts: -------------------------------------------------------------------------------- 1 | enum StatusReport { 2 | NoReport = 0, //!< No status reporting 3 | NormalReport = 1, //!< Normal level of status reporting 4 | FullReport = 2, //!< Full level of status reporting 5 | } 6 | 7 | export default StatusReport; 8 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/MixingModel/MixingModel.ts: -------------------------------------------------------------------------------- 1 | enum MixingModel { 2 | Mix1 = 0, //!< Complete mix model 3 | Mix2 = 1, //!< 2-compartment model 4 | FIFO = 2, //!< First in, first out model 5 | LIFO = 3, //!< Last in, first out model 6 | } 7 | 8 | export default MixingModel; 9 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/QualityType/QualityType.ts: -------------------------------------------------------------------------------- 1 | enum QualityType { 2 | None = 0, //!< No quality analysis 3 | Chem = 1, //!< Chemical fate and transport 4 | Age = 2, //!< Water age analysis 5 | Trace = 3, //!< Source tracing analysis 6 | } 7 | 8 | export default QualityType; 9 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/ActionCodeType/ActionCodeType.ts: -------------------------------------------------------------------------------- 1 | enum ActionCodeType { 2 | Unconditional = 0, //!< Delete all controls and connecing links 3 | Conditional = 1, //!< Cancel object deletion if it appears in controls or has connecting links 4 | } 5 | 6 | export default ActionCodeType; 7 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/PumpStateType/PumpStateType.ts: -------------------------------------------------------------------------------- 1 | enum PumpStateType { 2 | PumpXHead = 0, //!< Pump closed - cannot supply head 3 | PumpClosed = 2, //!< Pump closed 4 | PumpOpen = 3, //!< Pump open 5 | PumpXFlow = 5, //!< Pump open - cannot supply flow 6 | } 7 | 8 | export default PumpStateType; 9 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/ObjectType/ObjectType.ts: -------------------------------------------------------------------------------- 1 | enum ObjectType { 2 | Node = 0, //!< Nodes 3 | Link = 1, //!< Links 4 | TimePat = 2, //!< Time patterns 5 | Curve = 3, //!< Data curves 6 | Control = 4, //!< Simple controls 7 | Rule = 5, //!< Control rules 8 | } 9 | 10 | export default ObjectType; 11 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/CurveType/CurveType.ts: -------------------------------------------------------------------------------- 1 | enum CurveType { 2 | VolumeCurve = 0, //!< Tank volume v. depth curve 3 | PumpCurve = 1, //!< Pump head v. flow curve 4 | EfficCurve = 2, //!< Pump efficiency v. flow curve 5 | HlossCurve = 3, //!< Valve head loss v. flow curve 6 | GenericCurve = 4, //!< Generic curve 7 | } 8 | 9 | export default CurveType; 10 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/ControlType/ControlType.ts: -------------------------------------------------------------------------------- 1 | enum ControlType { 2 | LowLevel = 0, //!< Act when pressure or tank level drops below a setpoint 3 | HiLevel = 1, //!< Act when pressure or tank level rises above a setpoint 4 | Timer = 2, //!< Act at a prescribed elapsed amount of time 5 | TimeOfDay = 3, //!< Act at a particular time of day 6 | } 7 | 8 | export default ControlType; 9 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/InitHydOption/InitHydOption.ts: -------------------------------------------------------------------------------- 1 | enum InitHydOption { 2 | NoSave = 0, //!< Don't save hydraulics; don't re-initialize flows 3 | Save = 1, //!< Save hydraulics to file, don't re-initialize flows 4 | InitFlow = 10, //!< Don't save hydraulics; re-initialize flows 5 | SaveAndInit = 11, //!< Save hydraulics; re-initialize flows 6 | } 7 | 8 | export default InitHydOption; 9 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/SourceType/SourceType.ts: -------------------------------------------------------------------------------- 1 | enum SourceType { 2 | Concen = 0, //!< Sets the concentration of external inflow entering a node 3 | Mass = 1, //!< Injects a given mass/minute into a node 4 | SetPoint = 2, //!< Sets the concentration leaving a node to a given value 5 | FlowPaced = 3, //!< Adds a given value to the concentration leaving a node 6 | } 7 | 8 | export default SourceType; 9 | -------------------------------------------------------------------------------- /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-js/src/enum/StatisticType/StatisticType.ts: -------------------------------------------------------------------------------- 1 | enum StatisticType { 2 | Series = 0, //!< Report all time series points 3 | Average = 1, //!< Report average value over simulation period 4 | Minimum = 2, //!< Report minimum value over simulation period 5 | Maximum = 3, //!< Report maximum value over simulation period 6 | Range = 4, //!< Report maximum - minimum over simulation period 7 | } 8 | 9 | export default StatisticType; 10 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/LinkType/LinkType.ts: -------------------------------------------------------------------------------- 1 | enum LinkType { 2 | CVPipe = 0, //!< Pipe with check valve 3 | Pipe = 1, //!< Pipe 4 | Pump = 2, //!< Pump 5 | PRV = 3, //!< Pressure reducing valve 6 | PSV = 4, //!< Pressure sustaining valve 7 | PBV = 5, //!< Pressure breaker valve 8 | FCV = 6, //!< Flow control valve 9 | TCV = 7, //!< Throttle control valve 10 | GPV = 8, //!< General purpose valve 11 | } 12 | 13 | export default LinkType; 14 | -------------------------------------------------------------------------------- /packages/epanet-js/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import dts from "vite-plugin-dts"; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: "src/index.ts", 8 | name: "EpanetJs", 9 | formats: ["es", "cjs"], 10 | fileName: (format) => `index.${format === "es" ? "mjs" : "cjs"}`, 11 | }, 12 | rollupOptions: { 13 | external: ["@model-create/epanet-engine"], 14 | }, 15 | }, 16 | plugins: [dts()], 17 | }); 18 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/CountType/CountType.ts: -------------------------------------------------------------------------------- 1 | enum CountType { 2 | NodeCount = 0, //!< Number of nodes (junctions + tanks + reservoirs) 3 | TankCount = 1, //!< Number of tanks and reservoirs 4 | LinkCount = 2, //!< Number of links (pipes + pumps + valves) 5 | PatCount = 3, //!< Number of time patterns 6 | CurveCount = 4, //!< Number of data curves 7 | ControlCount = 5, //!< Number of simple controls 8 | RuleCount = 6, //!< Number of rule-based controls 9 | } 10 | 11 | export default CountType; 12 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/FlowUnits/FlowUnits.ts: -------------------------------------------------------------------------------- 1 | enum FlowUnits { 2 | CFS = 0, //!< Cubic feet per second 3 | GPM = 1, //!< Gallons per minute 4 | MGD = 2, //!< Million gallons per day 5 | IMGD = 3, //!< Imperial million gallons per day 6 | AFD = 4, //!< Acre-feet per day 7 | LPS = 5, //!< Liters per second 8 | LPM = 6, //!< Liters per minute 9 | MLD = 7, //!< Million liters per day 10 | CMH = 8, //!< Cubic meters per hour 11 | CMD = 9, //!< Cubic meters per day 12 | } 13 | 14 | export default FlowUnits; 15 | -------------------------------------------------------------------------------- /packages/epanet-js/test/data/tankTest.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | [JUNCTIONS] 3 | J1 0 0 4 | J2 0 0 5 | [RESERVOIRS] 6 | R1 10 7 | [TANKS] 8 | T1 10 3 2 5 200 20 CURVE_ID 9 | T2 10 3 2 5 200 20 10 | [PIPES] 11 | P1 J1 J2 1 200 1 0.1 OPEN 12 | P2 J2 T1 1 200 1 0.1 OPEN 13 | P3 J2 T2 1 200 1 0.1 OPEN 14 | [VALVES] 15 | [PUMPS] 16 | [COORDINATES] 17 | J1 0 0 18 | J2 1 1 19 | T1 2 2 20 | T2 4 4 21 | R1 0 0 22 | [STATUS] 23 | [TIMES] 24 | [OPTIONS] 25 | [PATTERNS] 26 | [CURVES] 27 | CURVE_ID 2 5 28 | CURVE_ID 5 5 29 | [DEMANDS] 30 | [VERTICES] 31 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleOperator/RuleOperator.ts: -------------------------------------------------------------------------------- 1 | enum RuleOperator { 2 | EqualTo = 0, //!< Equal to | EN_R_EQ 3 | NotEqualTo = 1, //!< Not equal | EN_R_NE 4 | LessOrEqualTo = 2, //!< Less than or equal to | EN_R_LE 5 | GreaterOrEqualTo = 3, //!< Greater than or equal to | EN_R_GE 6 | LessThan = 4, //!< Less than | EN_R_LT 7 | GreaterThan = 5, //!< Greater than | EN_R_GT 8 | Is = 6, //!< Is equal to | EN_R_IS 9 | Not = 7, //!< Is not equal to | EN_R_NOT 10 | Below = 8, //!< Is below | EN_R_BELOW 11 | Above = 9, //!< Is above | EN_R_ABOVE 12 | } 13 | 14 | export default RuleOperator; 15 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/AnalysisStatistic/AnalysisStatistic.ts: -------------------------------------------------------------------------------- 1 | enum AnalysisStatistic { 2 | Iterations = 0, //!< Number of hydraulic iterations taken 3 | RelativeError = 1, //!< Sum of link flow changes / sum of link flows 4 | MaxHeadError = 2, //!< Largest head loss error for links 5 | MaxFlowChange = 3, //!< Largest flow change in links 6 | MassBalance = 4, //!< Cumulative water quality mass balance ratio 7 | DeficientNodes = 5, //!< Number of pressure deficient nodes 8 | DemandReduction = 6, //!< % demand reduction at pressure deficient nodes 9 | } 10 | 11 | export default AnalysisStatistic; 12 | -------------------------------------------------------------------------------- /packages/epanet-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "types": ["vitest/globals"] 18 | }, 19 | "include": ["src", "test"] 20 | } -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/RuleVariable/RuleVariable.ts: -------------------------------------------------------------------------------- 1 | enum RuleVariable { 2 | Demand = 0, //!< Nodal demand 3 | Head = 1, //!< Nodal hydraulic head 4 | Grade = 2, //!< Nodal hydraulic grade 5 | Level = 3, //!< Tank water level 6 | Pressure = 4, //!< Nodal pressure 7 | Flow = 5, //!< Link flow rate 8 | Status = 6, //!< Link status 9 | Setting = 7, //!< Link setting 10 | Power = 8, //!< Pump power output 11 | Time = 9, //!< Elapsed simulation time 12 | ClockTime = 10, //!< Time of day 13 | FillTime = 11, //!< Time to fill a tank 14 | DrainTime = 12, //!< Time to drain a tank 15 | } 16 | 17 | export default RuleVariable; 18 | -------------------------------------------------------------------------------- /packages/epanet-js/test/Workspace.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import { Workspace } from "../src"; 3 | 4 | const workspace = new Workspace(); 5 | test("Returns workspace version", async () => { 6 | await workspace.loadModule(); 7 | expect(workspace.version).toBe(20200); 8 | }); 9 | 10 | test("Returns an error", () => { 11 | expect(workspace.getError(201)).toBe("Error 201: syntax error"); 12 | }); 13 | 14 | test("Read and write a file", () => { 15 | const multiLine = `Test File 16 | New Line`; 17 | workspace.writeFile("test.inp", multiLine); 18 | const result = workspace.readFile("test.inp"); 19 | 20 | expect(result).toBe(multiLine); 21 | }); 22 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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/src/enum/TimeParameter/TimeParameter.ts: -------------------------------------------------------------------------------- 1 | enum TimeParameter { 2 | Duration = 0, //!< Total simulation duration 3 | HydStep = 1, //!< Hydraulic time step 4 | QualStep = 2, //!< Water quality time step 5 | PatternStep = 3, //!< Time pattern period 6 | PatternStart = 4, //!< Time when time patterns begin 7 | ReportStep = 5, //!< Reporting time step 8 | ReportStart = 6, //!< Time when reporting starts 9 | RuleStep = 7, //!< Rule-based control evaluation time step 10 | Statistic = 8, //!< Reporting statistic code (see @ref EN_StatisticType) 11 | Periods = 9, //!< Number of reporting time periods (read only) 12 | StartTime = 10, //!< Simulation starting time of day 13 | HTime = 11, //!< Elapsed time of current hydraulic solution (read only) 14 | QTime = 12, //!< Elapsed time of current quality solution (read only) 15 | HaltFlag = 13, //!< Flag indicating if the simulation was halted (read only) 16 | NextEvent = 14, //!< Shortest time until a tank becomes empty or full (read only) 17 | NextEventTank = 15, //!< Index of tank with shortest time to become empty or full (read only) 18 | } 19 | 20 | export default TimeParameter; 21 | -------------------------------------------------------------------------------- /packages/epanet-engine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@model-create/epanet-engine", 3 | "version": "0.8.0-alpha.8", 4 | "type": "module", 5 | "description": "EPANET WASM engine", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.js", 8 | "typings": "dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.cjs" 13 | } 14 | }, 15 | "scripts": { 16 | "build:dockerimage": "docker build -t epanet-js-engine .", 17 | "build:dockerimage-arm64": "docker build --build-arg EMSDK_TAG_SUFFIX='-arm64' -t epanet-js-engine .", 18 | "build:dockerimage-noCache": "docker build --no-cache --build-arg EMSDK_TAG_SUFFIX='-arm64' -t epanet-js-engine .", 19 | "build:emscripten": "docker run --rm -v \"$(pwd):/src\" epanet-js-engine ./build.sh", 20 | "build:types": "node type-gen/create-types.js", 21 | "build": "npm run build:dockerimage && npm run build:emscripten && npm run build:types", 22 | "build:arm64": "npm run build:dockerimage-arm64 && npm run build:emscripten && npm run build:types", 23 | "prepublishOnly": "npm run build:dockerimage-noCache && npm run build:emscripten && npm run build:types" 24 | }, 25 | "keywords": [], 26 | "files": [ 27 | "dist" 28 | ], 29 | "author": "", 30 | "license": "MIT" 31 | } 32 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/LinkProperty/LinkProperty.ts: -------------------------------------------------------------------------------- 1 | enum LinkProperty { 2 | Diameter = 0, //!< Pipe/valve diameter 3 | Length = 1, //!< Pipe length 4 | Roughness = 2, //!< Pipe roughness coefficient 5 | MinorLoss = 3, //!< Pipe/valve minor loss coefficient 6 | InitStatus = 4, //!< Initial status (see @ref EN_LinkStatusType) 7 | InitSetting = 5, //!< Initial pump speed or valve setting 8 | KBulk = 6, //!< Bulk chemical reaction coefficient 9 | KWall = 7, //!< Pipe wall chemical reaction coefficient 10 | Flow = 8, //!< Current computed flow rate (read only) 11 | Velocity = 9, //!< Current computed flow velocity (read only) 12 | Headloss = 10, //!< Current computed head loss (read only) 13 | Status = 11, //!< Current link status (see @ref EN_LinkStatusType) 14 | Setting = 12, //!< Current link setting 15 | Energy = 13, //!< Current computed pump energy usage (read only) 16 | LinkQual = 14, //!< Current computed link quality (read only) 17 | LinkPattern = 15, //!< Pump speed time pattern index 18 | PumpState = 16, //!< Current computed pump state (read only) (see @ref EN_PumpStateType) 19 | PumpEffic = 17, //!< Current computed pump efficiency (read only) 20 | PumpPower = 18, //!< Pump constant power rating 21 | PumpHCurve = 19, //!< Pump head v. flow curve index 22 | PumpECurve = 20, //!< Pump efficiency v. flow curve index 23 | PumpECost = 21, //!< Pump average energy price 24 | PumpEPat = 22, //!< Pump energy price time pattern index 25 | } 26 | 27 | export default LinkProperty; 28 | -------------------------------------------------------------------------------- /packages/epanet-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epanet-js", 3 | "version": "0.8.0-alpha.8", 4 | "license": "MIT", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.mjs", 8 | "types": "dist/src/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.mjs", 15 | "require": "./dist/index.cjs", 16 | "types": "./dist/src/index.d.ts" 17 | } 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/modelcreate/epanet-js.git" 22 | }, 23 | "scripts": { 24 | "build": "vite build", 25 | "dev": "vite build --watch", 26 | "typecheck": "tsc --noEmit", 27 | "test": "vitest", 28 | "test:watch": "vitest watch", 29 | "test:coverage": "vitest run --coverage", 30 | "test:coverage-ci": "vitest run --coverage --reporter=junit --outputFile=test-report.junit.xml", 31 | "prepublishOnly": "pnpm run build" 32 | }, 33 | "dependencies": { 34 | "@model-create/epanet-engine": "workspace:*" 35 | }, 36 | "peerDependencies": {}, 37 | "prettier": { 38 | "semi": true, 39 | "singleQuote": false, 40 | "tabWidth": 2, 41 | "trailingComma": "all", 42 | "printWidth": 80 43 | }, 44 | "description": "EPANET engine in javascript", 45 | "author": "Luke Butler", 46 | "devDependencies": { 47 | "@types/node": "^22.15.3", 48 | "@vitest/coverage-v8": "^3.1.2", 49 | "typescript": "^5.0.0", 50 | "vite": "^5.0.0", 51 | "vite-plugin-dts": "^3.0.0", 52 | "vitest": "^3.1.2" 53 | } 54 | } -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/Option/Option.ts: -------------------------------------------------------------------------------- 1 | enum Option { 2 | Trials = 0, //!< Maximum trials allowed for hydraulic convergence 3 | Accuracy = 1, //!< Total normalized flow change for hydraulic convergence 4 | Tolerance = 2, //!< Water quality tolerance 5 | Emitexpon = 3, //!< Exponent in emitter discharge formula 6 | DemandMult = 4, //!< Global demand multiplier 7 | HeadError = 5, //!< Maximum head loss error for hydraulic convergence 8 | FlowChange = 6, //!< Maximum flow change for hydraulic convergence 9 | HeadlossForm = 7, //!< Head loss formula (see @ref EN_HeadLossType) 10 | GlobalEffic = 8, //!< Global pump efficiency (percent) 11 | GlobalPrice = 9, //!< Global energy price per KWH 12 | GlobalPattern = 10, //!< Index of a global energy price pattern 13 | DemandCharge = 11, //!< Energy charge per max. KW usage 14 | SpGravity = 12, //!< Specific gravity 15 | SpViscos = 13, //!< Specific viscosity (relative to water at 20 deg C) 16 | Unbalanced = 14, //!< Extra trials allowed if hydraulics don't converge 17 | CheckFreq = 15, //!< Frequency of hydraulic status checks 18 | MaxCheck = 16, //!< Maximum trials for status checking 19 | DampLimit = 17, //!< Accuracy level where solution damping begins 20 | SpDiffus = 18, //!< Specific diffusivity (relative to chlorine at 20 deg C) 21 | BulkOrder = 19, //!< Bulk water reaction order for pipes 22 | WallOrder = 20, //!< Wall reaction order for pipes (either 0 or 1) 23 | TankOrder = 21, //!< Bulk water reaction order for tanks 24 | ConcenLimit = 22, //!< Limiting concentration for growth reactions 25 | } 26 | 27 | export default Option; 28 | -------------------------------------------------------------------------------- /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-js/test/Project.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../src"; 2 | import { NodeType, NodeProperty } from "../src/enum"; 3 | 4 | const workspace = new Workspace(); 5 | await workspace.loadModule(); 6 | 7 | describe("Epanet Project", () => { 8 | describe("Project Initialization", () => { 9 | test("throw error if module not loaded", async () => { 10 | const newWorkspace = new Workspace(); 11 | const model = new Project(newWorkspace); 12 | 13 | expect(() => model.init("report.rpt", "out.bin", 0, 0)).toThrow( 14 | "EPANET engine not loaded. Call loadModule() on the Workspace first.", 15 | ); 16 | await newWorkspace.loadModule(); 17 | expect(() => model.init("report.rpt", "out.bin", 0, 0)).not.toThrow(); 18 | }); 19 | }); 20 | describe("addNode", () => { 21 | test("should throw without a network init", () => { 22 | function catchError() { 23 | const model = new Project(workspace); 24 | model.addNode("J1", NodeType.Junction); 25 | } 26 | 27 | expect(catchError).toThrow("102"); 28 | }); 29 | 30 | test("add new node with properties", () => { 31 | const model = new Project(workspace); 32 | model.init("report.rpt", "out.bin", 0, 0); 33 | const nodeId = model.addNode("J1", NodeType.Junction); 34 | model.setJunctionData(nodeId, 700, 0, ""); 35 | 36 | const nodeType = model.getNodeType(nodeId); 37 | expect(nodeType).toEqual(NodeType.Junction); 38 | 39 | const elev = model.getNodeValue(nodeId, NodeProperty.Elevation); 40 | expect(elev).toEqual(700); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/epanet-js/test/data/AllElementsSmallNetwork.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | All Elements Small Network 3 | [JUNCTIONS] 4 | 1 6 5 | 2 6 6 | 3 15.2 7 | 15 15.2 8 | 9_UP 4 9 | 9_DOWN 4 10 | 12_UP 15.2 11 | 12_DOWN 15.2 12 | [RESERVOIRS] 13 | 10 80 14 | [TANKS] 15 | 11 10 2 0 4 20 0 16 | [PIPES] 17 | 4 10 15 200 200 148 0 OPEN 18 | 5 9_DOWN 2 100 100 148 0 OPEN 19 | 6 15 9_UP 50 100 148 0 OPEN 20 | 7 2 1 100 80 148 0 OPEN 21 | 8 10 12_UP 100 100 148 0 OPEN 22 | 13 12_DOWN 11 607.2 80 148 0 OPEN 23 | 14 11 3 477.35 80 148 0 OPEN 24 | [VALVES] 25 | 12 12_UP 12_DOWN 200 TCV 0.01 0 26 | [PUMPS] 27 | 9 9_UP 9_DOWN POWER 0.4 SPEED 1 28 | [STATUS] 29 | 12 OPEN 30 | [OPTIONS] 31 | Units LPS 32 | Headloss H-W 33 | Unbalanced Continue 10 34 | Accuracy 0.0001 35 | [TIMES] 36 | Duration 86400 SEC 37 | Start ClockTime 21600 SEC 38 | Pattern Start 0 SEC 39 | Pattern TimeStep 3600 SEC 40 | Report Start 0 SEC 41 | Report TimeStep 3600 SEC 42 | Hydraulic TimeStep 3600 SEC 43 | [PATTERNS] 44 | 1 0.645 0.869 1.093 1.317 1.263 1.208 45 | 1 1.054 1.093 1.131 1.077 0.923 0.923 46 | 1 0.923 0.892 0.761 0.629 0.498 0.467 47 | 1 0.421 0.344 0.367 0.321 0.374 0.421 48 | [DEMANDS] 49 | 1 1 1 50 | 2 1 1 51 | 3 1 1 52 | 15 0 1 53 | [COORDINATES] 54 | 1 -0.47797779 39.36906038 55 | 2 -0.4892666 39.37166954 56 | 3 -0.50223914 39.36409428 57 | 15 -0.491875 39.36694853 58 | 9_UP -0.49 39.36694853 59 | 9_DOWN -0.49 39.36694853 60 | 12_UP -0.51414114 39.3671807 61 | 12_DOWN -0.51414114 39.3671807 62 | 10 -0.50892647 39.37036765 63 | 11 -0.50766808 39.36499539 64 | [VERTICES] 65 | 4 -0.50334559 39.36838235 66 | 4 -0.49775735 39.36742647 67 | 4 -0.49511029 39.36702206 68 | 4 -0.49349265 39.36694853 69 | [CONTROLS] 70 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/NodeProperty/NodeProperty.ts: -------------------------------------------------------------------------------- 1 | enum NodeProperty { 2 | Elevation = 0, //!< Elevation 3 | BaseDemand = 1, //!< Primary demand baseline value 4 | Pattern = 2, //!< Primary demand time pattern index 5 | Emitter = 3, //!< Emitter flow coefficient 6 | Initqual = 4, //!< Initial quality 7 | SourceQual = 5, //!< Quality source strength 8 | SourcePat = 6, //!< Quality source pattern index 9 | SourceType = 7, //!< Quality source type (see @ref EN_SourceType) 10 | TankLevel = 8, //!< Current computed tank water level (read only) 11 | Demand = 9, //!< Current computed demand (read only) 12 | Head = 10, //!< Current computed hydraulic head (read only) 13 | Pressure = 11, //!< Current computed pressure (read only) 14 | Quality = 12, //!< Current computed quality (read only) 15 | SourceMass = 13, //!< Current computed quality source mass inflow (read only) 16 | InitVolume = 14, //!< Tank initial volume (read only) 17 | MixModel = 15, //!< Tank mixing model (see @ref EN_MixingModel) 18 | MixZoneVol = 16, //!< Tank mixing zone volume (read only) 19 | TankDiam = 17, //!< Tank diameter 20 | MinVolume = 18, //!< Tank minimum volume 21 | VolCurve = 19, //!< Tank volume curve index 22 | MinLevel = 20, //!< Tank minimum level 23 | MaxLevel = 21, //!< Tank maximum level 24 | MixFraction = 22, //!< Tank mixing fraction 25 | TankKBulk = 23, //!< Tank bulk decay coefficient 26 | TankVolume = 24, //!< Current computed tank volume (read only) 27 | MaxVolume = 25, //!< Tank maximum volume (read only) 28 | CanOverFlow = 26, //!< Tank can overflow (= 1) or not (= 0) 29 | DemandDeficit = 27, //!< Amount that full demand is reduced under PDA (read only) 30 | } 31 | 32 | export default NodeProperty; 33 | -------------------------------------------------------------------------------- /packages/epanet-js/src/enum/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ActionCodeType } from './ActionCodeType'; 2 | export { default as AnalysisStatistic } from './AnalysisStatistic'; 3 | export { default as ControlType } from './ControlType'; 4 | export { default as CountType } from './CountType'; 5 | export { default as CurveType } from './CurveType'; 6 | export { default as DemandModel } from './DemandModel'; 7 | export { default as FlowUnits } from './FlowUnits'; 8 | export { default as HeadLossType } from './HeadLossType'; 9 | export { default as InitHydOption } from './InitHydOption'; 10 | export { default as LinkProperty } from './LinkProperty'; 11 | export { default as LinkStatusType } from './LinkStatusType'; 12 | export { default as LinkType } from './LinkType'; 13 | export { default as MixingModel } from './MixingModel'; 14 | export { default as NodeProperty } from './NodeProperty'; 15 | export { default as NodeType } from './NodeType'; 16 | export { default as ObjectType } from './ObjectType'; 17 | export { default as Option } from './Option'; 18 | export { default as PumpStateType } from './PumpStateType'; 19 | export { default as PumpType } from './PumpType'; 20 | export { default as QualityType } from './QualityType'; 21 | export { default as RuleObject } from './RuleObject'; 22 | export { default as RuleOperator } from './RuleOperator'; 23 | export { default as RuleStatus } from './RuleStatus'; 24 | export { default as RuleVariable } from './RuleVariable'; 25 | export { default as SizeLimits } from './SizeLimits'; 26 | export { default as SourceType } from './SourceType'; 27 | export { default as StatisticType } from './StatisticType'; 28 | export { default as StatusReport } from './StatusReport'; 29 | export { default as TimeParameter } from './TimeParameter'; 30 | -------------------------------------------------------------------------------- /packages/epanet-js/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { EpanetModule } from "@model-create/epanet-engine"; 2 | 3 | // Define memory types more strictly 4 | export type EpanetMemoryType = "int" | "double" | "char" | "char-title"; 5 | 6 | // Mapping for JS return types 7 | export interface MemoryTypes { 8 | int: number; 9 | double: number; 10 | char: string; 11 | "char-title": string; 12 | } 13 | 14 | // Define input argument description 15 | export interface InputArgDef { 16 | // Type hint for potential validation or complex marshalling (optional) 17 | typeHint?: 18 | | "string" 19 | | "number" 20 | | "enum" 21 | | "boolean" 22 | | "double[]" 23 | | "length" 24 | | string; 25 | /** Set to true if this JS string argument needs conversion to a char* pointer */ 26 | isStringPtr?: boolean; 27 | /** For length parameters, specifies which array parameter this length corresponds to */ 28 | lengthFor?: string; 29 | } 30 | 31 | // Define output argument description 32 | export interface OutputArgDef { 33 | /** The name of the output property in the returned object */ 34 | name: string; 35 | /** The type of the output value */ 36 | type: EpanetMemoryType; 37 | } 38 | 39 | // Define the structure for API function metadata 40 | export interface ApiFunctionDefinition { 41 | /** The exact name exported by WASM (e.g., '_EN_getnodeindex') */ 42 | wasmFunctionName: keyof EpanetModule; // Allow string for flexibility 43 | 44 | /** Describes the INPUT arguments the JS function receives (excluding project handle) */ 45 | inputArgDefs: InputArgDef[]; 46 | 47 | /** List of output arguments with names and types */ 48 | outputArgDefs: OutputArgDef[]; 49 | 50 | /** Optional: Minimum EPANET version required */ 51 | minVersion?: number; 52 | 53 | /** Optional: Custom formatting for return values */ 54 | postProcess?: (results: any[]) => any; 55 | 56 | arrayInputs?: { 57 | [key: string]: { 58 | type: "double"; 59 | lengthParam: string; 60 | }; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /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-js/src/Workspace/Workspace.ts: -------------------------------------------------------------------------------- 1 | import EpanetEngine from "@model-create/epanet-engine"; 2 | 3 | export class Workspace { 4 | private _emscriptenModule: typeof EpanetEngine; 5 | private _instance: Awaited> | undefined; 6 | private _FS: Awaited>["FS"] | undefined; 7 | constructor() { 8 | this._emscriptenModule = EpanetEngine; 9 | } 10 | 11 | async loadModule(): Promise { 12 | const engine = await this._emscriptenModule(); 13 | this._instance = engine; 14 | this._FS = engine.FS; 15 | } 16 | 17 | private checkEngineLoaded(): void { 18 | if (!this._instance) { 19 | throw new Error("EPANET engine not loaded. Call loadModule() first."); 20 | } 21 | } 22 | 23 | get isLoaded(): boolean { 24 | if (!this._instance) { 25 | return false; 26 | } 27 | return true; 28 | } 29 | 30 | get instance(): NonNullable { 31 | this.checkEngineLoaded(); 32 | return this._instance!; 33 | } 34 | 35 | private get FS(): NonNullable { 36 | this.checkEngineLoaded(); 37 | return this._FS!; 38 | } 39 | 40 | get version() { 41 | const intPointer = this.instance._malloc(4); 42 | this.instance._EN_getversion(intPointer); 43 | const returnValue = this.instance.getValue(intPointer, "i32"); 44 | 45 | this.instance._free(intPointer); 46 | 47 | return returnValue; 48 | } 49 | 50 | getError(code: number) { 51 | const title1Ptr = this.instance._malloc(256); //EN_MAXMSG 52 | this.instance._EN_geterror(code, title1Ptr, 256); 53 | const errMessage = this.instance.UTF8ToString(title1Ptr); 54 | this.instance._free(title1Ptr); 55 | return errMessage; 56 | } 57 | 58 | writeFile(path: string, data: string | Uint8Array) { 59 | this.FS.writeFile(path, data); 60 | } 61 | 62 | readFile(file: string): string; 63 | readFile(file: string, encoding: "utf8"): string; 64 | readFile(file: string, encoding: "binary"): Uint8Array; 65 | readFile(file: any, encoding?: "utf8" | "binary"): any { 66 | if (!encoding || encoding === "utf8") { 67 | encoding = "utf8"; 68 | return this.FS.readFile(file, { 69 | encoding, 70 | }) as string; 71 | } 72 | return this.FS.readFile(file, { 73 | encoding, 74 | }) as Uint8Array; 75 | } 76 | } 77 | 78 | export default Workspace; 79 | -------------------------------------------------------------------------------- /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/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-js/test/data/net1_backup.inp: -------------------------------------------------------------------------------- 1 | [TITLE] 2 | 3 | [JUNCTIONS] 4 | N1 700.0000 5 | N2 600.0000 6 | 7 | [RESERVOIRS] 8 | 9 | [TANKS] 10 | 11 | [PIPES] 12 | L1 N1 N2 100.0000 50.0000 1.0000 1.0000 CV 13 | 14 | [PUMPS] 15 | 16 | [VALVES] 17 | 18 | [DEMANDS] 19 | N1 0.000000 20 | N2 0.000000 21 | 22 | [EMITTERS] 23 | 24 | [STATUS] 25 | 26 | [PATTERNS] 27 | 28 | [CURVES] 29 | 30 | [CONTROLS] 31 | 32 | [RULES] 33 | 34 | [QUALITY] 35 | 36 | [SOURCES] 37 | 38 | [MIXING] 39 | 40 | [REACTIONS] 41 | ORDER BULK 1.00 42 | ORDER WALL 1 43 | ORDER TANK 1.00 44 | GLOBAL BULK 0.000000 45 | GLOBAL WALL 0.000000 46 | 47 | [ENERGY] 48 | GLOBAL EFFIC 75.0000 49 | DEMAND CHARGE 0.0000 50 | 51 | [TIMES] 52 | DURATION 0:00:00 53 | HYDRAULIC TIMESTEP 1:00:00 54 | QUALITY TIMESTEP 0:06:00 55 | REPORT TIMESTEP 1:00:00 56 | REPORT START 0:00:00 57 | PATTERN TIMESTEP 1:00:00 58 | PATTERN START 0:00:00 59 | RULE TIMESTEP 0:06:00 60 | START CLOCKTIME 0:00:00 61 | STATISTIC NONE 62 | 63 | [OPTIONS] 64 | UNITS CFS 65 | PRESSURE PSI 66 | HEADLOSS H-W 67 | UNBALANCED STOP 68 | QUALITY NONE 69 | DEMAND MULTIPLIER 1.0000 70 | EMITTER EXPONENT 0.5000 71 | VISCOSITY 1.000000 72 | DIFFUSIVITY 1.000000 73 | SPECIFIC GRAVITY 1.000000 74 | TRIALS 200 75 | ACCURACY 0.00100000 76 | TOLERANCE 0.01000000 77 | CHECKFREQ 2 78 | MAXCHECK 10 79 | DAMPLIMIT 0.00000000 80 | 81 | [REPORT] 82 | PAGESIZE 0 83 | STATUS NO 84 | SUMMARY YES 85 | ENERGY NO 86 | MESSAGES YES 87 | NODES NONE 88 | LINKS NONE 89 | Elevation NO 90 | Demand PRECISION 2 91 | Head PRECISION 2 92 | Pressure PRECISION 2 93 | Quality PRECISION 2 94 | Length NO 95 | Diameter NO 96 | Flow PRECISION 2 97 | Velocity PRECISION 2 98 | Headloss PRECISION 2 99 | Quality NO 100 | State NO 101 | Setting NO 102 | Reaction NO 103 | 104 | [COORDINATES] 105 | 106 | [VERTICES] 107 | 108 | [END] 109 | -------------------------------------------------------------------------------- /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 | echo "Creating index.cjs from index.js with CommonJS export" 86 | sed -e '$ s/export default Module;/module.exports = Module;/' -e 's/import\.meta\.url/__filename/' dist/index.js > dist/index.cjs 87 | 88 | ) 89 | echo "=============================================" 90 | echo "Compiling wasm bindings done" 91 | echo "=============================================" -------------------------------------------------------------------------------- /packages/epanet-js/test/benchmark/runSim.js: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from '../../dist/index.mjs'; 2 | 3 | import { Project as ProjectJs, Workspace as WorkspaceJs } from "epanet-js"; 4 | import fs from 'fs'; 5 | 6 | async function benchmarkRunLongSim(iterations = 3) { 7 | const results = []; 8 | 9 | // Load the input file once 10 | const inpFileName = "./networks/sw-network1.inp"; 11 | const inpText = fs.readFileSync(inpFileName); 12 | 13 | for (let i = 1; i <= iterations; i++) { 14 | const ws = new Workspace(); 15 | await ws.loadModule(); 16 | const model = new Project(ws); 17 | 18 | // Write the input file to the workspace 19 | ws.writeFile("net1.inp", inpText); 20 | 21 | // Open the project 22 | model.open("net1.inp", "report.rpt", "out.bin"); 23 | 24 | // Run the simulation and measure time 25 | const startTime = performance.now(); 26 | model.solveH(); 27 | const endTime = performance.now(); 28 | 29 | const durationSeconds = (endTime - startTime) / 1000; 30 | results.push({ 31 | iteration: i, 32 | durationSeconds 33 | }); 34 | } 35 | 36 | const totalDuration = results.reduce((sum, r) => sum + r.durationSeconds, 0); 37 | const averageDuration = totalDuration / iterations; 38 | 39 | return { 40 | results, 41 | totalDuration, 42 | averageDuration, 43 | iterations 44 | }; 45 | } 46 | 47 | async function benchmarkRunLongSimEpanetJs(iterations = 3) { 48 | const results = []; 49 | 50 | // Load the input file once 51 | const inpFileName = "./tests/networks/sw-network1.inp"; 52 | const inpText = fs.readFileSync(inpFileName); 53 | 54 | for (let i = 1; i <= iterations; i++) { 55 | 56 | 57 | const ws = new Workspace(); 58 | const model = new Project(ws); 59 | 60 | ws.writeFile("net1.inp", inpText); 61 | 62 | model.open("net1.inp", "report.rpt", "out.bin"); 63 | 64 | const startTime = performance.now(); 65 | model.solveH(); 66 | const endTime = performance.now(); 67 | 68 | const durationSeconds = (endTime - startTime) / 1000; 69 | results.push({ 70 | iteration: i, 71 | durationSeconds 72 | }); 73 | } 74 | 75 | const totalDuration = results.reduce((sum, r) => sum + r.durationSeconds, 0); 76 | const averageDuration = totalDuration / iterations; 77 | 78 | return { 79 | results, 80 | totalDuration, 81 | averageDuration, 82 | iterations 83 | }; 84 | } 85 | 86 | // Run the benchmark and log results 87 | benchmarkRunLongSim(3).then(results => { 88 | console.log('EPANET-js Long Simulation Benchmark Results:'); 89 | console.log('------------------------------------------'); 90 | console.log(`Number of iterations: ${results.iterations}`); 91 | console.log(`Total duration: ${results.totalDuration.toFixed(3)} seconds`); 92 | console.log(`Average duration: ${results.averageDuration.toFixed(3)} seconds`); 93 | console.log('\nIndividual iteration results:'); 94 | results.results.forEach(r => { 95 | console.log(` Iteration ${r.iteration}: ${r.durationSeconds.toFixed(3)} seconds`); 96 | }); 97 | }).catch(error => { 98 | console.error('Benchmark failed:', error); 99 | }); 100 | 101 | 102 | benchmarkRunLongSimEpanetJs(3).then(results => { 103 | console.log('Epanet-js 0.7.0 Long Simulation Benchmark Results:'); 104 | console.log('------------------------------------------'); 105 | console.log(`Number of iterations: ${results.iterations}`); 106 | console.log(`Total duration: ${results.totalDuration.toFixed(3)} seconds`); 107 | console.log(`Average duration: ${results.averageDuration.toFixed(3)} seconds`); 108 | }); -------------------------------------------------------------------------------- /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-js/test/version-guard.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, vi } from "vitest"; 2 | import Project from "../src/Project/Project"; 3 | import { Workspace } from "../src/Workspace/Workspace"; 4 | 5 | // Helper to create a Mock Workspace with a specific EPANET version 6 | class MockWorkspace extends Workspace { 7 | constructor(epanetVersion: number) { 8 | super(); 9 | const mockInstance = { 10 | _malloc: vi.fn((size) => (size > 0 ? 1000 : 0)), 11 | _free: vi.fn(), 12 | getValue: vi.fn((ptr, type) => (type === "i32" ? epanetVersion : 0)), 13 | lengthBytesUTF8: vi.fn((ptr) => 10), 14 | UTF8ToString: vi.fn((ptr) => "mock string"), 15 | stringToUTF8: vi.fn((str, len, ptr) => { 16 | if (ptr) { 17 | const buffer = new Uint8Array(ptr); 18 | for (let i = 0; i < len; i++) { 19 | buffer[i] = str.charCodeAt(i); 20 | } 21 | } 22 | return len; 23 | }), 24 | HEAP8: new Int8Array(), 25 | FS: { 26 | writeFile: vi.fn(), 27 | readFile: vi.fn(), 28 | }, 29 | _EN_getversion: vi.fn((ptr) => 0), 30 | _EN_getnodeindex: vi.fn(() => 0), 31 | _EN_getspecialnodeprop_v23: vi.fn((idx, ptr) => 0), 32 | _EN_createproject: vi.fn((idx, ptr) => 0), 33 | } as any; 34 | Object.defineProperty(this, "instance", { get: () => mockInstance }); 35 | } 36 | 37 | async loadModule(): Promise { 38 | // No-op for testing 39 | } 40 | } 41 | 42 | // --- Test Suite --- 43 | describe.skip("EPANET Version Guarding", () => { 44 | // Use version integers as defined in Project.ts / apiDefinitions 45 | const baselineVersion = 20200; // e.g., 2.2.0 46 | const nextVersion = 20300; // e.g., 2.3.0 47 | const oldVersion = 20100; // e.g., 2.1.0 48 | 49 | it("should initialize successfully with required baseline version", () => { 50 | const workspace = new MockWorkspace(baselineVersion); 51 | expect(() => new Project(workspace)).not.toThrow(); 52 | }); 53 | 54 | it("should initialize successfully with a newer version", () => { 55 | const workspace = new MockWorkspace(nextVersion); 56 | expect(() => new Project(workspace)).not.toThrow(); 57 | }); 58 | 59 | it("should throw error if version is below absolute minimum", () => { 60 | const workspace = new MockWorkspace(oldVersion); 61 | expect(() => new Project(workspace)).toThrow(/EPANET Version Too Low/); 62 | }); 63 | 64 | it("should allow calling baseline functions with baseline version", () => { 65 | const workspace = new MockWorkspace(baselineVersion); 66 | const project = new Project(workspace); 67 | expect(() => project.getNodeIndex("N1")).not.toThrow(); 68 | }); 69 | 70 | it("should allow calling baseline functions with newer version", () => { 71 | const workspace = new MockWorkspace(nextVersion); 72 | const project = new Project(workspace); 73 | expect(() => project.getNodeIndex("N1")).not.toThrow(); 74 | }); 75 | 76 | it.skip("should THROW when calling version-specific function with baseline version", () => { 77 | const workspace = new MockWorkspace(baselineVersion); 78 | const project = new Project(workspace); 79 | const v23MethodName = "openX"; 80 | expect(() => 81 | project[v23MethodName]("net.inp", "net.rpt", "net.bin"), 82 | ).toThrow(/Method 'openX' requires EPANET v2\.3\.0.*loaded is v2\.2\.0/); 83 | }); 84 | 85 | it.skip("should ALLOW calling version-specific function with required version", () => { 86 | const workspace = new MockWorkspace(nextVersion); 87 | const project = new Project(workspace); 88 | const v23MethodName = "openX"; 89 | expect(() => (project as any)[v23MethodName](1)).not.toThrow(); 90 | }); 91 | 92 | it.skip("should throw if underlying WASM function is missing", () => { 93 | const workspace = new MockWorkspace(nextVersion); 94 | const mockInstance = workspace.instance; 95 | delete (mockInstance as any)._EN_getspecialnodeprop_v23; 96 | const project = new Project(workspace); 97 | const v23MethodName = "openX"; 98 | expect(() => (project as any)[v23MethodName](1)).toThrow( 99 | /EPANET function '_EN_openX' \(for method 'openX'\) not found/, 100 | ); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /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-js/test/Project/WaterQualityAnalysisFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | import { InitHydOption, CountType, NodeProperty } from "../../src/enum"; 3 | 4 | import fs from "fs"; 5 | 6 | const net1 = fs.readFileSync(__dirname + "/../data/net1.inp", "utf8"); 7 | 8 | describe("Epanet Water Quality Functions", () => { 9 | let ws: Workspace; 10 | let model: Project; 11 | 12 | beforeEach(async () => { 13 | ws = new Workspace(); 14 | await ws.loadModule(); 15 | model = new Project(ws); 16 | ws.writeFile("net1.inp", net1); 17 | model.open("net1.inp", "report.rpt", "out.bin"); 18 | model.solveH(); // Hydraulic analysis is required before water quality 19 | }); 20 | 21 | describe("Complete Water Quality Analysis", () => { 22 | test("solveQ performs complete water quality analysis", () => { 23 | model.solveQ(); 24 | 25 | const tankIndex = model.getNodeIndex("2"); 26 | const tankWQResuls = model.getNodeValue(tankIndex, NodeProperty.Quality); 27 | expect(tankWQResuls).toBeGreaterThan(0); 28 | 29 | // Verify the analysis completed by checking the binary output file 30 | const bin = ws.readFile("out.bin", "binary"); 31 | const epanetMagicNumber = new DataView(bin.buffer).getInt32(0, true); 32 | expect(epanetMagicNumber).toEqual(516114521); 33 | }); 34 | }); 35 | 36 | describe("Step-by-Step Water Quality Analysis", () => { 37 | test("nextQ advances through time periods", () => { 38 | model.openQ(); 39 | model.initQ(InitHydOption.Save); 40 | 41 | let tStep = Infinity; 42 | let timeStepCount = 0; 43 | 44 | do { 45 | model.runQ(); 46 | tStep = model.nextQ(); 47 | timeStepCount++; 48 | } while (tStep > 0); 49 | 50 | model.closeQ(); 51 | 52 | // Verify we completed the analysis 53 | expect(tStep).toEqual(0); 54 | expect(timeStepCount).toBeGreaterThan(1); 55 | }); 56 | 57 | test("stepQ advances simulation by one time step", () => { 58 | model.openQ(); 59 | model.initQ(InitHydOption.Save); 60 | 61 | let tStep = Infinity; 62 | let timeStepCount = 0; 63 | 64 | do { 65 | model.runQ(); 66 | tStep = model.stepQ(); 67 | timeStepCount++; 68 | } while (tStep > 0); 69 | 70 | model.closeQ(); 71 | 72 | // Verify we completed the analysis 73 | expect(tStep).toEqual(0); 74 | expect(timeStepCount).toBeGreaterThan(1); 75 | }); 76 | }); 77 | 78 | describe("Reusing Hydraulic Results", () => { 79 | test("can reuse hydraulic results from file", () => { 80 | // Save hydraulic results 81 | model.saveHydFile("hydFile.out"); 82 | model.close(); 83 | 84 | // Create new model and reuse hydraulic results 85 | const model2 = new Project(ws); 86 | model2.open("net1.inp", "net1-2.rpt", "out-2.bin"); 87 | model2.useHydFile("hydFile.out"); 88 | 89 | // Run water quality analysis 90 | model2.openQ(); 91 | model2.initQ(InitHydOption.Save); 92 | 93 | let tStep = Infinity; 94 | do { 95 | model2.runQ(); 96 | tStep = model2.stepQ(); 97 | } while (tStep > 0); 98 | 99 | model2.closeQ(); 100 | 101 | // Verify the analysis completed 102 | const bin = ws.readFile("out-2.bin", "binary"); 103 | const epanetMagicNumber = new DataView(bin.buffer).getInt32(0, true); 104 | expect(epanetMagicNumber).toEqual(516114521); 105 | }); 106 | }); 107 | 108 | describe("Water Quality Initialization Options", () => { 109 | test("initQ with Save option preserves hydraulic results", () => { 110 | model.openQ(); 111 | model.initQ(InitHydOption.Save); 112 | 113 | // Run a few steps 114 | model.runQ(); 115 | model.nextQ(); 116 | model.runQ(); 117 | 118 | // Verify we can still access hydraulic results 119 | const nodeCount = model.getCount(CountType.NodeCount); 120 | expect(nodeCount).toBeGreaterThan(0); 121 | 122 | model.closeQ(); 123 | }); 124 | 125 | test("initQ with NoSave option does not preserve hydraulic results", () => { 126 | model.openQ(); 127 | model.initQ(InitHydOption.NoSave); 128 | 129 | // Run a few steps 130 | model.runQ(); 131 | model.nextQ(); 132 | model.runQ(); 133 | 134 | // Verify we can still access hydraulic results 135 | const nodeCount = model.getCount(CountType.NodeCount); 136 | expect(nodeCount).toBeGreaterThan(0); 137 | 138 | model.closeQ(); 139 | }); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /packages/epanet-js/test/Project/ProjectFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | import { CountType, NodeType, ObjectType } from "../../src/enum"; 3 | import fs from "fs"; 4 | 5 | const net1 = fs.readFileSync(__dirname + "/../data/net1.inp", "utf8"); 6 | const netGenerated = fs.readFileSync( 7 | __dirname + "/../data/net1_backup.inp", 8 | "utf8", 9 | ); 10 | 11 | // TODO: Sharing the workspace between tests is causing memory issues and flaky tests. 12 | 13 | describe("Project Functions", async () => { 14 | let model: Project; 15 | let ws: Workspace; 16 | 17 | beforeEach(async () => { 18 | ws = new Workspace(); 19 | await ws.loadModule(); 20 | model = new Project(ws); 21 | }); 22 | 23 | afterEach(() => { 24 | model.close(); 25 | }); 26 | 27 | describe("Project Initialization", () => { 28 | test("should initialize a new project", () => { 29 | model.init("report.rpt", "out.bin", 0, 0); 30 | const { line1, line2, line3 } = model.getTitle(); 31 | expect(line1).toEqual(""); 32 | expect(line2).toEqual(""); 33 | expect(line3).toEqual(""); 34 | }); 35 | 36 | test("should throw error with bad properties during initialization", () => { 37 | function catchError() { 38 | model.init("repor{/st.rpt", "ou{/t.bin", 0, 0); 39 | } 40 | expect(catchError).toThrow("303"); 41 | }); 42 | }); 43 | 44 | describe("Project Opening and Closing", () => { 45 | test("should open an existing project", () => { 46 | ws.writeFile("net1.inp", net1); 47 | model.open("net1.inp", "report.rpt", "out.bin"); 48 | const nodeCount = model.getCount(CountType.NodeCount); 49 | expect(nodeCount).toEqual(11); 50 | }); 51 | 52 | test("should throw error when accessing closed project", () => { 53 | ws.writeFile("net1.inp", net1); 54 | model.open("net1.inp", "report.rpt", "out.bin"); 55 | model.close(); 56 | 57 | function catchError() { 58 | model.getCount(CountType.NodeCount); 59 | } 60 | expect(catchError).toThrow("Error 102: no network data available"); 61 | }); 62 | }); 63 | 64 | describe("Project Title Operations", () => { 65 | test("should set and get project title", () => { 66 | model.init("report.rpt", "out.bin", 0, 0); 67 | model.setTitle("Title 1", "Title Line 2", ""); 68 | const { line1, line2, line3 } = model.getTitle(); 69 | 70 | expect(line1).toEqual("Title 1"); 71 | expect(line2).toEqual("Title Line 2"); 72 | expect(line3).toEqual(""); 73 | }); 74 | }); 75 | 76 | describe("Project Count Operations", () => { 77 | test("should get correct counts for an existing project", () => { 78 | ws.writeFile("net1.inp", net1); 79 | model.open("net1.inp", "report.rpt", "out.bin"); 80 | const nodeCount = model.getCount(CountType.NodeCount); 81 | const linkCount = model.getCount(CountType.LinkCount); 82 | 83 | expect(nodeCount).toEqual(11); 84 | expect(linkCount).toEqual(13); 85 | }); 86 | }); 87 | 88 | describe("Project File Operations", () => { 89 | test("should save project to INP file", () => { 90 | ws.writeFile("net1.inp", net1); 91 | model.init("report.rpt", "out.bin", 0, 0); 92 | 93 | //Create Network 94 | const node1Id = model.addNode("N1", NodeType.Junction); 95 | model.setJunctionData(node1Id, 700, 0, ""); 96 | const node2Id = model.addNode("N2", NodeType.Junction); 97 | model.setJunctionData(node2Id, 600, 0, ""); 98 | const linkId = model.addLink("L1", 0, "N1", "N2"); 99 | model.setPipeData(linkId, 100, 50, 1, 1); 100 | 101 | model.saveInpFile("net1_backup.inp"); 102 | const duplicateFile = ws.readFile("net1_backup.inp"); 103 | 104 | expect(duplicateFile).toEqual(netGenerated); 105 | }); 106 | 107 | test("should run an existing project and generate output files", () => { 108 | ws.writeFile("net1.inp", net1); 109 | model.runProject("net1.inp", "report.rpt", "out.bin"); 110 | 111 | const rpt = ws.readFile("report.rpt"); 112 | const bin = ws.readFile("out.bin", "binary"); 113 | const epanetMagicNumber = new DataView(bin.buffer).getInt32(0, true); 114 | 115 | expect(rpt.length).toBeGreaterThan(0); 116 | expect(epanetMagicNumber).toEqual(516114521); 117 | }); 118 | }); 119 | 120 | describe("Project Comment Operations", () => { 121 | test("should set and get comments for objects", () => { 122 | ws.writeFile("net1.inp", net1); 123 | model.open("net1.inp", "report.rpt", "out.bin"); 124 | 125 | model.setComment(ObjectType.Node, 1, "Comment 1"); 126 | const comment = model.getComment(ObjectType.Node, 1); 127 | expect(comment).toEqual("Comment 1"); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /packages/epanet-js/test/benchmark/index.js: -------------------------------------------------------------------------------- 1 | import { Project, Workspace, NodeType, FlowUnits, HeadLossType } from '../../dist/index.mjs'; 2 | 3 | // Benchmark function that returns performance metrics 4 | async function benchmarkProjectMethods(iterations = 100000) { 5 | const ws = new Workspace(); 6 | await ws.loadModule(); 7 | const model = new Project(ws); 8 | 9 | // Initialize project 10 | model.init("report.rpt", "out.bin", FlowUnits.CFS, HeadLossType.HW); 11 | 12 | // Add a node for testing 13 | const nodeIndex = model.addNode("J1", NodeType.Junction); 14 | 15 | // Benchmark getNodeIndex 16 | const startTimeGetNodeIndex = performance.now(); 17 | for (let i = 0; i < iterations; i++) { 18 | model.getNodeIndex("J1"); 19 | } 20 | const endTimeGetNodeIndex = performance.now(); 21 | const getNodeIndexDuration = (endTimeGetNodeIndex - startTimeGetNodeIndex) / 1000; 22 | const getNodeIndexPerSecond = iterations / getNodeIndexDuration; 23 | 24 | // Benchmark getNodeValue 25 | const startTimeGetNodeValue = performance.now(); 26 | for (let i = 0; i < iterations; i++) { 27 | model.getNodeValue(nodeIndex, 0); // 0 is elevation property 28 | } 29 | const endTimeGetNodeValue = performance.now(); 30 | const getNodeValueDuration = (endTimeGetNodeValue - startTimeGetNodeValue) / 1000; 31 | const getNodeValuePerSecond = iterations / getNodeValueDuration; 32 | 33 | // Benchmark setNodeValue 34 | const startTimeSetNodeValue = performance.now(); 35 | for (let i = 0; i < iterations; i++) { 36 | model.setNodeValue(nodeIndex, 0, 100); // Set elevation to 100 37 | } 38 | const endTimeSetNodeValue = performance.now(); 39 | const setNodeValueDuration = (endTimeSetNodeValue - startTimeSetNodeValue) / 1000; 40 | const setNodeValuePerSecond = iterations / setNodeValueDuration; 41 | 42 | // Add a link for testing 43 | const linkIndex = model.addLink("P1", 0, "J1", "J1"); // 0 is pipe type 44 | 45 | // Benchmark getLinkValue 46 | const startTimeGetLinkValue = performance.now(); 47 | for (let i = 0; i < iterations; i++) { 48 | model.getLinkValue(linkIndex, 0); // 0 is length property 49 | } 50 | const endTimeGetLinkValue = performance.now(); 51 | const getLinkValueDuration = (endTimeGetLinkValue - startTimeGetLinkValue) / 1000; 52 | const getLinkValuePerSecond = iterations / getLinkValueDuration; 53 | 54 | // Benchmark setLinkValue 55 | const startTimeSetLinkValue = performance.now(); 56 | for (let i = 0; i < iterations; i++) { 57 | model.setLinkValue(linkIndex, 0, 1000); // Set length to 1000 58 | } 59 | const endTimeSetLinkValue = performance.now(); 60 | const setLinkValueDuration = (endTimeSetLinkValue - startTimeSetLinkValue) / 1000; 61 | const setLinkValuePerSecond = iterations / setLinkValueDuration; 62 | 63 | return { 64 | getNodeIndex: { 65 | duration: getNodeIndexDuration, 66 | callsPerSecond: getNodeIndexPerSecond, 67 | millionCallsPerSecond: getNodeIndexPerSecond / 1000000 68 | }, 69 | getNodeValue: { 70 | duration: getNodeValueDuration, 71 | callsPerSecond: getNodeValuePerSecond, 72 | millionCallsPerSecond: getNodeValuePerSecond / 1000000 73 | }, 74 | setNodeValue: { 75 | duration: setNodeValueDuration, 76 | callsPerSecond: setNodeValuePerSecond, 77 | millionCallsPerSecond: setNodeValuePerSecond / 1000000 78 | }, 79 | getLinkValue: { 80 | duration: getLinkValueDuration, 81 | callsPerSecond: getLinkValuePerSecond, 82 | millionCallsPerSecond: getLinkValuePerSecond / 1000000 83 | }, 84 | setLinkValue: { 85 | duration: setLinkValueDuration, 86 | callsPerSecond: setLinkValuePerSecond, 87 | millionCallsPerSecond: setLinkValuePerSecond / 1000000 88 | }, 89 | iterations 90 | }; 91 | } 92 | 93 | // Run the benchmark and log results 94 | benchmarkProjectMethods(5_000_000).then(results => { 95 | console.log('EPANET-js Method Performance Benchmark Results:'); 96 | console.log('--------------------------------------------'); 97 | console.log(`Iterations per test: ${results.iterations}`); 98 | console.log('\nMethod Performance:'); 99 | console.log('------------------'); 100 | 101 | for (const [method, metrics] of Object.entries(results)) { 102 | if (method !== 'iterations') { 103 | console.log(`\n${method}:`); 104 | console.log(` Duration: ${metrics.duration.toFixed(3)} seconds`); 105 | console.log(` Calls per second: ${metrics.callsPerSecond.toFixed(0)}`); 106 | console.log(` Million calls per second: ${metrics.millionCallsPerSecond.toFixed(3)}`); 107 | } 108 | } 109 | }).catch(error => { 110 | console.error('Benchmark failed:', error); 111 | }); 112 | -------------------------------------------------------------------------------- /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-js/test/Project/DataCurveFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | import { CurveType } from "../../src/enum"; 3 | 4 | import fs from "fs"; 5 | 6 | const net1 = fs.readFileSync(__dirname + "/../data/tankTest.inp", "utf8"); 7 | const ws = new Workspace(); 8 | await ws.loadModule(); 9 | 10 | describe("Data Curve Functions", () => { 11 | let model: Project; 12 | 13 | beforeEach(() => { 14 | model = new Project(ws); 15 | model.init("report.rpt", "out.bin", 0, 0); 16 | }); 17 | 18 | afterEach(() => { 19 | model.close(); 20 | }); 21 | 22 | describe("Basic Curve Operations", () => { 23 | test("should add and get curve", () => { 24 | // Add a new curve (added with a single pint of 1.0,1.0) 25 | model.addCurve("PUMP1"); 26 | const curveIndex = model.getCurveIndex("PUMP1"); 27 | expect(curveIndex).toBe(1); 28 | 29 | // Get curve by index 30 | const curve = model.getCurve(curveIndex); 31 | expect(curve.id).toBe("PUMP1"); 32 | expect(curve.x).toEqual([1]); 33 | expect(curve.y).toEqual([1]); 34 | 35 | // Get curve ID 36 | expect(model.getCurveId(curveIndex)).toBe("PUMP1"); 37 | 38 | // Get curve length 39 | expect(model.getCurveLenth(curveIndex)).toBe(1); 40 | 41 | // Get curve type (default should be GenericCurve) 42 | expect(model.getCurveType(curveIndex)).toBe(CurveType.GenericCurve); 43 | }); 44 | 45 | test("should set curve values", () => { 46 | // Add a new curve 47 | model.addCurve("PUMP1"); 48 | const curveIndex = model.getCurveIndex("PUMP1"); 49 | 50 | // Set curve values using setCurve 51 | const xValues = [0, 1000, 2000]; 52 | const yValues = [100, 80, 50]; 53 | model.setCurve(curveIndex, xValues, yValues); 54 | 55 | // Verify curve values 56 | const curve = model.getCurve(curveIndex); 57 | expect(curve.x).toEqual(xValues); 58 | expect(curve.y).toEqual(yValues); 59 | expect(model.getCurveLenth(curveIndex)).toBe(3); 60 | 61 | // Verify individual points 62 | const point1 = model.getCurveValue(curveIndex, 1); 63 | expect(point1.x).toBe(0); 64 | expect(point1.y).toBe(100); 65 | 66 | const point2 = model.getCurveValue(curveIndex, 2); 67 | expect(point2.x).toBe(1000); 68 | expect(point2.y).toBe(80); 69 | }); 70 | 71 | test("should throw error when x and y arrays have different lengths", () => { 72 | // Add a new curve 73 | model.addCurve("PUMP1"); 74 | const curveIndex = model.getCurveIndex("PUMP1"); 75 | 76 | // Attempt to set curve with mismatched array lengths 77 | const xValues = [0, 1000, 2000]; 78 | const yValues = [100, 80]; // Different length than xValues 79 | 80 | expect(() => { 81 | model.setCurve(curveIndex, xValues, yValues); 82 | }).toThrow( 83 | "All array arguments must have the same length. First array length: 3, mismatched arrays: 2", 84 | ); 85 | }); 86 | 87 | test("should modify curve values", () => { 88 | // Add a new curve 89 | model.addCurve("PUMP1"); 90 | const curveIndex = model.getCurveIndex("PUMP1"); 91 | 92 | // Set initial curve values 93 | model.setCurve(curveIndex, [0, 1000], [100, 80]); 94 | 95 | // Modify individual point 96 | model.setCurveValue(curveIndex, 2, 1200, 75); 97 | 98 | // Verify modified values 99 | const point = model.getCurveValue(curveIndex, 2); 100 | expect(point.x).toBe(1200); 101 | expect(point.y).toBe(75); 102 | 103 | // Modify curve ID 104 | model.setCurveId(curveIndex, "PUMP1_MODIFIED"); 105 | expect(model.getCurveId(curveIndex)).toBe("PUMP1_MODIFIED"); 106 | }); 107 | 108 | test("should handle multiple curves", () => { 109 | // Add first curve 110 | model.addCurve("PUMP1"); 111 | const curve1Index = model.getCurveIndex("PUMP1"); 112 | model.setCurve(curve1Index, [0, 1000], [100, 80]); 113 | 114 | // Add second curve 115 | model.addCurve("PUMP2"); 116 | let curve2Index = model.getCurveIndex("PUMP2"); 117 | model.setCurve(curve2Index, [0, 500, 1000], [120, 100, 60]); 118 | 119 | // Verify both curves exist and have correct data 120 | expect(model.getCurve(curve1Index).x).toEqual([0, 1000]); 121 | expect(model.getCurve(curve2Index).x).toEqual([0, 500, 1000]); 122 | 123 | // Delete first curve 124 | model.deleteCurve(curve1Index); 125 | 126 | // Verify second curve still exists and first curve is gone 127 | expect(() => (curve2Index = model.getCurveIndex("PUMP1"))).toThrow(); 128 | curve2Index = model.getCurveIndex("PUMP2"); 129 | expect(model.getCurve(curve2Index).id).toBe("PUMP2"); 130 | }); 131 | }); 132 | 133 | describe("Real-world Data Tests", () => { 134 | test("should read curve from INP file", () => { 135 | ws.writeFile("net1.inp", net1); 136 | const model = new Project(ws); 137 | model.open("net1.inp", "net1.rpt", "out.bin"); 138 | 139 | const curve = model.getCurve(1); 140 | 141 | expect(curve.id).toEqual("CURVE_ID"); 142 | expect(curve.x).toEqual([2, 5]); 143 | expect(curve.y).toEqual([5, 5]); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /packages/epanet-js/test/Project/NodalDemandFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | import { NodeType, DemandModel, FlowUnits, HeadLossType } from "../../src/enum"; 3 | import { readFileSync } from "fs"; 4 | import { join } from "path"; 5 | 6 | const net1 = readFileSync(join(__dirname, "../data/net1.inp"), "utf8"); 7 | 8 | const ws = new Workspace(); 9 | await ws.loadModule(); 10 | 11 | describe("Nodal Demand Functions", () => { 12 | let model: Project; 13 | 14 | beforeEach(() => { 15 | model = new Project(ws); 16 | }); 17 | 18 | afterEach(() => { 19 | model.close(); 20 | }); 21 | 22 | describe("Basic Demand Operations", () => { 23 | test("should add and delete demands", () => { 24 | model.init("report.rpt", "out.bin", FlowUnits.LPS, HeadLossType.HW); 25 | 26 | // Create node 27 | const nodeIndex = model.addNode("J1", NodeType.Junction); 28 | model.setJunctionData(nodeIndex, 700, 0, ""); 29 | 30 | // Create demand pattern 31 | model.addPattern("Hospital"); 32 | model.addPattern("Commercial"); 33 | // Add first demand 34 | model.addDemand(nodeIndex, 100, "Hospital", "LargeUser1"); 35 | expect(model.getNumberOfDemands(nodeIndex)).toEqual(2); 36 | 37 | // Add second demand 38 | model.addDemand(nodeIndex, 50, "Commercial", "LargeUser2"); 39 | expect(model.getNumberOfDemands(nodeIndex)).toEqual(3); 40 | 41 | // Verify demand properties 42 | const demand1Index = model.getDemandIndex(nodeIndex, "LargeUser1"); 43 | const demand2Index = model.getDemandIndex(nodeIndex, "LargeUser2"); 44 | 45 | expect(model.getBaseDemand(nodeIndex, demand1Index)).toEqual(100); 46 | expect(model.getBaseDemand(nodeIndex, demand2Index)).toEqual(50); 47 | 48 | // Delete first demand 49 | model.deleteDemand(nodeIndex, demand1Index); 50 | expect(model.getNumberOfDemands(nodeIndex)).toEqual(2); 51 | }); 52 | 53 | test("should set and get demand properties", () => { 54 | model.init("report.rpt", "out.bin", FlowUnits.LPS, HeadLossType.HW); 55 | 56 | model.addPattern("Residential"); 57 | model.addPattern("Residentialv2"); 58 | model.getPatternIndex("Residential"); 59 | const patternIndex2 = model.getPatternIndex("Residentialv2"); 60 | 61 | // Create node and demand 62 | const nodeIndex = model.addNode("J1", NodeType.Junction); 63 | model.setJunctionData(nodeIndex, 700, 0, ""); 64 | model.addDemand(nodeIndex, 100, "Residential", "Residential"); 65 | 66 | // Get demand index 67 | const demandIndex = model.getDemandIndex(nodeIndex, "Residential"); 68 | 69 | // Set and verify base demand 70 | model.setBaseDemand(nodeIndex, demandIndex, 150); 71 | expect(model.getBaseDemand(nodeIndex, demandIndex)).toEqual(150); 72 | 73 | // Set and verify demand name 74 | model.setDemandName(nodeIndex, demandIndex, "Updated"); 75 | expect(model.getDemandName(nodeIndex, demandIndex)).toEqual("Updated"); 76 | 77 | // Set and verify demand pattern 78 | model.setDemandPattern(nodeIndex, demandIndex, patternIndex2); 79 | expect(model.getDemandPattern(nodeIndex, demandIndex)).toEqual(2); 80 | }); 81 | }); 82 | 83 | describe("Demand Model Operations", () => { 84 | test("should set and get demand model", () => { 85 | model.init("report.rpt", "out.bin", FlowUnits.LPS, HeadLossType.HW); 86 | 87 | // Set demand model 88 | model.setDemandModel(DemandModel.PDA, 20, 25, 0.5); 89 | 90 | // Get and verify demand model 91 | const modelData = model.getDemandModel(); 92 | expect(modelData.type).toEqual(DemandModel.PDA); 93 | expect(modelData.pmin).toEqual(20); 94 | expect(modelData.preq).toEqual(25); 95 | expect(modelData.pexp).toEqual(0.5); 96 | }); 97 | 98 | test("should handle multiple demand models", () => { 99 | model.init("report.rpt", "out.bin", FlowUnits.LPS, HeadLossType.HW); 100 | 101 | // Test DDA model 102 | model.setDemandModel(DemandModel.DDA, 0, 0, 0); 103 | let modelData = model.getDemandModel(); 104 | expect(modelData.type).toEqual(DemandModel.DDA); 105 | 106 | // Test PDA model 107 | model.setDemandModel(DemandModel.PDA, 20, 30, 0.5); 108 | modelData = model.getDemandModel(); 109 | expect(modelData.type).toEqual(DemandModel.PDA); 110 | }); 111 | }); 112 | 113 | describe("Real-world Data Tests", () => { 114 | test("should handle demands from net1.inp", () => { 115 | ws.writeFile("net1.inp", net1); 116 | model.open("net1.inp", "report.rpt", "out.bin"); 117 | 118 | // Test junction with demand 119 | const junctionIndex = model.getNodeIndex("11"); 120 | expect(model.getNumberOfDemands(junctionIndex)).toEqual(3); 121 | 122 | // Test junction without demand (based demand only) 123 | const junctionIndex2 = model.getNodeIndex("10"); 124 | expect(model.getNumberOfDemands(junctionIndex2)).toEqual(1); 125 | }); 126 | 127 | test("should handle demand patterns from net1.inp", () => { 128 | ws.writeFile("net1.inp", net1); 129 | model.open("net1.inp", "report.rpt", "out.bin"); 130 | 131 | // Test junction with pattern 132 | const junctionIndex = model.getNodeIndex("11"); 133 | const demandIndex = 1; // First demand 134 | const patternIndex = model.getDemandPattern(junctionIndex, demandIndex); 135 | expect(patternIndex).toEqual(1); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /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-js/test/Project/AnalysisOptionsFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | import { 3 | FlowUnits, 4 | NodeType, 5 | Option, 6 | QualityType, 7 | TimeParameter, 8 | } from "../../src/enum"; 9 | 10 | const ws = new Workspace(); 11 | await ws.loadModule(); 12 | 13 | describe("Analysis Options Functions", () => { 14 | let model: Project; 15 | 16 | beforeEach(() => { 17 | model = new Project(ws); 18 | model.init("report.rpt", "out.bin", 0, 0); 19 | }); 20 | 21 | afterEach(() => { 22 | model.close(); 23 | }); 24 | 25 | describe("Flow Units", () => { 26 | test("should get and set flow units", () => { 27 | // Test default flow units 28 | expect(model.getFlowUnits()).toBe(FlowUnits.CFS); 29 | 30 | // Test setting different flow units 31 | model.setFlowUnits(FlowUnits.GPM); 32 | expect(model.getFlowUnits()).toBe(FlowUnits.GPM); 33 | 34 | model.setFlowUnits(FlowUnits.LPS); 35 | expect(model.getFlowUnits()).toBe(FlowUnits.LPS); 36 | }); 37 | }); 38 | 39 | describe("Options", () => { 40 | test("should get and set analysis options", () => { 41 | // Test default values 42 | expect(model.getOption(Option.Trials)).toBe(200); 43 | expect(model.getOption(Option.Accuracy)).toBe(0.001); 44 | expect(model.getOption(Option.Tolerance)).toBe(0.01); 45 | 46 | // Test setting options 47 | model.setOption(Option.Trials, 100); 48 | expect(model.getOption(Option.Trials)).toBe(100); 49 | 50 | model.setOption(Option.Accuracy, 0.0001); 51 | expect(model.getOption(Option.Accuracy)).toBe(0.0001); 52 | 53 | model.setOption(Option.Tolerance, 0.005); 54 | expect(model.getOption(Option.Tolerance)).toBe(0.005); 55 | }); 56 | }); 57 | 58 | describe("Quality Info and Type", () => { 59 | test("should get and set quality info and type", () => { 60 | // Test default quality info 61 | const defaultQualityInfo = model.getQualityInfo(); 62 | expect(defaultQualityInfo.qualType).toBe(QualityType.None); 63 | expect(defaultQualityInfo.chemName).toBe(""); 64 | expect(defaultQualityInfo.chemUnits).toBe(""); 65 | expect(defaultQualityInfo.traceNode).toBe(0); 66 | 67 | // Test setting chemical quality 68 | model.setQualityType(QualityType.Chem, "Chlorine", "mg/L", ""); 69 | const chemQualityInfo = model.getQualityInfo(); 70 | expect(chemQualityInfo.qualType).toBe(QualityType.Chem); 71 | expect(chemQualityInfo.chemName).toBe("Chlorine"); 72 | expect(chemQualityInfo.chemUnits).toBe("mg/L"); 73 | 74 | // Test setting age quality 75 | model.setQualityType(QualityType.Age, "", "", ""); 76 | const ageQualityInfo = model.getQualityInfo(); 77 | expect(ageQualityInfo.qualType).toBe(QualityType.Age); 78 | 79 | // Create junction J-1 for trace quality test 80 | model.addNode("J-1", NodeType.Junction); 81 | 82 | // Test setting trace quality 83 | model.setQualityType(QualityType.Trace, "", "", "J-1"); 84 | const traceQualityInfo = model.getQualityInfo(); 85 | expect(traceQualityInfo.qualType).toBe(QualityType.Trace); 86 | }); 87 | 88 | test("should get quality type", () => { 89 | // Test default quality type 90 | const defaultQualityType = model.getQualityType(); 91 | expect(defaultQualityType.qualType).toBe(QualityType.None); 92 | expect(defaultQualityType.traceNode).toBe(0); 93 | 94 | // Create junction J-1 for trace quality test 95 | model.addNode("J-1", NodeType.Junction); 96 | 97 | // Test setting and getting chemical quality type 98 | model.setQualityType(QualityType.Chem, "Chlorine", "mg/L", ""); 99 | const chemQualityType = model.getQualityType(); 100 | expect(chemQualityType.qualType).toBe(QualityType.Chem); 101 | expect(chemQualityType.traceNode).toBe(0); 102 | 103 | // Test setting and getting trace quality type 104 | model.setQualityType(QualityType.Trace, "", "", "J-1"); 105 | const traceQualityType = model.getQualityType(); 106 | expect(traceQualityType.qualType).toBe(QualityType.Trace); 107 | expect(traceQualityType.traceNode).toBe(1); // Node index should be 1 since it's the first node added 108 | }); 109 | }); 110 | 111 | describe("Time Parameters", () => { 112 | test("should get and set time parameters", () => { 113 | // Test default values 114 | expect(model.getTimeParameter(TimeParameter.Duration)).toBe(0); 115 | expect(model.getTimeParameter(TimeParameter.HydStep)).toBe(3600); // 1 hour in seconds 116 | expect(model.getTimeParameter(TimeParameter.QualStep)).toBe(360); // 6 minutes in seconds 117 | expect(model.getTimeParameter(TimeParameter.ReportStep)).toBe(3600); // 1 hour in seconds 118 | 119 | // Test setting time parameters 120 | model.setTimeParameter(TimeParameter.Duration, 86400); // 24 hours 121 | expect(model.getTimeParameter(TimeParameter.Duration)).toBe(86400); 122 | 123 | model.setTimeParameter(TimeParameter.HydStep, 1800); // 30 minutes 124 | expect(model.getTimeParameter(TimeParameter.HydStep)).toBe(1800); 125 | 126 | model.setTimeParameter(TimeParameter.QualStep, 600); // 10 minutes 127 | expect(model.getTimeParameter(TimeParameter.QualStep)).toBe(600); 128 | 129 | model.setTimeParameter(TimeParameter.ReportStep, 7200); // 2 hours 130 | expect(model.getTimeParameter(TimeParameter.ReportStep)).toBe(7200); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /packages/epanet-js/test/Project/TimePatternFunctions.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | 3 | describe("Epanet Time Pattern Functions", () => { 4 | let ws: Workspace; 5 | let model: Project; 6 | 7 | beforeEach(async () => { 8 | ws = new Workspace(); 9 | await ws.loadModule(); 10 | model = new Project(ws); 11 | model.init("report.rpt", "out.bin", 0, 0); 12 | }); 13 | 14 | describe("Pattern Management", () => { 15 | test("add and get pattern index", () => { 16 | model.addPattern("testPattern"); 17 | const patIndex = model.getPatternIndex("testPattern"); 18 | expect(patIndex).toBeGreaterThan(0); 19 | }); 20 | 21 | test("get pattern id", () => { 22 | model.addPattern("testPattern"); 23 | const patIndex = model.getPatternIndex("testPattern"); 24 | const patId = model.getPatternId(patIndex); 25 | expect(patId).toEqual("testPattern"); 26 | }); 27 | 28 | test("set pattern id", () => { 29 | model.addPattern("testPattern"); 30 | const patIndex = model.getPatternIndex("testPattern"); 31 | model.setPatternId(patIndex, "newPatternId"); 32 | const newPatId = model.getPatternId(patIndex); 33 | expect(newPatId).toEqual("newPatternId"); 34 | }); 35 | 36 | test("delete pattern", () => { 37 | model.addPattern("testPattern"); 38 | const patIndex = model.getPatternIndex("testPattern"); 39 | model.deletePattern(patIndex); 40 | 41 | // Verify pattern is deleted by checking if we can get its ID 42 | expect(() => model.getPatternId(patIndex)).toThrow(); 43 | }); 44 | }); 45 | 46 | describe("Pattern Values", () => { 47 | test("set and get pattern value", () => { 48 | model.addPattern("testPattern"); 49 | const patIndex = model.getPatternIndex("testPattern"); 50 | 51 | model.setPattern(patIndex, [99, 98, 97, 96, 95]); 52 | expect(model.getPatternValue(patIndex, 1)).toEqual(99); 53 | expect(model.getPatternValue(patIndex, 2)).toEqual(98); 54 | expect(model.getPatternValue(patIndex, 3)).toEqual(97); 55 | expect(model.getPatternValue(patIndex, 4)).toEqual(96); 56 | expect(model.getPatternValue(patIndex, 5)).toEqual(95); 57 | 58 | // Set individual values 59 | model.setPatternValue(patIndex, 1, 2.0); 60 | model.setPatternValue(patIndex, 2, 3.0); 61 | model.setPatternValue(patIndex, 3, 4.0); 62 | 63 | // Get values back 64 | expect(model.getPatternValue(patIndex, 1)).toEqual(2.0); 65 | expect(model.getPatternValue(patIndex, 2)).toEqual(3.0); 66 | expect(model.getPatternValue(patIndex, 3)).toEqual(4.0); 67 | }); 68 | 69 | test("set pattern with array", () => { 70 | model.addPattern("testPattern"); 71 | const patIndex = model.getPatternIndex("testPattern"); 72 | 73 | const values = [2.0, 3.0, 4.0, 5.0, 6.0]; 74 | model.setPattern(patIndex, values); 75 | 76 | // Verify pattern length 77 | const patLength = model.getPatternLength(patIndex); 78 | expect(patLength).toEqual(values.length); 79 | 80 | // Verify all values 81 | values.forEach((value, index) => { 82 | expect(model.getPatternValue(patIndex, index + 1)).toEqual(value); 83 | }); 84 | }); 85 | 86 | test("get average pattern value", () => { 87 | model.addPattern("testPattern"); 88 | const patIndex = model.getPatternIndex("testPattern"); 89 | 90 | const values = [2.0, 3.0, 4.0, 5.0, 6.0]; 91 | model.setPattern(patIndex, values); 92 | 93 | const average = model.getAveragePatternValue(patIndex); 94 | const expectedAverage = values.reduce((a, b) => a + b) / values.length; 95 | expect(average).toEqual(expectedAverage); 96 | }); 97 | 98 | test("pattern value bounds", () => { 99 | model.addPattern("testPattern"); 100 | const patIndex = model.getPatternIndex("testPattern"); 101 | 102 | // Test setting value at period 0 (should throw) 103 | expect(() => model.setPatternValue(patIndex, 0, 1.0)).toThrow(); 104 | 105 | // Test getting value at period 0 (should throw) 106 | expect(() => model.getPatternValue(patIndex, 0)).toThrow(); 107 | 108 | // Test setting value beyond pattern length 109 | model.setPattern(patIndex, [1.0, 2.0]); 110 | expect(() => model.setPatternValue(patIndex, 3, 1.0)).toThrow(); 111 | expect(() => model.getPatternValue(patIndex, 3)).toThrow(); 112 | }); 113 | }); 114 | 115 | describe("Multiple Patterns", () => { 116 | test("manage multiple patterns", () => { 117 | // Add multiple patterns 118 | model.addPattern("pattern1"); 119 | model.addPattern("pattern2"); 120 | model.addPattern("pattern3"); 121 | 122 | const pat1Index = model.getPatternIndex("pattern1"); 123 | const pat2Index = model.getPatternIndex("pattern2"); 124 | let pat3Index = model.getPatternIndex("pattern3"); 125 | 126 | // Set different values for each pattern 127 | model.setPattern(pat1Index, [1.0, 2.0]); 128 | model.setPattern(pat2Index, [3.0, 4.0]); 129 | model.setPattern(pat3Index, [5.0, 6.0]); 130 | 131 | // Verify each pattern maintains its own values 132 | expect(model.getPatternValue(pat1Index, 1)).toEqual(1.0); 133 | expect(model.getPatternValue(pat2Index, 1)).toEqual(3.0); 134 | expect(model.getPatternValue(pat3Index, 1)).toEqual(5.0); 135 | 136 | // Delete middle pattern 137 | model.deletePattern(pat2Index); 138 | 139 | // Get the index of the remaining pattern 140 | pat3Index = model.getPatternIndex("pattern3"); 141 | 142 | // Verify other patterns are unaffected 143 | expect(model.getPatternValue(pat1Index, 1)).toEqual(1.0); 144 | expect(model.getPatternValue(pat3Index, 1)).toEqual(5.0); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /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-js/test/Project/SimpleControlFunctons.test.ts: -------------------------------------------------------------------------------- 1 | import { Project, Workspace } from "../../src"; 2 | import { ControlType, NodeProperty, LinkProperty } from "../../src/enum"; 3 | 4 | import fs from "fs"; 5 | 6 | const net1 = fs.readFileSync(__dirname + "/../data/net1.inp", "utf8"); 7 | 8 | describe("Epanet Simple Control Functions", () => { 9 | let ws: Workspace; 10 | let model: Project; 11 | 12 | beforeEach(async () => { 13 | ws = new Workspace(); 14 | await ws.loadModule(); 15 | model = new Project(ws); 16 | ws.writeFile("net1.inp", net1); 17 | model.open("net1.inp", "report.rpt", "out.bin"); 18 | }); 19 | 20 | describe("Control Management", () => { 21 | test("add control for tank level", () => { 22 | // Get indices for the tank and link 23 | const tankIndex = model.getNodeIndex("2"); 24 | const linkIndex = model.getLinkIndex("9"); 25 | 26 | // Add a control to open the link when tank level is below 110 27 | const controlIndex = model.addControl( 28 | ControlType.LowLevel, 29 | linkIndex, 30 | 1.0, // Open setting 31 | tankIndex, 32 | 110.0, // Level 33 | ); 34 | 35 | expect(controlIndex).toBeGreaterThan(0); 36 | 37 | // Verify the control was added correctly 38 | const control = model.getControl(controlIndex); 39 | expect(control.type).toEqual(ControlType.LowLevel); 40 | expect(control.linkIndex).toEqual(linkIndex); 41 | expect(control.setting).toEqual(1.0); 42 | expect(control.nodeIndex).toEqual(tankIndex); 43 | expect(control.level).toEqual(110.0); 44 | }); 45 | 46 | test("add control for tank level with high level condition", () => { 47 | // Get indices for the tank and link 48 | const tankIndex = model.getNodeIndex("2"); 49 | const linkIndex = model.getLinkIndex("9"); 50 | 51 | // Add a control to close the link when tank level is above 140 52 | const controlIndex = model.addControl( 53 | ControlType.HiLevel, 54 | linkIndex, 55 | 0.0, // Closed setting 56 | tankIndex, 57 | 140.0, // Level 58 | ); 59 | 60 | expect(controlIndex).toBeGreaterThan(0); 61 | 62 | // Verify the control was added correctly 63 | const control = model.getControl(controlIndex); 64 | expect(control.type).toEqual(ControlType.HiLevel); 65 | expect(control.linkIndex).toEqual(linkIndex); 66 | expect(control.setting).toEqual(0.0); 67 | expect(control.nodeIndex).toEqual(tankIndex); 68 | expect(control.level).toEqual(140.0); 69 | }); 70 | 71 | test("delete control", () => { 72 | // Get indices for the tank and link 73 | const tankIndex = model.getNodeIndex("2"); 74 | const linkIndex = model.getLinkIndex("9"); 75 | 76 | // Add a control 77 | const controlIndex = model.addControl( 78 | ControlType.LowLevel, 79 | linkIndex, 80 | 1.0, 81 | tankIndex, 82 | 110.0, 83 | ); 84 | 85 | // Delete the control 86 | model.deleteControl(controlIndex); 87 | 88 | // Verify the control was deleted 89 | expect(() => model.getControl(controlIndex)).toThrow(); 90 | }); 91 | 92 | test("modify existing control", () => { 93 | // Get indices for the tank and link 94 | const tankIndex = model.getNodeIndex("2"); 95 | const linkIndex = model.getLinkIndex("9"); 96 | 97 | // Add a control 98 | const controlIndex = model.addControl( 99 | ControlType.LowLevel, 100 | linkIndex, 101 | 1.0, 102 | tankIndex, 103 | 110.0, 104 | ); 105 | 106 | // Modify the control 107 | model.setControl( 108 | controlIndex, 109 | ControlType.HiLevel, 110 | linkIndex, 111 | 0.0, 112 | tankIndex, 113 | 140.0, 114 | ); 115 | 116 | // Verify the control was modified 117 | const control = model.getControl(controlIndex); 118 | expect(control.type).toEqual(ControlType.HiLevel); 119 | expect(control.setting).toEqual(0.0); 120 | expect(control.level).toEqual(140.0); 121 | }); 122 | }); 123 | 124 | describe("Control Behavior", () => { 125 | test("control affects simulation results step by step", () => { 126 | // Get indices for the tank and link 127 | const tankIndex = model.getNodeIndex("2"); 128 | const linkIndex = model.getLinkIndex("9"); 129 | 130 | // Add controls for tank level 131 | model.addControl(ControlType.LowLevel, linkIndex, 1.0, tankIndex, 110.0); 132 | model.addControl(ControlType.HiLevel, linkIndex, 0.0, tankIndex, 120.0); 133 | 134 | // Initialize hydraulic analysis 135 | model.openH(); 136 | model.initH(0); 137 | 138 | // Run step by step 139 | let tStep = Infinity; 140 | let stepCount = 0; 141 | const maxSteps = 100; // Prevent infinite loop 142 | const tankLevels: number[] = []; 143 | const linkStatuses: number[] = []; 144 | 145 | do { 146 | // Run hydraulic analysis for current time step 147 | model.runH(); 148 | tStep = model.nextH(); 149 | stepCount++; 150 | 151 | const psiPerFeet = 0.4333; 152 | 153 | // Get current tank level and link status 154 | const tankLevel = model.getNodeValue(tankIndex, NodeProperty.Pressure); 155 | const linkStatus = model.getLinkValue(linkIndex, LinkProperty.Status); 156 | 157 | // Store results for verification 158 | tankLevels.push(tankLevel / psiPerFeet); 159 | linkStatuses.push(linkStatus); 160 | 161 | // Verify tank level stays within control bounds 162 | expect(tankLevel / psiPerFeet).toBeGreaterThanOrEqual(109.9); 163 | expect(tankLevel / psiPerFeet).toBeLessThanOrEqual(120.1); 164 | } while (tStep > 0 && stepCount < maxSteps); 165 | 166 | // Close hydraulic analysis 167 | model.closeH(); 168 | 169 | // Verify we completed the simulation 170 | expect(tStep).toEqual(0); 171 | expect(stepCount).toBeGreaterThan(1); 172 | 173 | // Verify we saw some variation in tank levels 174 | const minLevel = Math.min(...tankLevels); 175 | const maxLevel = Math.max(...tankLevels); 176 | expect(maxLevel - minLevel).toBeGreaterThan(0); 177 | 178 | // Verify we saw both open and closed states for the link 179 | const uniqueStatuses = new Set(linkStatuses); 180 | expect(uniqueStatuses.size).toBeGreaterThan(1); 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /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 `