Files
EPANET/src/quality.c

695 lines
19 KiB
C

/*
******************************************************************************
Project: OWA EPANET
Version: 2.2
Module: quality.c
Description: implements EPANET's water quality engine
Authors: see AUTHORS
Copyright: see AUTHORS
License: see LICENSE
Last Updated: 02/03/2020
******************************************************************************
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "mempool.h"
#include "types.h"
#include "funcs.h"
// Stagnant flow tolerance
const double Q_STAGNANT = 0.005 / GPMperCFS; // 0.005 gpm = 1.114e-5 cfs
// Exported functions
double findsourcequal(Project *, int, double, long);
// Imported functions
extern char setreactflag(Project *);
extern double getucf(double);
extern void ratecoeffs(Project *);
extern void initsegs(Project *);
extern void reversesegs(Project *, int);
extern int sortnodes(Project *);
extern void transport(Project *, long);
// Local functions
static double sourcequal(Project *, Psource);
static void evalmassbalance(Project *);
static double findstoredmass(Project *);
static int flowdirchanged(Project *);
int openqual(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: returns error code
** Purpose: opens water quality solver
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int errcode = 0;
int n;
// Return if no quality analysis requested
if (qual->Qualflag == NONE) return errcode;
// Build nodal adjacency lists if they don't already exist
if (net->Adjlist == NULL)
{
// Check for too few nodes & no fixed grade nodes
if (net->Nnodes < 2) return 223;
if (net->Ntanks == 0) return 224;
// Build adjacency lists
errcode = buildadjlists(net);
if (errcode ) return errcode;
// Check for unconnected nodes
if (errcode = unlinked(pr)) return errcode;
}
// Create a memory pool for water quality segments
qual->OutOfMemory = FALSE;
qual->SegPool = mempool_create();
if (qual->SegPool == NULL) errcode = 101;
// Allocate arrays for link flow direction & reaction rates
n = net->Nlinks + 1;
qual->FlowDir = (FlowDirection *)calloc(n, sizeof(FlowDirection));
qual->PipeRateCoeff = (double *)calloc(n, sizeof(double));
// Allocate arrays used for volume segments in links & tanks
n = net->Nlinks + net->Ntanks + 1;
qual->FirstSeg = (Pseg *)calloc(n, sizeof(Pseg));
qual->LastSeg = (Pseg *)calloc(n, sizeof(Pseg));
// Allocate memory for topologically sorted nodes
qual->SortedNodes = (int *)calloc(n, sizeof(int));
ERRCODE(MEMCHECK(qual->FlowDir));
ERRCODE(MEMCHECK(qual->PipeRateCoeff));
ERRCODE(MEMCHECK(qual->FirstSeg));
ERRCODE(MEMCHECK(qual->LastSeg));
ERRCODE(MEMCHECK(qual->SortedNodes));
return errcode;
}
int initqual(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: none
** Purpose: re-initializes water quality solver
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Hydraul *hyd = &pr->hydraul;
Quality *qual = &pr->quality;
Times *time = &pr->times;
int i;
int errcode = 0;
// Re-position hydraulics file
if (!hyd->OpenHflag)
{
fseek(pr->outfile.HydFile, pr->outfile.HydOffset, SEEK_SET);
}
// Set elapsed times to zero
time->Qtime = 0;
time->Htime = 0;
time->Rtime = time->Rstart;
pr->report.Nperiods = 0;
// Initialize node quality
for (i = 1; i <= net->Nnodes; i++)
{
if (qual->Qualflag == TRACE) qual->NodeQual[i] = 0.0;
else qual->NodeQual[i] = net->Node[i].C0;
if (net->Node[i].S != NULL) net->Node[i].S->Smass = 0.0;
}
if (qual->Qualflag == NONE) return errcode;
// Initialize tank quality
for (i = 1; i <= net->Ntanks; i++)
{
net->Tank[i].C = qual->NodeQual[net->Tank[i].Node];
}
// Initialize quality at trace node (if applicable)
if (qual->Qualflag == TRACE) qual->NodeQual[qual->TraceNode] = 100.0;
// Compute Schmidt number
if (qual->Diffus > 0.0) qual->Sc = hyd->Viscos / qual->Diffus;
else qual->Sc = 0.0;
// Compute unit conversion factor for bulk react. coeff.
qual->Bucf = getucf(qual->BulkOrder);
qual->Tucf = getucf(qual->TankOrder);
// Check if modeling a reactive substance
qual->Reactflag = setreactflag(pr);
// Reset memory pool used for pipe & tank segments
qual->FreeSeg = NULL;
mempool_reset(qual->SegPool);
// Create initial set of pipe & tank segments
initsegs(pr);
// Initialize link flow direction indicator
for (i = 1; i <= net->Nlinks; i++) qual->FlowDir[i] = ZERO_FLOW;
// Initialize avg. reaction rates
qual->Wbulk = 0.0;
qual->Wwall = 0.0;
qual->Wtank = 0.0;
qual->Wsource = 0.0;
// Initialize mass balance components
qual->MassBalance.initial = findstoredmass(pr);
qual->MassBalance.inflow = 0.0;
qual->MassBalance.outflow = 0.0;
qual->MassBalance.reacted = 0.0;
qual->MassBalance.final = 0.0;
qual->MassBalance.ratio = 0.0;
return errcode;
}
int runqual(Project *pr, long *t)
/*
**--------------------------------------------------------------
** Input: none
** Output: t = current simulation time (sec)
** Returns: error code
** Purpose: retrieves hydraulics for next hydraulic time step
** (at time t) and saves current results to file
**--------------------------------------------------------------
*/
{
Hydraul *hyd = &pr->hydraul;
Quality *qual = &pr->quality;
Times *time = &pr->times;
long hydtime = 0; // Hydraulic solution time
long hydstep = 0; // Hydraulic time step
int errcode = 0;
// Update reported simulation time
*t = time->Qtime;
// Read hydraulic solution from hydraulics file
if (time->Qtime == time->Htime)
{
// Read hydraulic results from file
if (!hyd->OpenHflag)
{
if (!readhyd(pr, &hydtime)) return 307;
if (!readhydstep(pr, &hydstep)) return 307;
time->Htime = hydtime;
}
// Save current results to output file
if (time->Htime >= time->Rtime)
{
if (pr->outfile.Saveflag)
{
errcode = saveoutput(pr);
pr->report.Nperiods++;
}
time->Rtime += time->Rstep;
}
if (errcode) return errcode;
// If simulating water quality
if (qual->Qualflag != NONE && time->Qtime < time->Dur)
{
// ... compute reaction rate coeffs.
if (qual->Reactflag && qual->Qualflag != AGE) ratecoeffs(pr);
// ... topologically sort network nodes if flow directions change
if (flowdirchanged(pr) == TRUE)
{
errcode = sortnodes(pr);
}
}
if (!hyd->OpenHflag) time->Htime = hydtime + hydstep;
}
return errcode;
}
int nextqual(Project *pr, long *tstep)
/*
**--------------------------------------------------------------
** Input: none
** Output: tstep = time step (sec) over which quality was updated
** Returns: error code
** Purpose: updates water quality in network until next hydraulic
** event occurs (after tstep secs.)
**--------------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
Times *time = &pr->times;
long hydstep; // Time step until next hydraulic event
long dt, qtime;
int errcode = 0;
// Find time step till next hydraulic event
*tstep = 0;
hydstep = 0;
if (time->Htime <= time->Dur) hydstep = time->Htime - time->Qtime;
// Perform water quality routing over this time step
if (qual->Qualflag != NONE && hydstep > 0)
{
// Repeat over each quality time step until tstep is reached
qtime = 0;
while (!qual->OutOfMemory && qtime < hydstep)
{
dt = MIN(time->Qstep, hydstep - qtime);
qtime += dt;
transport(pr, dt);
}
if (qual->OutOfMemory) errcode = 101;
}
// Update mass balance ratio
evalmassbalance(pr);
// Update current time
if (!errcode) *tstep = hydstep;
time->Qtime += hydstep;
// If no more time steps remain
if (!errcode && *tstep == 0)
{
// ... report overall mass balance
if (qual->Qualflag != NONE && pr->report.Statflag)
{
writemassbalance(pr);
}
// ... write the final portion of the binary output file
if (pr->outfile.Saveflag) errcode = savefinaloutput(pr);
}
return errcode;
}
int stepqual(Project *pr, long *tleft)
/*
**--------------------------------------------------------------
** Input: none
** Output: tleft = time left in simulation
** Returns: error code
** Purpose: updates quality conditions over a single
** quality time step
**--------------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
Times *time = &pr->times;
long dt, hstep, t, tstep;
int errcode = 0;
tstep = time->Qstep;
do
{
// Set local time step to quality time step
dt = tstep;
// Find time step until next hydraulic event
hstep = time->Htime - time->Qtime;
// If next hydraulic event occurs before end of local time step
if (hstep < dt)
{
// ... adjust local time step to next hydraulic event
dt = hstep;
// ... transport quality over local time step
if (qual->Qualflag != NONE) transport(pr, dt);
time->Qtime += dt;
// ... quit if running quality concurrently with hydraulics
if (pr->hydraul.OpenHflag) break;
// ... otherwise call runqual() to update hydraulics
errcode = runqual(pr, &t);
time->Qtime = t;
}
// Otherwise transport quality over current local time step
else
{
if (qual->Qualflag != NONE) transport(pr, dt);
time->Qtime += dt;
}
// Reduce quality time step by local time step
tstep -= dt;
if (qual->OutOfMemory) errcode = 101;
} while (!errcode && tstep > 0);
// Update mass balance ratio
evalmassbalance(pr);
// Update total simulation time left
*tleft = time->Dur - time->Qtime;
// If no more time steps remain
if (!errcode && *tleft == 0)
{
// ... report overall mass balance
if (qual->Qualflag != NONE && pr->report.Statflag)
{
writemassbalance(pr);
}
// ... write the final portion of the binary output file
if (pr->outfile.Saveflag) errcode = savefinaloutput(pr);
}
return errcode;
}
int closequal(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: returns error code
** Purpose: closes water quality solver
**--------------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
int errcode = 0;
if (qual->Qualflag != NONE)
{
if (qual->SegPool) mempool_delete(qual->SegPool);
FREE(qual->FirstSeg);
FREE(qual->LastSeg);
FREE(qual->PipeRateCoeff);
FREE(qual->FlowDir);
FREE(qual->SortedNodes);
}
freeadjlists(&pr->network);
return errcode;
}
double avgqual(Project *pr, int k)
/*
**--------------------------------------------------------------
** Input: k = link index
** Output: returns quality concentration
** Purpose: computes current average quality in link k
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
double vsum = 0.0, msum = 0.0;
Pseg seg;
if (qual->Qualflag == NONE) return 0.0;
// Sum up the quality and volume in each segment of the link
if (qual->FirstSeg != NULL)
{
seg = qual->FirstSeg[k];
while (seg != NULL)
{
vsum += seg->v;
msum += (seg->c) * (seg->v);
seg = seg->prev;
}
}
// Compute average quality if link has volume
if (vsum > 0.0) return (msum / vsum);
// Otherwise use the average quality of the link's end nodes
else
{
return ((qual->NodeQual[net->Link[k].N1] +
qual->NodeQual[net->Link[k].N2]) / 2.);
}
}
double findsourcequal(Project *pr, int n, double volout, long tstep)
/*
**---------------------------------------------------------------------
** Input: n = node index
** volout = volume of node outflow over time step
** tstep = current quality time step
** Output: returns concentration added by an external quality source.
** Purpose: computes contribution (if any) of mass addition from an
** external quality source at a node.
**---------------------------------------------------------------------
*/
{
Network *net = &pr->network;
Hydraul *hyd = &pr->hydraul;
Quality *qual = &pr->quality;
Times *time = &pr->times;
double massadded = 0.0, c;
Psource source;
// Sources only apply to CHEMICAL analyses
if (qual->Qualflag != CHEM) return 0.0;
// Return 0 if node is not a quality source or has no outflow
source = net->Node[n].S;
if (source == NULL) return 0.0;
if (source->C0 == 0.0) return 0.0;
if (volout / tstep <= Q_STAGNANT) return 0.0;
// Added source concentration depends on source type
c = sourcequal(pr, source);
switch (source->Type)
{
// Concentration Source:
case CONCEN:
if (net->Node[n].Type == JUNCTION)
{
// ... source requires a negative demand at the node
if (hyd->NodeDemand[n] < 0.0)
{
c = -c * hyd->NodeDemand[n] * tstep / volout;
}
else c = 0.0;
}
break;
// Mass Inflow Booster Source:
case MASS:
// ... convert source input from mass/sec to concentration
c = c * tstep / volout;
break;
// Setpoint Booster Source:
// Source quality is difference between source strength
// & node quality
case SETPOINT:
c = MAX(c - qual->NodeQual[n], 0.0);
break;
// Flow-Paced Booster Source:
// Source quality equals source strength
case FLOWPACED:
break;
}
// Source mass added over time step = source concen. * outflow volume
massadded = c * volout;
// Update source's total mass added
source->Smass += massadded;
// Update Wsource
if (time->Htime >= time->Rstart)
{
qual->Wsource += massadded;
}
return c;
}
double sourcequal(Project *pr, Psource source)
/*
**--------------------------------------------------------------
** Input: source = a water quality source object
** Output: returns strength of quality source
** Purpose: determines source strength in current time period
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Times *time = &pr->times;
int i;
long k;
double c;
// Get source concentration (or mass flow) in original units
c = source->C0;
// Convert mass flow rate from min. to sec.
// and convert concen. from liters to cubic feet
if (source->Type == MASS) c /= 60.0;
else c /= pr->Ucf[QUALITY];
// Apply time pattern if assigned
i = source->Pat;
if (i == 0) return c;
k = ((time->Qtime + time->Pstart) / time->Pstep) %
(long)net->Pattern[i].Length;
return (c * net->Pattern[i].F[k]);
}
void evalmassbalance(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: none
** Purpose: computes the overall mass balance ratio of a
** quality constituent.
**--------------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
double massin;
double massout;
double massreacted;
if (qual->Qualflag == NONE) qual->MassBalance.ratio = 1.0;
else
{
qual->MassBalance.final = findstoredmass(pr);
massin = qual->MassBalance.initial + qual->MassBalance.inflow;
massout = qual->MassBalance.outflow + qual->MassBalance.final;
massreacted = qual->MassBalance.reacted;
if (massreacted > 0.0) massout += massreacted;
else massin -= massreacted;
if (massin == 0.0) qual->MassBalance.ratio = 1.0;
else qual->MassBalance.ratio = massout / massin;
}
}
double findstoredmass(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: returns total constituent mass stored in the network
** Purpose: finds the current mass of a constituent stored in
** all pipes and tanks.
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int i, k;
double totalmass = 0.0;
Pseg seg;
// Mass residing in each pipe
for (k = 1; k <= net->Nlinks; k++)
{
// Sum up the quality and volume in each segment of the link
seg = qual->FirstSeg[k];
while (seg != NULL)
{
totalmass += (seg->c) * (seg->v);
seg = seg->prev;
}
}
// Mass residing in each tank
for (i = 1; i <= net->Ntanks; i++)
{
// ... skip reservoirs
if (net->Tank[i].A == 0.0) continue;
// ... add up mass in each volume segment
else
{
k = net->Nlinks + i;
seg = qual->FirstSeg[k];
while (seg != NULL)
{
totalmass += seg->c * seg->v;
seg = seg->prev;
}
}
}
return totalmass;
}
int flowdirchanged(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: returns TRUE if flow direction changes in any link
** Purpose: finds new flow directions for each network link.
**--------------------------------------------------------------
*/
{
Hydraul *hyd = &pr->hydraul;
Quality *qual = &pr->quality;
int k;
int result = FALSE;
int newdir;
int olddir;
double q;
// Examine each network link
for (k = 1; k <= pr->network.Nlinks; k++)
{
// Determine sign (+1 or -1) of new flow rate
olddir = qual->FlowDir[k];
q = (hyd->LinkStatus[k] <= CLOSED) ? 0.0 : hyd->LinkFlow[k];
newdir = SGN(q);
// Indicate if flow is negligible
if (fabs(q) < Q_STAGNANT) newdir = 0;
// Reverse link's volume segments if flow direction changes sign
if (newdir * olddir < 0) reversesegs(pr, k);
// If flow direction changes either sign or magnitude then set
// result to true (e.g., if a link's positive flow becomes
// negligible then the network still needs to be re-sorted)
if (newdir != olddir) result = TRUE;
// ... replace old flow direction with the new direction
qual->FlowDir[k] = newdir;
}
return result;
}