Repository: pyswmm/Stormwater-Management-Model (language: python)

Issue:
Update `nrtest` instructions with step-by-step instructions for local testing
The instructions for running `nrtest` found [here](https://github.com/OpenWaterAnalytics/Stormwater-Management-Model/blob/develop/tests/README.md) need to be updated so developers can more easily run test locally. 
Small AppveyorCI and CMAKE Fixes
- Trying to make the lights green (at least on Appveyor).
Add benchmarks for extran and routing tests
This PR addresses Issue #132. The scripts `gen-config.cmd` and `run-nrtest.cmd` have also been added to the `tools` folder.   


Discussion / hints:
Unit testing has been added to SWMM. The testing instructions need to be updated to provide information on unit testing as well. 
Some of the settings pulled from a fresh downloaded compiler from EPA's website related to the default Release Configuration... (Looks like it does use fast floating point model...).

#### VC2010-CLE
```
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>MultiByte</CharacterSet>


  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>MaxSpeed</Optimization>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
      <CompileAs>CompileAsC</CompileAs>
      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
      <OpenMPSupport>true</OpenMPSupport>
      <FloatingPointModel>Fast</FloatingPointModel>
      <FloatingPointExceptions>false</FloatingPointExceptions>
    </ClCompile>
```

#### VC2010-DLL

```
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>

  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <Optimization>MaxSpeed</Optimization>
      <IntrinsicFunctions>false</IntrinsicFunctions>
      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
      <WholeProgramOptimization>true</WholeProgramOptimization>
      <ExceptionHandling>
      </ExceptionHandling>
      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
      <StructMemberAlignment>Default</StructMemberAlignment>
      <FloatingPointModel>Fast</FloatingPointModel>
      <FloatingPointExceptions>false</FloatingPointExceptions>
      <DisableLanguageExtensions>false</DisableLanguageExtensions>
      <WarningLevel>Level4</WarningLevel>
      <CompileAs>CompileAsC</CompileAs>
      <OpenMPSupport>true</OpenMPSupport>
      <MultiProcessorCompilation>false</MultiProcessorCompilation>
    </ClCompile>
```
@goanpeca, I've turned off most of the build matrix.  We're in the green for the Benchmark compiler!!!  (There are issues with finding OpenMP). VS2010, (if I understand this correctly), only allowed paid subscribers to use openMP for VS2010.  
@bemcdonnell appveyor is running now with 21/22 tests passing. 
@michaeltryby, it would be really helpful if you could have the testing framework print a few lines of the diff so we can get a quick preview of what is going on with it.  How complicated would it be if the tool would print ~10 lines of a failed RPT diff?
@michaeltryby, maybe make a small injection here: 

https://github.com/michaeltryby/Stormwater-Management-Model/blob/develop/tools/nrtest-swmm/main.py#L79-L89

```python

def report_compare(path_test, path_ref, (comp_args)):
    '''
    Compares results in two report files ignoring contents of header and footer. 
    '''
    with open(path_test ,'r') as ftest, open(path_ref, 'r') as fref:
        for (test_line, ref_line) in it.izip(hdf.parse(ftest, 4, 4)[1], 
                                             hdf.parse(fref, 4, 4)[1]): 
            if test_line != ref_line: 
                print('benchmark-> {}\ntest       ->{}'.format(ref_line.replace('\n',''), test_line.replace('\n','')))
                return False
              
    return True 
```
@bemcdonnell couple of thought ...
1) Running tests on CI is not a substitute for running tests locally. 
2) Developers should run the tests locally to make sure their code works before issuing a PR. 
3) Being a developer comes with a lot of responsibility. One of those is testing code locally.  
4) If you want to retrieve failure information from the CI server, that should be handled in the CI configuration after tests run and a failure is detected. NOT in the `nrtest` comparison routine!  On failure have the server email or cache the failing test artifacts where you can retrieve them. 
5) The developer should be testing locally. Getting information back from the server can be used in situations where there are differences between local and remote testing.  
6) When in doubt you should be testing locally. 
7) The great thing is, its super easy to run `nrtest` locally!
On Appveyor it looks like you are able to associate and retrieve artifacts with a CI build.   
https://help.appveyor.com/discussions/questions/1649-how-to-keep-a-build-artifact-when-tests-fail

The support for savings test artifacts isn't as good on Travis. They only support S3.
https://github.com/travis-ci/artifacts
@bemcdonnell I propose we accept this PR as is and create an issue to configure appveyor to retrieve failed test artifacts. What do you think?   
@bemcdonnell I can also create an issue to update the readme for running `nrtest` found [here](https://github.com/OpenWaterAnalytics/Stormwater-Management-Model/blob/develop/tests/README.md) with step-by-step instructions for Windows. 
@michaeltryby, i wonder why we have one failure...
@bemcdonnell Let's find out!

Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b7ebd6629..10969a2b0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,7 +22,7 @@ endif()
 
 
 project(swmm-solver
-    VERSION 5.2.3
+    VERSION 5.2.4
     LANGUAGES C CXX
 )
 
diff --git a/src/solver/consts.h b/src/solver/consts.h
index 3cf490793..394b5e2e8 100644
--- a/src/solver/consts.h
+++ b/src/solver/consts.h
@@ -3,7 +3,7 @@
 //
 //   Project: EPA SWMM5
 //   Version: 5.2
-//   Date:    02/12/23  (Build 5.2.3)
+//   Date:    07/15/23  (Build 5.2.4)
 //   Author:  L. Rossman
 //
 //   Various Constants
@@ -17,7 +17,7 @@
 //------------------
 
 // OWA Version string stored in version.h
-// #define   VERSION            52003
+// #define   VERSION            52004
 #define   MAGICNUMBER        516114522
 #define   EOFMARK            0x1A           // Use 0x04 for UNIX systems
 #define   MAXTITLE           3              // Max. # title lines
diff --git a/src/solver/dwflow.c b/src/solver/dwflow.c
index d6484fe57..25b2b874a 100644
--- a/src/solver/dwflow.c
+++ b/src/solver/dwflow.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     08/01/22  (Build 5.2.1)
+//   Date:     06/12/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //             M. Tryby (EPA)
 //             R. Dickinson (CDM)
@@ -26,6 +26,8 @@
 //     (qOld) in call to link_getLossRate. 
 //   Build 5.2.1:
 //   - Implements the new option to skip checking for normal flow limitations.
+//   Build 5.2.4:
+//   - Arguments to function link_getLossRate changed.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -231,7 +233,7 @@ void  dwflow_findConduitFlow(int j, int steps, double omega, double dt)
     }
 
     // --- 6. term for evap and seepage losses per unit length
-    dq6 = link_getLossRate(j, qLast) * 2.5 * dt * v / link_getLength(j);
+    dq6 = link_getLossRate(j, DW, qLast, dt) * 2.5 * dt * v / link_getLength(j);
 
     // --- combine terms to find new conduit flow
     denom = 1.0 + dq1 + dq5;
diff --git a/src/solver/dynwave.c b/src/solver/dynwave.c
index ac302e849..85968e2a9 100644
--- a/src/solver/dynwave.c
+++ b/src/solver/dynwave.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21  (Build 5.2.0)
+//   Date:     07/13/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //             M. Tryby (EPA)
 //             R. Dickinson (CDM)
@@ -44,6 +44,9 @@
 //   - Roll back the 5.1.014 change for conduit losses in updateNodeFlows().
 //   Build 5.2.0:
 //   - Support added for reporting most frequent non-converging links.
+//   Build 5.2.4:
+//   - Conduit evap+seepage outflow split evenly between outflow from
+//     conduit's upstream and non-outfall downstream nodes.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -129,7 +132,7 @@ void dynwave_init()
             " Not enough memory for dynamic wave routing.");
         return;
     }
-
+    
     // --- initialize node surface areas & crown elev.
     for (i = 0; i < Nobjects[NODE]; i++ )
     {
@@ -144,6 +147,7 @@ void dynwave_init()
         j = Link[i].node1;
         z = Node[j].invertElev + Link[i].offset1 + Link[i].xsect.yFull;
         Node[j].crownElev = MAX(Node[j].crownElev, z);
+        
         j = Link[i].node2;
         z = Node[j].invertElev + Link[i].offset2 + Link[i].xsect.yFull;
         Node[j].crownElev = MAX(Node[j].crownElev, z);
@@ -534,29 +538,39 @@ void updateNodeFlows(int i)
     int    n1 = Link[i].node1;
     int    n2 = Link[i].node2;
     double q = Link[i].newFlow;
-    double uniformLossRate = 0.0;
-
-    // --- compute any uniform seepage loss from a conduit
-    if ( Link[i].type == CONDUIT )
-    {
-        k = Link[i].subIndex;
-        uniformLossRate = Conduit[k].evapLossRate + Conduit[k].seepLossRate; 
-        barrels = Conduit[k].barrels;
-        uniformLossRate *= barrels;
-    }
+    double conduitLossRate = 0.0;
 
     // --- update total inflow & outflow at upstream/downstream nodes
     if ( q >= 0.0 )
     {
-        Node[n1].outflow += q + uniformLossRate;
+        Node[n1].outflow += q;
         Node[n2].inflow  += q;
     }
     else
     {
         Node[n1].inflow   -= q;
-        Node[n2].outflow  -= q - uniformLossRate;
+        Node[n2].outflow  -= q;
     }
-
+ 
+    // --- add any uniform evap & seepage loss from conduit link
+    if ( Link[i].type == CONDUIT )
+    {
+        k = Link[i].subIndex;
+        barrels = Conduit[k].barrels;
+        conduitLossRate = (Conduit[k].evapLossRate + Conduit[k].seepLossRate) *
+                          barrels;
+        if (conduitLossRate > 0.0)
+        {
+            // --- outfall nodes do not share evap & seepage losses
+            if (Node[n1].type != OUTFALL && Node[n2].type != OUTFALL)
+                conduitLossRate /= 2.0;
+            if (Node[n1].type != OUTFALL)
+                Node[n1].outflow += conduitLossRate;
+            if (Node[n2].type != OUTFALL)
+                Node[n2].outflow += conduitLossRate;
+        }
+    }
+    
     // --- add surf. area contributions to upstream/downstream nodes
     Xnode[Link[i].node1].newSurfArea += Link[i].surfArea1 * barrels;
     Xnode[Link[i].node2].newSurfArea += Link[i].surfArea2 * barrels;
@@ -585,7 +599,7 @@ int findNodeDepths(double dt)
 //
 {
     int i;
-    double yOld;        // previous node depth (ft)
+    double yOld = 0.0;       // previous node depth (ft)
 
     // --- compute outfall depths based on flow in connecting link
     for ( i = 0; i < Nobjects[LINK]; i++ ) link_setOutfallDepth(i);
@@ -654,12 +668,11 @@ void setNodeDepth(int i, double dt)
     Node[i].overflow = 0.0;
     surfArea = Xnode[i].newSurfArea;
     surfArea = MAX(surfArea, MinSurfArea);
-
+    
     // --- determine average net flow volume into node over the time step
     dQ = Node[i].inflow - Node[i].outflow;
     dV = 0.5 * (Node[i].oldNetInflow + dQ) * dt;
 
-
     // --- determine if node is EXTRAN surcharged
     if (SurchargeMethod == EXTRAN)
     {
diff --git a/src/solver/flowrout.c b/src/solver/flowrout.c
index f296c3fb0..73f5f67d4 100644
--- a/src/solver/flowrout.c
+++ b/src/solver/flowrout.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     05/02/22  (Build 5.2.1)
+//   Date:     06/12/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //             M. Tryby (EPA)
 //
@@ -27,6 +27,8 @@
 //   Build 5.2.1:
 //   - For storage routing, after convergence the reported depth is now
 //     based on the last volume found rather than the next trial depth. 
+//   Build 5.2.4:
+//   - Arguments to link_getLossRate changed.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -767,7 +769,7 @@ int steadyflow_execute(int j, double* qin, double* qout, double tStep)
         else 
         {
             // --- adjust flow for evap and infil losses
-            q -= link_getLossRate(j, q);
+            q -= link_getLossRate(j, SF, q, tStep);
          
             // --- flow can't exceed full flow 
             if ( q > Link[j].qFull )
diff --git a/src/solver/funcs.h b/src/solver/funcs.h
index 21af0d54d..642318ebd 100644
--- a/src/solver/funcs.h
+++ b/src/solver/funcs.h
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21 (Build 5.2.0)
+//   Date:     06/12/23 (Build 5.2.4)
 //   Author:   L. Rossman
 //             M. Tryby (EPA)
 //
@@ -29,6 +29,8 @@
 //   - Support added for named variables & math expressions in control rules.
 //   - Support added for tracking a gage's prior n-hour rainfall total.
 //   - Refactored external inflow code.
+//   Build 5.2.4:
+//   - Additional arguments added to function link_getLossRate.
 //-----------------------------------------------------------------------------
 
 #ifndef FUNCS_H
@@ -425,7 +427,7 @@ double  link_getYnorm(int link, double q);
 double  link_getVelocity(int link, double q, double y);
 double  link_getFroude(int link, double v, double y);
 double  link_getPower(int link);
-double  link_getLossRate(int link, double q);
+double  link_getLossRate(int link, int routeModel, double q, double tstep);
 char    link_getFullState(double a1, double a2, double aFull);
 
 void    link_getResults(int link, double wt, float x[]);
diff --git a/src/solver/inlet.c b/src/solver/inlet.c
index c44ae9fcb..f3aa180a8 100644
--- a/src/solver/inlet.c
+++ b/src/solver/inlet.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     10/08/22 (Build 5.2.2)
+//   Date:     07/13/23 (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   Street/Channel Inlet Functions
@@ -18,6 +18,10 @@
 //   - Substitutes the constant BIG for HUGE.
 //   Build 5.2.2:
 //   - Additional statistics added to Street Flow Summary table.
+//   Build 5.2.4:
+//   - Fixed expression for equivalent gutter slope in getCurbInletCapture.
+//   - Corrected sign in equation for effective head in a curb inlet
+//     with an inclined throat opening in getCurbOrificeFlow.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -1444,7 +1448,7 @@ double getCurbInletCapture(double Q, double L)
 //  Input:   Q = flow rate seen by inlet (cfs)
 //           L = length of inlet opening (ft)
 //  Output:  returns captured flow rate (cfs)
-//  Purpose: finds the flow captured by an on-sag inlet.
+//  Purpose: finds the flow captured by an on-grade curb opening inlet.
 //
 {
     double Se = Sx,     // equivalent gutter slope
@@ -1460,7 +1464,7 @@ double getCurbInletCapture(double Q, double L)
     {
         Sr = Sw / Sx;
         Eo = getEo(Sr, T-W, W);
-        Se = Sx + Sw * Eo;                                 //HEC-22 Eq(4-24)
+        Se = Sx + (a/W) * Eo;                              //HEC-22 Eq(4-24)
     }
 
     // --- opening length for full capture
@@ -1741,7 +1745,7 @@ double getCurbOrificeFlow(double di, double h, double L, int throatAngle)
     if (throatAngle == HORIZONTAL_THROAT)
         d = di - h / 2.0;
     else if (throatAngle == INCLINED_THROAT)
-        d = di + (h / 2.0) * 0.7071;
+        d = di - (h / 2.0) * 0.7071;
     return 0.67 * h * L * sqrt(2.0 * 32.16 * d);           //HEC-22 Eq(4-31a)
 }
 
diff --git a/src/solver/kinwave.c b/src/solver/kinwave.c
index b137e4ce9..cafa2e346 100644
--- a/src/solver/kinwave.c
+++ b/src/solver/kinwave.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21  (Build 5.2.0)
+//   Date:     06/12/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //             M. Tryby (EPA)
 //
@@ -15,6 +15,8 @@
 //   - Conduit inflow passed to function that computes conduit losses.
 //   Build 5.1.014:
 //   - Arguments to function link_getLossRate changed.
+//   Build 5.2.4:
+//   - Arguments to function link_getLossRate changed again.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -100,7 +102,7 @@ int kinwave_execute(int j, double* qinflow, double* qoutflow, double tStep)
     qin = (*qinflow) / Conduit[k].barrels / Qfull;
 
     // --- compute evaporation and infiltration loss rate
-    q3 = link_getLossRate(j, qin*Qfull) / Qfull;
+    q3 = link_getLossRate(j, KW, qin*Qfull, tStep) / Qfull;
 
     // --- normalize previous areas
     a1 = Conduit[k].a1 / Afull;
diff --git a/src/solver/lid.c b/src/solver/lid.c
index a6e82e16c..63de8920e 100644
--- a/src/solver/lid.c
+++ b/src/solver/lid.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     02/10/23   (Build 5.2.3)
+//   Date:     07/13/23   (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   This module handles all data processing involving LID (Low Impact
@@ -70,6 +70,8 @@
 //   - Covered property added to RAIN_BARREL parameters
 //   Build 5.2.3
 //   - Fixed double counting of initial water volume in green roof drain mat.
+//   Build 5.2.4
+//   - Fixed test for invalid data in readDrainData function.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -786,7 +788,7 @@ int readDrainData(int j, char* toks[], int ntoks)
     for (i = 0; i < 6; i++) x[i] = 0.0;
     for (i = 2; i < 8; i++)
     {
-        if ( (ntoks > i) && (! getDouble(toks[i], &x[i-2]) || x[i-2]) < 0.0 )
+        if ( (ntoks > i) && (! getDouble(toks[i], &x[i-2]) || x[i-2] < 0.0) )
             return error_setInpError(ERR_NUMBER, toks[i]);
     }
 
@@ -1084,6 +1086,7 @@ void validateLidProc(int j)
     // moved clogging factor conversion logic to readStorageData and readPavementData
     // in 14d2e62e9a6baad89f6cd2dc0c55907e1d2289b2 to allow multiple calls to validateLidProc
     // in toolkit.c
+    
     //... for certain LID types, immediate overflow of excess surface water
     //    occurs if either the surface roughness or slope is zero
     LidProcs[j].surface.canOverflow = TRUE;
diff --git a/src/solver/lidproc.c b/src/solver/lidproc.c
index a74fd67f7..d0876fd15 100644
--- a/src/solver/lidproc.c
+++ b/src/solver/lidproc.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21   (Build 5.2.0)
+//   Date:     07/13/23   (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   This module computes the hydrologic performance of an LID (Low Impact
@@ -53,6 +53,11 @@
 //   - Fixed failure to account for effect of Impervious Surface Fraction on
 //     pavement permeability for Permeable Pavement LID
 //   - Fixed units conversion for pavement depth in detailed report file.
+//   Build 5.2.4:
+//   - Modified flux limits in biocellFluxRates, pavementFluxRates and
+//     trenchFluxRates.
+//   - Corrected head calculation in getStorageDrainRate when unit has both
+//     a soil and pavement layer.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -700,61 +705,64 @@ void biocellFluxRates(double x[], double f[])
         SurfaceInfil = MIN(SurfaceInfil, maxRate);
     }
 
-    //... storage & soil layers are full
-    else if ( soilTheta >= soilPorosity && storageDepth >= storageThickness )
+    else
     {
-        //... limiting rate is smaller of soil perc and storage outflow
-        maxRate = StorageExfil + StorageDrain;
-        if ( SoilPerc < maxRate )
+        //... storage & soil layers are full
+        if ( soilTheta >= soilPorosity && storageDepth >= storageThickness )
         {
-            maxRate = SoilPerc;
-            if ( maxRate > StorageExfil ) StorageDrain = maxRate - StorageExfil;
-            else
+            //... limiting rate is smaller of soil perc and storage outflow
+            maxRate = StorageExfil + StorageDrain;
+            if ( SoilPerc < maxRate )
             {
-                StorageExfil = maxRate;
-                StorageDrain = 0.0;
+                maxRate = SoilPerc;
+                if ( maxRate > StorageExfil ) StorageDrain = maxRate - StorageExfil;
+                else
+                {
+                    StorageExfil = maxRate;
+                    StorageDrain = 0.0;
+                }
             }
-        }
-        else SoilPerc = maxRate;
-
-        //... apply limiting rate to surface infil.
-        SurfaceInfil = MIN(SurfaceInfil, maxRate);
-    }
+            else SoilPerc = maxRate;
 
-    //... either layer not full
-    else if ( storageThickness > 0.0 )
-    {
-        //... limit storage exfiltration by available storage volume
-        maxRate = SoilPerc - StorageEvap + storageDepth*storageVoidFrac/Tstep;
-        StorageExfil = MIN(StorageExfil, maxRate);
-        StorageExfil = MAX(StorageExfil, 0.0);
+            //... apply limiting rate to surface infil.
+            SurfaceInfil = MIN(SurfaceInfil, maxRate);
+        }
 
-        //... limit underdrain flow by volume above drain offset
-        if ( StorageDrain > 0.0 )
+        //... either layer not full
+        else
         {
-            maxRate = -StorageExfil - StorageEvap;
-            if ( storageDepth >= storageThickness) maxRate += SoilPerc;
-            if ( theLidProc->drain.offset <= storageDepth )
+            //... limit storage exfiltration by available storage volume
+            maxRate = SoilPerc - StorageEvap + storageDepth*storageVoidFrac/Tstep;
+            StorageExfil = MIN(StorageExfil, maxRate);
+            StorageExfil = MAX(StorageExfil, 0.0);
+
+            //... limit underdrain flow by volume above drain offset
+            if ( StorageDrain > 0.0 )
             {
-                maxRate += (storageDepth - theLidProc->drain.offset) *
-                           storageVoidFrac/Tstep;
+                maxRate = -StorageExfil - StorageEvap;
+                if ( storageDepth >= storageThickness) maxRate += SoilPerc;
+                if ( theLidProc->drain.offset <= storageDepth )
+                {
+                    maxRate += (storageDepth - theLidProc->drain.offset) *
+                               storageVoidFrac/Tstep;
+                }
+                maxRate = MAX(maxRate, 0.0);
+                StorageDrain = MIN(StorageDrain, maxRate);
             }
-            maxRate = MAX(maxRate, 0.0);
-            StorageDrain = MIN(StorageDrain, maxRate);
-        }
-
-        //... limit soil perc by unused storage volume
-        maxRate = StorageExfil + StorageDrain + StorageEvap +
-                  (storageThickness - storageDepth) *
-                  storageVoidFrac/Tstep;
-        SoilPerc = MIN(SoilPerc, maxRate);
+        
+            //... limit soil perc by unused storage volume
+            maxRate = StorageExfil + StorageDrain + StorageEvap +
+                      (storageThickness - storageDepth) *
+                      storageVoidFrac/Tstep;
+            SoilPerc = MIN(SoilPerc, maxRate);
 
-        //... limit surface infil. by unused soil volume
-        maxRate = (soilPorosity - soilTheta) * soilThickness / Tstep +
-                  SoilPerc + SoilEvap;
-        SurfaceInfil = MIN(SurfaceInfil, maxRate);
+            //... limit surface infil. by unused soil volume
+            maxRate = (soilPorosity - soilTheta) * soilThickness / Tstep +
+                      SoilPerc + SoilEvap;
+            SurfaceInfil = MIN(SurfaceInfil, maxRate);
+        }
     }
-
+    
     //... find surface layer outflow rate
     SurfaceOutflow = getSurfaceOutflowRate(surfaceDepth);
 
@@ -849,7 +857,7 @@ void trenchFluxRates(double x[], double f[])
     SurfaceOutflow = getSurfaceOutflowRate(surfaceDepth);
 
     // ... find net fluxes for each layer
-    f[SURF] = SurfaceInflow - SurfaceEvap - StorageInflow - SurfaceOutflow /
+    f[SURF] = (SurfaceInflow - SurfaceEvap - StorageInflow - SurfaceOutflow) /
               theLidProc->surface.voidFrac;;
     f[STOR] = (StorageInflow - StorageEvap - StorageExfil - StorageDrain) /
               theLidProc->storage.voidFrac;
@@ -1012,6 +1020,7 @@ void pavementFluxRates(double x[], double f[])
             StorageExfil = MIN(StorageExfil, SoilPerc);
             StorageDrain = SoilPerc - StorageExfil;
         }
+        PavePerc = MIN(PavePerc, SoilPerc);        
 
         //... limit surface infil. by available pavement volume
         availVolume = (paveThickness - paveDepth) * paveVoidFrac;
@@ -1027,6 +1036,8 @@ void pavementFluxRates(double x[], double f[])
         PavePerc = MIN(PavePerc, SoilPerc);
         SoilPerc = PavePerc;
         SurfaceInfil = MIN(SurfaceInfil,PavePerc); 
+        maxRate = MAX(StorageVolume / Tstep + SoilPerc - StorageEvap, 0.0);
+	    StorageExfil = MIN(StorageExfil, maxRate); 
     }
 
     //... no adjoining layers are full
@@ -1416,7 +1427,11 @@ double  getStorageDrainRate(double storageDepth, double soilTheta,
                 //     depth in layer above it
                 if ( soilTheta >= soilPorosity )
                 {
-                    if ( paveThickness > 0.0 ) head += paveDepth;
+                    if ( paveThickness > 0.0 )
+                    {
+                        head += paveDepth;
+                        if ( paveDepth >= paveThickness ) head += surfaceDepth;
+                    }    
                     else head += surfaceDepth;
                 }
             }
@@ -1424,7 +1439,7 @@ double  getStorageDrainRate(double storageDepth, double soilTheta,
 
         // --- no soil layer so increase head by water level in pavement
         //     layer and possibly surface layer
-        if ( paveThickness > 0.0 )
+        else if ( paveThickness > 0.0 )
         {
             head += paveDepth;
             if ( paveDepth >= paveThickness ) head += surfaceDepth;
diff --git a/src/solver/link.c b/src/solver/link.c
index 052a030b9..c8e035f4c 100644
--- a/src/solver/link.c
+++ b/src/solver/link.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     10/29/22   (Build 5.2.2)
+//   Date:     06/12/23   (Build 5.2.4)
 //   Author:   L. Rossman
 //             M. Tryby (EPA)
 //
@@ -46,6 +46,8 @@
 //   - Warning no longer issued when conduit elevation drop < MIN_DELTA_Z.
 //   Build 5.2.2:
 //   - Warning for conduit elevation drop < MIN_DELTA_Z restored.
+//   Build 5.2.4:
+//   - Conduit evap+seepage loss under DW routing limited by conduit volume.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -99,7 +101,8 @@ static double conduit_getLength(int j);
 static double conduit_getLengthFactor(int j, int k, double roughness);
 static double conduit_getSlope(int j);
 static double conduit_getInflow(int j);
-static double conduit_getLossRate(int j, double q);
+static double conduit_getLossRate(int j, int routeModel, double q,
+              double tstep);
 
 static int    pump_readParams(int j, int k, char* tok[], int ntoks);
 static void   pump_validate(int j, int k);
@@ -887,9 +890,10 @@ double link_getPower(int j)
 
 //=============================================================================
 
-double link_getLossRate(int j, double q)
+double link_getLossRate(int j, int routeModel, double q, double tstep)
 //
 //  Input:   j = link index
+//           routeModel = flow routing model type
 //           q = flow rate (ft3/sec)
 //           tstep = time step (sec)
 //  Output:  returns uniform loss rate in link (ft3/sec)
@@ -897,7 +901,8 @@ double link_getLossRate(int j, double q)
 //           evaporation and seepage.
 //
 {
-    if ( Link[j].type == CONDUIT ) return conduit_getLossRate(j, q);
+    if ( Link[j].type == CONDUIT )
+        return conduit_getLossRate(j, routeModel, q, tstep);
     else return 0.0;
 }
 
@@ -1326,10 +1331,12 @@ double conduit_getInflow(int j)
 
 //=============================================================================
 
-double conduit_getLossRate(int j, double q)
+double conduit_getLossRate(int j, int routeModel, double q, double tstep)
 //
 //  Input:   j = link index
+//           routeModel = type of flow routing model
 //           q = current link flow rate (cfs)
+//           tstep = current routing time step (sec)
 //  Output:  returns rate of evaporation & seepage losses (ft3/sec)
 //  Purpose: computes volumetric rate of water evaporation & seepage
 //           from a conduit (per barrel).
@@ -1338,7 +1345,7 @@ double conduit_getLossRate(int j, double q)
     TXsect *xsect;
     double depth = 0.5 * (Link[j].oldDepth + Link[j].newDepth);
     double length;
-    double topWidth;
+    double width, topWidth;
     double evapLossRate = 0.0,
            seepLossRate = 0.0,
            totalLossRate = 0.0;
@@ -1359,19 +1366,25 @@ double conduit_getLossRate(int j, double q)
         if ( Link[j].seepRate > 0.0 )
         {
             // limit depth to depth at max width
-            if ( depth >= xsect->ywMax ) depth = xsect->ywMax;
+            if (xsect->type == RECT_CLOSED) width = xsect->wMax;
+            else
+            {
+                if ( depth >= xsect->ywMax ) depth = xsect->ywMax;
+                width = xsect_getWofY(xsect, depth);
+            }
 			
             // compute seepage loss rate across length of conduit
-            seepLossRate = Link[j].seepRate * xsect_getWofY(xsect, depth) *
-                           length;
+            seepLossRate = Link[j].seepRate * width * length;
             seepLossRate *= Adjust.hydconFactor;
         }
 
         // --- compute total loss rate
         totalLossRate = evapLossRate + seepLossRate;
 
-        // --- total loss rate cannot exceed flow rate
-        q = ABS(q);
+        // --- limit total loss rate to current volume for DW routing
+        //     or current link flow rate otherwise
+        if (routeModel == DW) q = Link[j].newVolume / tstep;
+        else q = ABS(q);        
         if (totalLossRate > q)
         {
             evapLossRate = evapLossRate * q / totalLossRate;
diff --git a/src/solver/project.c b/src/solver/project.c
index 55a0866c8..7d65c0f9d 100644
--- a/src/solver/project.c
+++ b/src/solver/project.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/21/22  (Build 5.2.2)
+//   Date:     07/17/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   Project management functions.
@@ -52,6 +52,10 @@
 //   Build 5.2.2:
 //   - Default number of threads changed from OMP max. number to 1
 //     to be consistent with User Manual.
+//   Build 5.2.4:
+//   - Default Inertial Damping changed from SOME to PARTIAL_DAMPING.
+//   - Default CourantFactor changed from 0 (fixed routing time step)
+//   - to 0.75 (variable time step)
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -850,12 +854,12 @@ void setDefaults()
    SurchargeMethod = EXTRAN;           // Use EXTRAN method for surcharging
    CrownCutoff     = 0.96;             // Fractional pipe crown cutoff 
    AllowPonding    = FALSE;            // No ponding at nodes
-   InertDamping    = SOME;             // Partial inertial damping
+   InertDamping    = PARTIAL_DAMPING;  // Partial inertial damping
    NormalFlowLtd   = BOTH;             // Default normal flow limitation
    ForceMainEqn    = H_W;              // Hazen-Williams eqn. for force mains
    LinkOffsets     = DEPTH_OFFSET;     // Use depth for link offsets
    LengtheningStep = 0;                // No lengthening of conduits
-   CourantFactor   = 0.0;              // No variable time step 
+   CourantFactor   = 0.75;             // Variable time step reduced to 75%
    MinSurfArea     = 0.0;              // Force use of default min. surface area
    MinSlope        = 0.0;              // No user supplied minimum conduit slope
    SkipSteadyState = FALSE;            // Do flow routing in steady state periods 
diff --git a/src/solver/street.c b/src/solver/street.c
index fd05451b7..0c55d9115 100644
--- a/src/solver/street.c
+++ b/src/solver/street.c
@@ -3,11 +3,14 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21  (Build 5.2.0)
+//   Date:     07/13/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   Street Cross-Section Functions
 //
+//   Build 5.2.4:
+//   - Fixed incorrect index used to retrieve street backing parameters
+//     in street_readParams.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -106,8 +109,8 @@ int street_readParams(char* tok[], int ntoks)
     // --- read street backing parameters
     if (ntoks > 8)
     {
-        if (!getDouble(tok[8], &x[8]) || x[k] < 0.0)
-            return error_setInpError(ERR_NUMBER, tok[k]);
+        if (!getDouble(tok[8], &x[8]) || x[8] < 0.0)
+            return error_setInpError(ERR_NUMBER, tok[8]);
         if (x[8] > 0.0)
         {
             if (ntoks < 11) return error_setInpError(ERR_ITEMS, "");
diff --git a/src/solver/surfqual.c b/src/solver/surfqual.c
index 858fcbee5..64593a012 100644
--- a/src/solver/surfqual.c
+++ b/src/solver/surfqual.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21  (Build 5.2.0)
+//   Date:     07/14/23  (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   Subcatchment water quality functions.
@@ -16,6 +16,10 @@
 //   - Support for separate accounting of LID drain flows included. 
 //   Build 5.1.014:
 //   - Fixed bug in computing effective BMP removal by LIDs.
+//   Build 5.2.4:
+//   - Set low runoff flow concentrations to zero before computing runoff
+//     mass loads rather than after so that they match wet weather mass
+//     inflows reported for conveyance system nodes. 
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -255,7 +259,7 @@ void  surfqual_getWashoff(int j, double runoff, double tStep)
     {
         // --- convert washoff load to a concentration
         cOut = 0.0;
-        if ( vOut1 > 0.0 ) cOut = OutflowLoad[p] / vOut1;
+        if ( vOut1 > 0.0 && hasOutflow ) cOut = OutflowLoad[p] / vOut1;
 
         // --- assign any difference between pre- and post-LID
         //     subcatchment outflow loads to BMP removal
@@ -281,7 +285,6 @@ void  surfqual_getWashoff(int j, double runoff, double tStep)
         }
         
         // --- save new washoff concentration
-        if ( !hasOutflow ) cOut = 0.0;
         Subcatch[j].newQual[p] = cOut / LperFT3;
         
         // OWA Addition --- update surface buildup loads (in lbs or kg)
diff --git a/src/solver/text.h b/src/solver/text.h
index 353296b8e..4bb46f059 100644
--- a/src/solver/text.h
+++ b/src/solver/text.h
@@ -3,7 +3,7 @@
 //
 //   Project: EPA SWMM5
 //   Version: 5.2
-//   Date:    12/22/22  (Build 5.2.3)
+//   Date:    07/15/23  (Build 5.2.4)
 //   Author:  L. Rossman
 //
 //   Text strings
@@ -25,7 +25,7 @@
 #define FMT03 " There are errors.\n"
 #define FMT04 " There are warnings.\n"
 #define FMT08 \
-  "\n  EPA STORM WATER MANAGEMENT MODEL - VERSION 5.2 (Build 5.2.3)"
+  "\n  EPA STORM WATER MANAGEMENT MODEL - VERSION 5.2 (Build 5.2.4)"
 #define FMT09 \
   "\n  ------------------------------------------------------------"
 #define FMT10 "\n"
diff --git a/src/solver/transect.c b/src/solver/transect.c
index 831ba017f..6ebd25558 100644
--- a/src/solver/transect.c
+++ b/src/solver/transect.c
@@ -3,7 +3,7 @@
 //
 //   Project:  EPA SWMM5
 //   Version:  5.2
-//   Date:     11/01/21   (Build 5.2.0)
+//   Date:     07/13/23   (Build 5.2.4)
 //   Author:   L. Rossman
 //
 //   Geometry processing for irregular cross-section transects.
@@ -12,6 +12,8 @@
 //   ==============
 //   Build 5.2.0:
 //   - Function added to create a transect for a Street cross-section.
+//   Build 5.2.4:
+//   - Corrected street transect points in transect_createStreetTransect.
 //-----------------------------------------------------------------------------
 #define _CRT_SECURE_NO_DEPRECATE
 
@@ -610,8 +612,8 @@ void  transect_createStreetTransect(TStreet* street)
     w2 = street->gutterWidth;
     w3 = street->width;
     w4 = w3 - w2;
-    y3 = street->gutterDepression;
-    y1 = street->curbHeight + y3;
+    y3 = street->gutterDepression + street->slope * w2;
+    y1 = street->curbHeight + street->gutterDepression;
     ymax = street->backSlope * street->backWidth + y1;
     y4 = y3 + street->slope * w4;
     ymax = MAX(ymax, y4);


Test patch:
diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 7ce6e299c..a23d17dc4 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -20,7 +20,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: [windows-2022, ubuntu-20.04, macos-13.4]
+        os: [windows-2022, ubuntu-20.04, macos-12]
         requirements: [requirements-swmm.txt]
         include:
           - os: windows-2022
@@ -58,9 +58,9 @@ jobs:
                 shell: bash
                 working-directory: ./ci-tools/linux
 
-          - os: macos-13.4
+          - os: macos-12
             sys_pkgs: brew install libomp #boost
-            boost_platform_version: 13.4
+            boost_platform_version: 12
             boost_toolsit: clang
             build_unit_test: make.zsh -t -g "Xcode"
             build_reg_test: make.zsh -g "Xcode"
@@ -94,7 +94,7 @@ jobs:
       - name: Clone ci-tools repo
         uses: actions/checkout@v3
         with:
-          repository: OpenWaterAnalytics/ci-tools
+          repository: pyswmm/ci-tools
           ref: master
           path: ci-tools
 
@@ -143,8 +143,8 @@ jobs:
 
       - name: Before reg test
         env:
-          NRTESTS_URL: https://github.com/OpenWaterAnalytics/swmm-nrtestsuite
-          BENCHMARK_TAG: v3.0.2
+          NRTESTS_URL: https://github.com/pyswmm/swmm-nrtestsuite
+          BENCHMARK_TAG: v3.1.0
         run: ./${{ matrix.before_reg_test }} ${{ env.BENCHMARK_TAG }}
 
       - name: Run reg test
diff --git a/tests/outfile/test_output.cpp b/tests/outfile/test_output.cpp
index 1d44400ad..2494bd3c1 100644
--- a/tests/outfile/test_output.cpp
+++ b/tests/outfile/test_output.cpp
@@ -268,6 +268,26 @@ BOOST_FIXTURE_TEST_CASE(test_getSubcatchSeries, Fixture) {
     BOOST_CHECK(check_cdd_float(test_vec, ref_vec, 3));
 }
 
+BOOST_FIXTURE_TEST_CASE(test_getSystemSeries, Fixture) {
+    error = SMO_getSystemSeries(p_handle, SMO_runoff_flow, 0, 10, &array,
+                                  &array_dim);
+    BOOST_REQUIRE(error == 0);
+
+    const int ref_dim            = 10;
+    float     ref_array[ref_dim] = {
+        0.0f, 6.216825f, 13.030855f, 24.252975f, 14.172027f, 4.1949716f,
+		0.322329f, 0.056010f, 0.024938f, 0.012474f};
+
+    std::vector<float> ref_vec;
+    ref_vec.assign(ref_array, ref_array + 10);
+
+    std::vector<float> test_vec;
+    test_vec.assign(array, array + array_dim);
+
+    BOOST_CHECK(check_cdd_float(test_vec, ref_vec, 3));
+}
+
+
 BOOST_FIXTURE_TEST_CASE(test_getSubcatchResult, Fixture) {
     error = SMO_getSubcatchResult(p_handle, 1, 1, &array, &array_dim);
     BOOST_REQUIRE(error == 0);