Files
EPANET/src/qualreact.c
Lew Rossman 626b93a10f Corrections to 2-compartment tank mixing
Fixes volume remaining in mixing zone when there is net outflow from a 2-compartment tank.
2024-12-16 23:30:32 -05:00

801 lines
23 KiB
C

/*
******************************************************************************
Project: OWA EPANET
Version: 2.2
Module: qualreact.c
Description: computes water quality reactions within pipes and tanks
Authors: see AUTHORS
Copyright: see AUTHORS
License: see LICENSE
Last Updated: 12/16/2024
******************************************************************************
*/
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include "types.h"
// Exported functions
char setreactflag(Project *);
double getucf(double);
void ratecoeffs(Project *);
void reactpipes(Project *, long);
void reacttanks(Project *, long);
double mixtank(Project *, int, double, double ,double);
// Imported functions
extern void addseg(Project *, int, double, double);
extern void reversesegs(Project *, int);
// Local functions
static double piperate(Project *, int);
static double pipereact(Project *, int, double, double, long);
static double tankreact(Project *, double, double, double, long);
static double bulkrate(Project *, double, double, double);
static double wallrate(Project *, double, double, double, double);
static void tankmix1(Project *, int, double, double, double);
static void tankmix2(Project *, int, double, double, double);
static void tankmix3(Project *, int, double, double, double);
static void tankmix4(Project *, int, double, double, double);
char setreactflag(Project *pr)
/*
**-----------------------------------------------------------
** Input: none
** Output: returns 1 for reactive WQ constituent, 0 otherwise
** Purpose: checks if reactive chemical being simulated
**-----------------------------------------------------------
*/
{
Network *net = &pr->network;
int i;
if (pr->quality.Qualflag == TRACE) return 0;
else if (pr->quality.Qualflag == AGE) return 1;
else
{
for (i = 1; i <= net->Nlinks; i++)
{
if (net->Link[i].Type <= PIPE)
{
if (net->Link[i].Kb != 0.0 || net->Link[i].Kw != 0.0) return 1;
}
}
for (i = 1; i <= net->Ntanks; i++)
{
if (net->Tank[i].Kb != 0.0) return 1;
}
}
return 0;
}
double getucf(double order)
/*
**--------------------------------------------------------------
** Input: order = bulk reaction order
** Output: returns a unit conversion factor
** Purpose: converts bulk reaction rates from per Liter to
** per FT3 basis
**--------------------------------------------------------------
*/
{
if (order < 0.0) order = 0.0;
if (order == 1.0) return (1.0);
else return (1. / pow(LperFT3, (order - 1.0)));
}
void ratecoeffs(Project *pr)
/*
**--------------------------------------------------------------
** Input: none
** Output: none
** Purpose: determines wall reaction coeff. for each pipe
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int k;
double kw;
for (k = 1; k <= net->Nlinks; k++)
{
kw = net->Link[k].Kw;
if (kw != 0.0) kw = piperate(pr, k);
net->Link[k].Rc = kw;
qual->PipeRateCoeff[k] = 0.0;
}
}
void reactpipes(Project *pr, long dt)
/*
**--------------------------------------------------------------
** Input: dt = time step
** Output: none
** Purpose: reacts water within each pipe over a time step.
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int k;
Pseg seg;
double cseg, rsum, vsum;
// Examine each link in network
for (k = 1; k <= net->Nlinks; k++)
{
// Skip non-pipe links (pumps & valves)
if (net->Link[k].Type != PIPE) continue;
rsum = 0.0;
vsum = 0.0;
// Examine each segment of the pipe
seg = qual->FirstSeg[k];
while (seg != NULL)
{
// React segment over time dt
cseg = seg->c;
seg->c = pipereact(pr, k, seg->c, seg->v, dt);
// Update reaction component of mass balance
qual->MassBalance.reacted += (cseg - seg->c) * seg->v;
// Accumulate volume-weighted reaction rate
if (qual->Qualflag == CHEM)
{
rsum += fabs(seg->c - cseg) * seg->v;
vsum += seg->v;
}
seg = seg->prev;
}
// Normalize volume-weighted reaction rate
if (vsum > 0.0) qual->PipeRateCoeff[k] = rsum / vsum / dt * SECperDAY;
else qual->PipeRateCoeff[k] = 0.0;
}
}
void reacttanks(Project *pr, long dt)
/*
**--------------------------------------------------------------
** Input: dt = time step
** Output: none
** Purpose: reacts water within each tank over a time step.
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int i, k;
double c;
Pseg seg;
Stank *tank;
// Examine each tank in network
for (i = 1; i <= net->Ntanks; i++)
{
// Skip reservoirs
tank = &net->Tank[i];
if (tank->A == 0.0) continue;
// k is segment chain belonging to tank i
k = net->Nlinks + i;
// React each volume segment in the chain
seg = qual->FirstSeg[k];
while (seg != NULL)
{
c = seg->c;
seg->c = tankreact(pr, seg->c, seg->v, tank->Kb, dt);
qual->MassBalance.reacted += (c - seg->c) * seg->v;
seg = seg->prev;
}
}
}
double piperate(Project *pr, int k)
/*
**--------------------------------------------------------------
** Input: k = link index
** Output: returns reaction rate coeff. for 1st-order wall
** reactions or mass transfer rate coeff. for 0-order
** reactions
** Purpose: finds wall reaction rate coeffs.
**--------------------------------------------------------------
*/
{
Network *net = &pr->network;
Hydraul *hyd = &pr->hydraul;
Quality *qual = &pr->quality;
double a, d, u, q, kf, kw, y, Re, Sh;
d = net->Link[k].Diam; // Pipe diameter, ft
// Ignore mass transfer if Schmidt No. is 0
if (qual->Sc == 0.0)
{
if (qual->WallOrder == 0.0) return BIG;
else return (net->Link[k].Kw * (4.0 / d) / pr->Ucf[ELEV]);
}
// Compute Reynolds No.
// Flow rate made consistent with how its saved to hydraulics file
q = (hyd->LinkStatus[k] <= CLOSED) ? 0.0 : hyd->LinkFlow[k];
a = PI * d * d / 4.0; // pipe area
u = fabs(q) / a; // flow velocity
Re = u * d / hyd->Viscos; // Reynolds number
// Compute Sherwood No. for stagnant flow
// (mass transfer coeff. = Diffus./radius)
if (Re < 1.0) Sh = 2.0;
// Compute Sherwood No. for turbulent flow using the Notter-Sleicher formula.
else if (Re >= 2300.0) Sh = 0.0149 * pow(Re, 0.88) * pow(qual->Sc, 0.333);
// Compute Sherwood No. for laminar flow using Graetz solution formula.
else
{
y = d / net->Link[k].Len * Re * qual->Sc;
Sh = 3.65 + 0.0668 * y / (1.0 + 0.04 * pow(y, 0.667));
}
// Compute mass transfer coeff. (in ft/sec)
kf = Sh * qual->Diffus / d;
// For zero-order reaction, return mass transfer coeff.
if (qual->WallOrder == 0.0) return kf;
// For first-order reaction, return apparent wall coeff.
kw = net->Link[k].Kw / pr->Ucf[ELEV]; // Wall coeff, ft/sec
kw = (4.0 / d) * kw * kf / (kf + fabs(kw)); // Wall coeff, 1/sec
return kw;
}
double pipereact(Project *pr, int k, double c, double v, long dt)
/*
**------------------------------------------------------------
** Input: k = link index
** c = current quality in segment
** v = segment volume
** dt = time step
** Output: returns new WQ value
** Purpose: computes new quality in a pipe segment after
** reaction occurs
**------------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
double cnew, dc, dcbulk, dcwall, rbulk, rwall;
// For water age (hrs), update concentration by timestep
if (qual->Qualflag == AGE)
{
dc = (double)dt / 3600.0;
cnew = c + dc;
cnew = MAX(0.0, cnew);
return cnew;
}
// Otherwise find bulk & wall reaction rates
rbulk = bulkrate(pr, c, net->Link[k].Kb, qual->BulkOrder) * qual->Bucf;
rwall = wallrate(pr, c, net->Link[k].Diam, net->Link[k].Kw, net->Link[k].Rc);
// Find change in concentration over timestep
dcbulk = rbulk * (double)dt;
dcwall = rwall * (double)dt;
// Update cumulative mass reacted
if (pr->times.Htime >= pr->times.Rstart)
{
qual->Wbulk += fabs(dcbulk) * v;
qual->Wwall += fabs(dcwall) * v;
}
// Update concentration
dc = dcbulk + dcwall;
cnew = c + dc;
cnew = MAX(0.0, cnew);
return cnew;
}
double tankreact(Project *pr, double c, double v, double kb, long dt)
/*
**-------------------------------------------------------
** Input: c = current quality in tank
** v = tank volume
** kb = reaction coeff.
** dt = time step
** Output: returns new WQ value
** Purpose: computes new quality in a tank after
** reaction occurs
**-------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
double cnew, dc, rbulk;
// For water age, update concentration by timestep
if (qual->Qualflag == AGE)
{
dc = (double)dt / 3600.0;
}
// For chemical analysis apply bulk reaction rate
else
{
// Find bulk reaction rate
rbulk = bulkrate(pr, c, kb, qual->TankOrder) * qual->Tucf;
// Find concentration change & update quality
dc = rbulk * (double)dt;
if (pr->times.Htime >= pr->times.Rstart)
{
qual->Wtank += fabs(dc) * v;
}
}
cnew = c + dc;
cnew = MAX(0.0, cnew);
return cnew;
}
double bulkrate(Project *pr, double c, double kb, double order)
/*
**-----------------------------------------------------------
** Input: c = current WQ concentration
** kb = bulk reaction coeff.
** order = bulk reaction order
** Output: returns bulk reaction rate
** Purpose: computes bulk reaction rate (mass/volume/time)
**-----------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
double c1;
// Find bulk reaction potential taking into account
// limiting potential & reaction order.
// Zero-order kinetics:
if (order == 0.0) c = 1.0;
// Michaelis-Menton kinetics:
else if (order < 0.0)
{
c1 = qual->Climit + SGN(kb) * c;
if (fabs(c1) < TINY) c1 = SGN(c1) * TINY;
c = c / c1;
}
// N-th order kinetics:
else
{
// Account for limiting potential
if (qual->Climit == 0.0) c1 = c;
else c1 = MAX(0.0, SGN(kb) * (qual->Climit - c));
// Compute concentration potential
if (order == 1.0) c = c1;
else if (order == 2.0) c = c1 * c;
else c = c1 * pow(MAX(0.0, c), order - 1.0);
}
// Reaction rate = bulk coeff. * potential
if (c < 0) c = 0;
return kb * c;
}
double wallrate(Project *pr, double c, double d, double kw, double kf)
/*
**------------------------------------------------------------
** Input: c = current WQ concentration
** d = pipe diameter
** kw = intrinsic wall reaction coeff.
** kf = mass transfer coeff. for 0-order reaction
** (ft/sec) or apparent wall reaction coeff.
** for 1-st order reaction (1/sec)
** Output: returns wall reaction rate in mass/ft3/sec
** Purpose: computes wall reaction rate
**------------------------------------------------------------
*/
{
Quality *qual = &pr->quality;
if (kw == 0.0 || d == 0.0) return (0.0);
if (qual->WallOrder == 0.0) // 0-order reaction */
{
kf = SGN(kw) * c * kf; //* Mass transfer rate (mass/ft2/sec)
kw = kw * SQR(pr->Ucf[ELEV]); // Reaction rate (mass/ft2/sec)
if (fabs(kf) < fabs(kw)) kw = kf; // Reaction mass transfer limited
return (kw * 4.0 / d); // Reaction rate (mass/ft3/sec)
}
else return (c * kf); // 1st-order reaction
}
double mixtank(Project *pr, int n, double volin, double massin, double volout)
/*
**------------------------------------------------------------
** Input: n = node index
** volin = inflow volume to tank over time step
** massin = mass inflow to tank over time step
** volout = outflow volume from tank over time step
** Output: returns new quality for tank
** Purpose: mixes inflow with tank's contents to update its quality.
**------------------------------------------------------------
*/
{
Network *net = &pr->network;
int i;
double vnet;
i = n - net->Njuncs;
vnet = volin - volout;
switch (net->Tank[i].MixModel)
{
case MIX1: tankmix1(pr, i, volin, massin, vnet); break;
case MIX2: tankmix2(pr, i, volin, massin, vnet); break;
case FIFO: tankmix3(pr, i, volin, massin, vnet); break;
case LIFO: tankmix4(pr, i, volin, massin, vnet); break;
}
return net->Tank[i].C;
}
void tankmix1(Project *pr, int i, double vin, double win, double vnet)
/*
**---------------------------------------------
** Input: i = tank index
** vin = inflow volume
** win = mass inflow
** vnet = inflow - outflow
** Output: none
** Purpose: updates quality in a complete mix tank model
**---------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int k;
double vnew;
Pseg seg;
Stank *tank = &net->Tank[i];
k = net->Nlinks + i;
seg = qual->FirstSeg[k];
if (seg)
{
vnew = seg->v + vin;
if (vnew > 0.0) seg->c = (seg->c * seg->v + win) / vnew;
seg->v += vnet;
seg->v = MAX(0.0, seg->v);
tank->C = seg->c;
// Account for mass lost in tank overflow
if (seg->v > tank->Vmax)
{
qual->MassBalance.outflow += ((seg->v) - tank->Vmax) * tank->C;
seg->v = tank->Vmax;
}
}
}
void tankmix2(Project *pr, int i, double vin, double win, double vnet)
/*
**------------------------------------------------
** Input: i = tank index
** vin = inflow volume
** win = mass inflow
** vnet = inflow - outflow
** Output: none
** Purpose: updates quality in a 2-compartment tank model
**------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int k;
double vt, // Transferred volume
vmz, // Full mixing zone volume
vsz; // Full stagnant zone volume
Pseg mixzone, // Mixing zone segment
stagzone; // Stagnant zone segment
Stank *tank = &pr->network.Tank[i];
// Identify segments for each compartment
k = net->Nlinks + i;
mixzone = qual->LastSeg[k];
stagzone = qual->FirstSeg[k];
if (mixzone == NULL || stagzone == NULL) return;
// Full mixing zone volume
vmz = tank->V1frac * tank->Vmax;
// Tank is filling
vt = 0.0;
if (vnet > 0.0)
{
vt = MAX(0.0, (mixzone->v + vnet - vmz));
if (vin > 0.0)
{
mixzone->c = ((mixzone->c) * (mixzone->v) + win) /
(mixzone->v + vin);
}
if (vt > 0.0)
{
stagzone->c = ((stagzone->c) * (stagzone->v) +
(mixzone->c) * vt) / (stagzone->v + vt);
}
}
// Tank is emptying
else if (vnet < 0.0)
{
if (stagzone->v > 0.0) vt = MIN(stagzone->v, (-vnet));
if (vin + vt > 0.0)
{
mixzone->c = ((mixzone->c) * (mixzone->v) + win +
(stagzone->c) * vt) / (mixzone->v + vin + vt);
}
}
// Update segment volumes
if (vt > 0.0)
{
if (vnet > 0.0)
{
mixzone->v = vmz;
stagzone->v += vt;
// Account for mass lost in overflow from stagnant zone
vsz = (tank->Vmax) - vmz;
if (stagzone->v > vsz)
{
qual->MassBalance.outflow += ((stagzone->v) - vsz) * stagzone->c;
stagzone->v = vsz;
}
}
else
{
stagzone->v = MAX(0.0, ((stagzone->v) - vt));
mixzone->v = vmz + vt + vnet;
}
}
else
{
mixzone->v += vnet;
mixzone->v = MIN(mixzone->v, vmz);
mixzone->v = MAX(0.0, mixzone->v);
if (vmz - mixzone->v > 0.0) stagzone->v = 0.0;
}
// Use quality of mixing zone to represent quality of
// tank since this is where outflow begins to flow from
tank->C = mixzone->c;
}
void tankmix3(Project *pr, int i, double vin, double win, double vnet)
/*
**----------------------------------------------------------
** Input: i = tank index
** vin = inflow volume
** win = mass inflow
** vnet = inflow - outflow
** Output: none
** Purpose: Updates quality in a First-In-First-Out (FIFO) tank model.
**----------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int k;
double vout, vseg;
double cin, vsum, wsum;
Pseg seg;
Stank *tank = &pr->network.Tank[i];
k = net->Nlinks + i;
if (qual->LastSeg[k] == NULL || qual->FirstSeg[k] == NULL) return;
// Add new last segment for flow entering the tank
if (vin > 0.0)
{
// ... increase segment volume if inflow has same quality as segment
cin = win / vin;
seg = qual->LastSeg[k];
if (fabs(seg->c - cin) < qual->Ctol) seg->v += vin;
// ... otherwise add a new last segment to the tank
else addseg(pr, k, vin, cin);
}
// Find volume leaving tank, adjusted so its volume doesn't exceed Vmax
vout = vin - vnet;
if (tank->V >= tank->Vmax && vnet > 0.0) vout = vin;
// Withdraw outflow from first segment
vsum = 0.0;
wsum = 0.0;
while (vout > 0.0)
{
seg = qual->FirstSeg[k];
if (seg == NULL) break;
vseg = seg->v; // Flow volume from leading seg
vseg = MIN(vseg, vout);
if (seg == qual->LastSeg[k]) vseg = vout;
vsum += vseg;
wsum += (seg->c) * vseg;
vout -= vseg; // Remaining flow volume
if (vout >= 0.0 && vseg >= seg->v) // Seg used up
{
if (seg->prev)
{
qual->FirstSeg[k] = seg->prev;
seg->prev = qual->FreeSeg;
qual->FreeSeg = seg;
}
}
else seg->v -= vseg; // Remaining volume in segment
}
// Use quality withdrawn from 1st segment
// to represent overall quality of tank
if (vsum > 0.0) tank->C = wsum / vsum;
else if (qual->FirstSeg[k] == NULL) tank->C = 0.0;
else tank->C = qual->FirstSeg[k]->c;
// Account for mass lost in overflow from 1st segment
if (tank->V >= tank->Vmax && vnet > 0.0)
qual->MassBalance.outflow += vnet * tank->C;
}
void tankmix4(Project *pr, int i, double vin, double win, double vnet)
/*
**----------------------------------------------------------
** Input: i = tank index
** vin = inflow volume
** win = mass inflow
** vnet = inflow - outflow
** Output: none
** Purpose: Updates quality in a Last In-First Out (LIFO) tank model.
**----------------------------------------------------------
*/
{
Network *net = &pr->network;
Quality *qual = &pr->quality;
int k;
double cin, vsum, wsum, vseg;
Pseg seg;
Stank *tank = &pr->network.Tank[i];
k = net->Nlinks + i;
if (qual->LastSeg[k] == NULL || qual->FirstSeg[k] == NULL) return;
// Find inflow concentration
if (vin > 0.0) cin = win / vin;
else cin = 0.0;
// If tank filling, then create new last seg
tank->C = qual->LastSeg[k]->c;
seg = qual->LastSeg[k];
if (vnet > 0.0)
{
// ... inflow quality is same as last segment's quality,
// so just add inflow volume to last segment
if (fabs(seg->c - cin) < qual->Ctol) seg->v += vnet;
// ... otherwise add a new last segment with inflow quality
else addseg(pr, k, vnet, cin);
// Update reported tank quality
tank->C = qual->LastSeg[k]->c;
// If tank full then remove vnet from leading segments
if (tank->V >= tank->Vmax)
{
wsum = 0.0;
while (vnet > 0.0)
{
seg = qual->FirstSeg[k];
if (seg == NULL) break;
vseg = seg->v; // Flow volume from leading seg
vseg = MIN(vseg, vnet);
if (seg == qual->LastSeg[k]) vseg = vnet;
wsum += (seg->c) * vseg;
vnet -= vseg; // Remaining flow volume
if (vnet >= 0.0 && vseg >= seg->v) // Seg used up
{
if (seg->prev)
{
qual->FirstSeg[k] = seg->prev;
seg->prev = qual->FreeSeg;
qual->FreeSeg = seg;
}
}
else seg->v -= vseg; // Remaining volume in segment
}
qual->MassBalance.outflow += wsum;
}
}
// If tank emptying then remove last segments until vnet consumed
else if (vnet < 0.0)
{
vsum = 0.0;
wsum = 0.0;
vnet = -vnet;
// Reverse segment chain so segments are processed from last to first
reversesegs(pr, k);
// While there is still volume to remove
while (vnet > 0.0)
{
// ... start with reversed first segment
seg = qual->FirstSeg[k];
if (seg == NULL) break;
// ... find volume to remove from it
vseg = seg->v;
vseg = MIN(vseg, vnet);
if (seg == qual->LastSeg[k]) vseg = vnet;
// ... update total volume & mass removed
vsum += vseg;
wsum += (seg->c) * vseg;
// ... update remiaing volume to remove
vnet -= vseg;
// ... if no more volume left in current segment
if (vnet >= 0.0 && vseg >= seg->v)
{
// ... replace current segment with previous one
if (seg->prev)
{
qual->FirstSeg[k] = seg->prev;
seg->prev = qual->FreeSeg;
qual->FreeSeg = seg;
}
}
// ... otherwise reduce volume of current segment
else seg->v -= vseg;
}
// Restore original orientation of segment chain
reversesegs(pr, k);
// Reported tank quality is mixture of flow released and any inflow
tank->C = (wsum + win) / (vsum + vin);
}
}