/* ****************************************************************************** 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: 05/15/2019 ****************************************************************************** */ #include #include #include #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; } } 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 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->V1max; // 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) { mixzone->v = vmz; if (vnet > 0.0) stagzone->v += vt; else stagzone->v = MAX(0.0, ((stagzone->v) - vt)); } else { mixzone->v += vnet; mixzone->v = MIN(mixzone->v, vmz); mixzone->v = MAX(0.0, mixzone->v); 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); } // Withdraw flow from first segment vsum = 0.0; wsum = 0.0; vout = vin - vnet; 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; } 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 inflows & outflows 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 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); } }