/*
******************************************************************************
Project:      OWA EPANET
Version:      2.3
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 it's 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 remaining 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);
    }
}
