/* ****************************************************************************** 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: 05/15/2019 ****************************************************************************** */ #include #include #include #include #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) { errcode = buildadjlists(net); if (errcode ) 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); } 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] =(FlowDirection) newdir; } return result; }