This plan details exact EPANET code changes to support SWMM 5.2-style Named Variables and Arithmetic Expressions, defined and used entirely within the [RULES] section. No new public APIs will be added; the engine will parse and use them internally at run time. Validate each step against the checklists at the bottom.
References: see RESEARCH_SWMM.md for SWMM implementation specifics.
- New
[RULES]lines (order-agnostic, prior to first use):VARIABLE name = OBJECT id ATTRIBUTEVARIABLE name = SYSTEM ATTRIBUTEEXPRESSION name = <math expression over named variables>
- Clause extensions:
IF/AND/OR <variableName> <relop> <value|variable|OBJECT id ATTRIBUTE>IF/AND/OR <expressionName> <relop> <value|variable|OBJECT id ATTRIBUTE>
- Supported functions/operators in expressions: same set as SWMM (
abs,sgn,step, trig incl. hyperbolic and inverses,log,log10,exp, operators+ - * / ^and parentheses). - Allowed OBJECT/ATTRIBUTE in variables match EPANET’s rule variables (
NODEorLINKandSYSTEMwithTIME/CLOCKTIME, etc.).
epanet/src/text.h
- Add keywords:
#define w_VARIABLE "VARIABLE"#define w_EXPRESSION "EXPRESSION"
epanet/src/rules.c
- Extend Ruleword enum and table to recognize new tokens:
- enum: add
r_VARIABLE,r_EXPRESSIONbeforer_ERROR - array: insert
w_VARIABLE,w_EXPRESSION
- enum: add
- Storage and counts (Rules wrapper; see types.h below) will be available via
pr->rules. - Counting pass: modify
addrule(Parser*, char*)to increment counts:- if
match(tok, w_RULE):parser->MaxRules++ - else if
match(tok, w_VARIABLE):pr->rules.VarCount++ - else if
match(tok, w_EXPRESSION):pr->rules.ExprCount++
- if
- Allocation: in
allocrules(Project*), after allocatingnet->Rule, allocatepr->rules.NamedVarsandpr->rules.ExpressionsusingVarCountandExprCount(setCurrentVar = CurrentExpr = -1). - Freeing: in
freerules(Project*), free compiled expressions and arrays (see types.h changes) in addition to existing rule cleanup. - Parsing (
ruledata(Project*)): add cases forr_VARIABLEandr_EXPRESSIONthat operate regardless ofRuleState(but beforeRULEbody begins is recommended):r_VARIABLE: parse formatsVARIABLE name = SYSTEM <Varword>VARIABLE name = <OBJECT> <id> <Varword>- Reject if
nameequals any Varword or conflicts with reserved keywords. - Resolve to object/index/variable triple with existing helpers:
findmatch(Object,...),findnode/findlink,findmatch(Varword,...). - Store in
NamedVars[++CurrentVar] = {name, object, index, variable}.
r_EXPRESSION: assemble expression string fromTok[3..Ntokens-1]; compile viamathexpr_create(exprString, getVariableIndex)and store inExpressions[++CurrentExpr] = {name, expression}; on failure set rule parser error (Errcode=201/202mapped to new math expr error code if desired) andRuleState=r_ERROR.
- Premise parsing (
newpremise(Project*, int)): expand to accept LHS as expression or named variable:- Before current object parsing, check if
Tok[1]matches an expression name; if yes, setp->exprIndex = exprIdxand skip object parsing; parse relop atTok[2], then parse RHS as value or variable (named or object triple). Setp->rhsIsVar=1and populate RHS triple if variable, else assignp->value. - Else check if
Tok[1]matches a named variable; if yes, setp->object/index/variablefrom that alias; then continue current relop and RHS logic; if RHS is a variable (named or object triple), set RHS fields; else setp->value. - Else fall back to existing formats (SYSTEM or explicit OBJECT id VAR).
- Validation: when both sides are variables and LHS is not an expression, if different variable kinds (e.g.,
STATUSvsFLOW), return 201 or optionally log a warning (consistent with SWMM’s WARN11 semantics).
- Before current object parsing, check if
- Evaluation:
checkpremise(Project*, Spremise*)remains dispatcher, but whenp->exprIndex >= 0handle incheckvaluebranch.checkvalue(Project*, Spremise*)modifications:- Compute LHS value:
- If
exprIndex >= 0, evaluate viamathexpr_eval(Expressions[exprIndex].expression, getNamedVariableValue). - Else compute as today based on
p->object/index/variable(existing switch).
- If
- Compute RHS value:
- If
p->rhsIsVar, compute value using the same logic as for LHS (object/index/variable triple), or if RHS references a named variable, resolve it pre-parsed to object/index/variable. - Else use
p->value.
- If
- Compare using existing tolerance logic on numeric values; time/clocktime stays in
checktime().
- Compute LHS value:
epanet/src/types.h
- Extend
Spremiseto support expressions and RHS variables:- Add fields (initialized to defaults where constructed):
int exprIndex; // -1 if not an expression LHSint rhsIsVar; // 1 if RHS is a variableint rhsObject; // r_NODE/r_LINK/r_SYSTEMint rhsIndex; // object indexint rhsVariable; // Varwords enum
- Add fields (initialized to defaults where constructed):
- Extend
Ruleswrapper to store named variables and expressions:- Counts and cursors:
int VarCount, ExprCount, CurrentVar, CurrentExpr; - Name length: define
#define MAXVARNAME 32inrules.cor a shared header and use here accordingly. - Named variables array:
typedef struct { char name[MAXVARNAME+1]; int object; int index; int variable; } NamedVar;NamedVar *NamedVars;
- Expressions array:
- Forward-declare
typedef struct ExprNode MathExpr;(frommathexpr.h) typedef struct { char name[MAXVARNAME+1]; MathExpr* expr; } NamedExpr;NamedExpr *Expressions;
- Forward-declare
- Counts and cursors:
epanet/src/input2.c
- First pass counting already routes
[RULES]tokens toaddrule(parser, tok). With the changes above,addrulewill handleVARIABLEandEXPRESSIONcounts alongsideRULE.
epanet/src/project.c(only if needed)
- Ensure lifecycle calls to
initrules,allocrules, andfreeruleshappen as they are today (they do). No public API changes.
- New Module:
epanet/src/mathexpr.candepanet/src/mathexpr.h
- Add SWMM’s
mathexpr.c/h(unmodified API):MathExpr* mathexpr_create(char*, int (*getVar)(char*))double mathexpr_eval(MathExpr*, double (*getVal)(int))void mathexpr_delete(MathExpr*)
- In
rules.c, implement two callbacks:static int getVariableIndex(char* name)that scanspr->rules.NamedVarsfor a case-insensitive name match and returns index or -1.static double getNamedVariableValue(int idx)that convertsNamedVars[idx]to a current numeric value using the same logic ascheckvalue(reuse helper to compute a (object,index,variable) value with unit conversion and tolerances).
- VARIABLE lines:
- Tokens:
[0]=VARIABLE,[1]=name,[2]==, then either[3]=SYSTEM [4]=Varor[3]=OBJECT [4]=id [5]=Var. - Reject if fewer than 5 tokens or bad keyword/operator.
- Name cannot match any
Varwordor reserved rule keywords.
- Tokens:
- EXPRESSION lines:
- Tokens:
[0]=EXPRESSION,[1]=name,[2]==,[3..]concatenated with single spaces to form the formula string.
- Tokens:
- Premises with names/expressions:
- Expression LHS:
[1]=ExprName,[2]=relop, RHS begins at[3]. - NamedVar LHS:
[1]=VarName,[2]=<relop>, RHS at[3]. - For RHS variable, accept either another named var or
OBJECT id Vartriple; else parse number/status as today. For LINK/NODE status/setting, keep existing status parsing when user usesIS OPEN/CLOSED/ACTIVE.
- Expression LHS:
- Time/clocktime premises remain evaluated by
checktime(). - Numeric premises use
checkvalue(); when an expression is used, units are those of each variable’s native outputs in EPANET, combined algebraically. It’s the user’s responsibility to maintain consistent units. - Status comparisons remain via
checkstatus(). - When comparing variable-vs-variable, use the same 0.001 tolerance; if the compared variables differ in dimension (e.g.,
FLOWvsPRESSURE), mark rule parse error or simply return false and log a warning (align to SWMM’s WARN11 approach—EPANET can log a parser message and continue).
writerule()currently writes only the IF/THEN/ELSE/Priority clauses. If needed later, prepend the set ofVARIABLEandEXPRESSIONlines used by any rule to the[RULES]section, preserving order of definition.
- Use existing rule parser error codes:
201unrecognized/misplaced token or bad attribute202bad number/time literal203bad node id;204bad link id- For math expression compile error, map to
201or introduce a dedicated code if desirable.
- Name conflicts: if
VARIABLE namematches an existing Varword or rule keyword, treat as201.
- No toolkit API changes. Engine parses and evaluates named variables and expressions only from
[RULES]in input files and inEN_addrule()text ingestion (which routes text intoruledata()already).
- Unit tests (parser):
- VARIABLE lines (SYSTEM TIME/CLOCKTIME; NODE/LINK variables)
- EXPRESSION lines (simple arithmetic; trig/log; nested parentheses; power)
- Premises using expression vs value; named var vs value; named var vs named var; named var vs object triple
- Invalid names, missing tokens, bad function names, bad object/variable names
- Integration tests:
- Small networks with
[RULES]using variables and expressions to toggle link status/setting - Check evaluation cadence is unaffected (uses existing
Rulestep) - Regression on existing rule models to ensure backward compatibility
- Small networks with
-
Phase A — Named Variables
- Add
w_VARIABLE, Ruleword enum/table entries - Count in
addruleand allocate arrays inallocrules - Extend
Spremisewith RHS variable fields (can be added now for both phases) - Parse
VARIABLElines intoNamedVars - Resolve named vars in
newpremiseLHS/RHS - Compute named var values in evaluation path
- Unit/integration tests green
- Add
-
Phase B — Arithmetic Expressions
- Add
w_EXPRESSION, Ruleword enum/table entries - Count in
addruleand allocateExpressions - Add
exprIndextoSpremise; parse expression LHS - Integrate
mathexpr.c/hand implement callbacks - Evaluate expressions via
mathexpr_evalincheckvalue - Variable-vs-variable comparisons in RHS supported
- Tests for functions/operators and mixed cases green
- Add
-
Phase C — Polishing
- Free compiled expressions in
freerules - Optional: writer enhancements for VARIABLE/EXPRESSION
- Backward compat verified on existing test suite
- Free compiled expressions in
Notes:
- Keep differences from SWMM minimal for maintainability; we mirror its architecture (named variable aliasing, expression parser callbacks) while aligning with EPANET’s rule and evaluation code.
- Storage is attached to
Rulesto localize lifetime and avoid public API exposure.