Rainfall as a triggering factor of an infinite slope mechanism#

© 2024 Daniel F. Ruiz, Exneyder A. Montoya-Araque y Universidad EAFIT.

This notebook can be interactively run in Google - Colab.

This notebook runs the model TRIGRS (Transient Rainfall Infiltration and Grid-Based Regional Slope-Stability Analysis), developed by Baum et al. [2002] and Baum et al. [2008], and parallelized later by Alvioli and Baum [2016]

Required modules and global setup for plots#

import os
import subprocess
import textwrap
import itertools
import requests

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
from IPython import get_ipython
from IPython.display import display

if 'google.colab' in str(get_ipython()):
    print('Running on CoLab. Installing the required modules...')
    # subprocess.run('pip install ipympl', shell=True);
    from google.colab import output, files
    output.enable_custom_widget_manager()
else:
    import tkinter as tk
    from tkinter.filedialog import askopenfilename

# Figures setup
# %matplotlib widget
mpl.rcParams.update({
    "font.family": "serif",
    "font.serif": ["Computer Modern Roman", "cmr", "cmr10", "DejaVu Serif"],  # or 
    "mathtext.fontset": "cm",  # Use Computer Modern fonts for math
    "axes.formatter.use_mathtext": True,  # Use mathtext for axis labels
    "axes.unicode_minus": False,   # Use standard minus sign instead of a unicode character
})

Creating directories#

# Create a folder called TRIGRS in the current working directory if it doesn't exist
workdir = os.getcwd()
exec_dir = os.path.join(workdir, "TRIGRS_1")
os.makedirs(f"{exec_dir}", exist_ok=True)  

# Create a folder inside TRIGRS to store the ascii files
inputs_dir = os.path.join(exec_dir, "inputs")
outputs_dir = os.path.join(exec_dir, "outputs")
os.makedirs(f"{inputs_dir}", exist_ok=True)
os.makedirs(f"{outputs_dir}", exist_ok=True)

Functions#

def tpx_in_maker(tpx_inputs):
    tpx_in_template = textwrap.dedent(
        f"""\
        TopoIndex 1.0.15; Name of project (up to 255 characters)
        TopoIndex analysis - Project: {tpx_inputs["proj_name"]}
        Flow-direction numbering scheme (ESRI=1, TopoIndex=2)
        {tpx_inputs["flow_dir_scheme"]}
        Exponent, Number of iterations
        {tpx_inputs["exponent_weight_fact"]}, {tpx_inputs["iterarions"]}
        Name of elevation grid file
        TRIGRS_1/inputs/dem.asc
        Name of direction grid
        TRIGRS_1/inputs/directions.asc
        Save listing of D8 downslope neighbor cells (TIdsneiList_XYZ)?  Enter T (.true.) or F (.false.)
        T
        Save grid of D8 downslope neighbor cells (***TIdscelGrid_XYZ)? Enter T (.true.) or F (.false.)
        T
        Save cell index number grid (TIcelindxGrid_XYZ)?  Enter T (.true.) or F (.false.)
        T
        Save list of cell number and corresponding index number (***TIcelindxList_XYZ)? Enter T (.true.) or F (.false.)
        T
        Save flow-direction grid remapped from ESRI to TopoIndex (TIflodirGrid_XYZ)? Enter T (.true.) or F (.false.)
        T
        Save grid of points on ridge crests (TIdsneiList_XYZ)? Enter T (.true.) or F (.false.); Sparse (T) or dense (F)?
        F,T
        ID code for output files? (8 characters or less)
        {tpx_inputs["proj_name"]}"""
    )
    with open("tpx_in.txt", "w") as file:
        file.write(tpx_in_template)
    return


def run_topoidx(tpx_inputs):
    tpx_in_maker(tpx_inputs)  # Generating initializing files
    subprocess.call(["chmod +x ./TRIGRS_1/Exe_TopoIndex"], shell=True)  # Change permission
    subprocess.call(["./TRIGRS_1/Exe_TopoIndex"])  # Running Fortran executable
    # Moving log file to outputs folder and renaming it
    os.rename(os.path.join(workdir, "TopoIndexLog.txt"), os.path.join(outputs_dir, f"TopoIndexLog_{tpx_inputs['proj_name']}.txt"))
    os.rename(os.path.join(workdir, "tpx_in.txt"), os.path.join(exec_dir, f"tpx_in_{tpx_inputs['proj_name']}.txt"))
    return


def tr_in_maker(trg_inputs):
    # Create text block for geotechnical parameters of each zone
    geoparams = ""
    for zone in trg_inputs["geoparams"].keys():
        z = trg_inputs["geoparams"][zone]
        # print(zone, z)
        txt_zone = "".join(
            [
                f"zone, {zone}\n",
                "cohesion, phi, uws, diffus, K-sat, Theta-sat, Theta-res, Alpha\n",
                f'{z["c"]}, {z["φ"]}, {z["γ_sat"]}, {z["D_sat"]}, {z["K_sat"]}, {z["θ_sat"]}, {z["θ_res"]}, {z["α"]}\n',
            ]
        )
        geoparams = str().join([geoparams, txt_zone])
    # Update rifil entry with placeholder for each file
    trg_inputs["rifil"] = str().join(
        [f"TRIGRS_1/inputs/ri{n}.asc\n" for n in range(1, len(trg_inputs["capt"]))]
    )

    # Template
    trg_in_template = (
        textwrap.dedent(
            f"""\
        Project: {trg_inputs['proj_name']}
        TRIGRS, version 2.1.00c, (meter-kilogram-second-degrees units)
        tx, nmax, mmax, zones
        {trg_inputs['tx']}, {trg_inputs['nmax']}, {trg_inputs['mmax']}, {trg_inputs['zones']}
        nzs, zmin, uww, nper t
        {trg_inputs['nzs']}, 0.001, 9.8e3, {trg_inputs['nper']}, {trg_inputs['t']}
        zmax, depth, rizero, Min_Slope_Angle (degrees), Max_Slope_Angle (degrees)
        {trg_inputs['zmax']}, {trg_inputs['depth']}, {trg_inputs['rizero']}, 0., 90.0
        """
        )
        + geoparams[:-1]
        + textwrap.dedent(
            f"""
        cri(1), cri(2), ..., cri(nper)
        {', '.join(map(str, trg_inputs['cri']))}
        capt(1), capt(2), ..., capt(n), capt(n+1)
        {', '.join(map(str, trg_inputs['capt']))}
        File name of slope angle grid (slofil)
        TRIGRS_1/inputs/slope.asc
        File name of digital elevation grid (elevfil)
        TRIGRS_1/inputs/dem.asc
        File name of property zone grid (zonfil)
        TRIGRS_1/inputs/zones.asc
        File name of depth grid (zfil)
        TRIGRS_1/inputs/zmax.asc
        File name of initial depth of water table grid   (depfil)
        TRIGRS_1/inputs/depthwt.asc
        File name of initial infiltration rate grid   (rizerofil)
        TRIGRS_1/inputs/rizero.asc
        List of file name(s) of rainfall intensity for each period, (rifil())
        """
        )
        + trg_inputs["rifil"][:-2]
        + textwrap.dedent(
            f"""
        File name of grid of D8 runoff receptor cell numbers (nxtfil)
        TRIGRS_1/inputs/TIdscelGrid_{trg_inputs['proj_name']}.txt
        File name of list of defining runoff computation order (ndxfil)
        TRIGRS_1/inputs/TIcelindxList_{trg_inputs['proj_name']}.txt
        File name of list of all runoff receptor cells  (dscfil)
        TRIGRS_1/inputs/TIdscelList_{trg_inputs['proj_name']}.txt
        File name of list of runoff weighting factors  (wffil)
        TRIGRS_1/inputs/TIwfactorList_{trg_inputs['proj_name']}.txt
        Folder where output grid files will be stored  (folder)
        TRIGRS_1/outputs/
        Identification code to be added to names of output files (suffix)
        {trg_inputs['proj_name']}
        Save grid files of runoff? Enter T (.true.) or F (.false.)
        F
        Save grid of minimum factor of safety? Enter T (.true.) or F (.false.)
        T
        Save grid of depth of minimum factor of safety? Enter T (.true.) or F (.false.)
        T
        Save grid of pressure head at depth of minimum factor of safety? Enter T (.true.) or F (.false.)
        T
        Save grid of computed water table depth or elevation? Enter T (.true.) or F (.false.) followed by 'depth,' or 'eleva'
        T, depth
        Save grid files of actual infiltration rate? Enter T (.true.) or F (.false.)
        F
        Save grid files of unsaturated zone basal flux? Enter T (.true.) or F (.false.)
        F
        Save listing of pressure head and factor of safety ("flag")? (-9 sparse xmdv , -8 down-sampled xmdv, -7 full xmdv, -6 sparse ijz, -5 down-sampled ijz, -4 full ijz, -3 Z-P-Fs-saturation list -2 detailed Z-P-Fs, -1 Z-P-Fs list, 0 none). Enter flag value followed by down-sampling interval (integer).
        {trg_inputs['ψ_flag']}, {trg_inputs['nzs']}
        Number of times to save output grids and (or) ijz/xmdv files
        {trg_inputs['n_outputs']}
        Times of output grids and (or) ijz/xmdv files
        {', '.join(map(str, trg_inputs['t_n_outputs']))}
        Skip other timesteps? Enter T (.true.) or F (.false.)
        F
        Use analytic solution for fillable porosity?  Enter T (.true.) or F (.false.)
        T
        Estimate positive pressure head in rising water table zone (i.e. in lower part of unsat zone)?  Enter T (.true.) or F (.false.)
        T
        Use psi0=-1/alpha? Enter T (.true.) or F (.false.) (False selects the default value, psi0=0)
        F
        Log mass balance results?   Enter T (.true.) or F (.false.)
        T
        Flow direction (Enter "gener", "slope", or "hydro")
        {trg_inputs['flowdir']}
        Add steady background flux to transient infiltration rate to prevent drying beyond the initial conditions during periods of zero infiltration?
        T
        Specify file extension for output grids. Enter T (.true.) for ".asc" or F for ".txt"
        T
        Ignore negative pressure head in computing factor of safety (saturated infiltration only)?   Enter T (.true.) or F (.false.)
        T
        Ignore height of capillary fringe in computing pressure head for unsaturated infiltration option?   Enter T (.true.) or F (.false.)
        T
        Parameters for deep pore-pressure estimate in SCOOPS ijz output: Depth below ground surface (positive, use negative value to cancel this option), pressure option (enter 'zero' , 'flow' , 'hydr' , or 'relh')
        -50.0,flow
        """
        )
    )
    # Create txt file
    with open("tr_in.txt", "w") as file:
        file.write(trg_in_template)
    return


def run_trigrs(trg_inputs):
    # Correcting sign for `Alpha` parameters for saturated/unsaturated models
    for z in trg_inputs["geoparams"].keys():
        zone = trg_inputs["geoparams"][z]
        if "S" in trg_inputs["model"]:
            zone["α"] = -1 * abs(zone["α"])
        else:
            zone["α"] = abs(zone["α"])

    # Correcting sign for `mmax` parameters for finite/infinite models
    if "I" in trg_inputs["model"]:
        trg_inputs["mmax"] = -1 * abs(trg_inputs["mmax"])
    else:
        trg_inputs["mmax"] = abs(trg_inputs["mmax"])

    # Generating initializing file and running TRIGRS executable
    tr_in_maker(trg_inputs)  # Initializing files from template
    if trg_inputs["parallel"]:
        exe = "./TRIGRS_1/Exe_TRIGRS_par"
        subprocess.call([f"chmod +x {exe}"], shell=True)  # Change permission
        subprocess.call(
            [f'mpirun -np {trg_inputs["NP"]} {exe}'], shell=True
        )  # Running TRIGRS executable
    else:
        exe = "./TRIGRS_1/Exe_TRIGRS_ser"
        subprocess.call([f"chmod +x {exe}"], shell=True)  # Change permission
        subprocess.call([exe], shell=True)  # Running TRIGRS executable

    # Moving files to output folders and renaming it
    proj_name, model = trg_inputs["proj_name"], trg_inputs["model"]
    # os.rename(
    #     "TrigrsLog.txt",
    #     f"TRIGRS_1/outputs/log_files/TrigrsLog_{proj_name}_{model}.txt",
    # )
    os.rename(os.path.join(workdir, "TrigrsLog.txt"), os.path.join(outputs_dir, f"TrigrsLog_{proj_name}_{model}.txt"))
    # os.rename("tr_in.txt", f"TRIGRS_1/tr_in_{proj_name}_{model}.txt")
    os.rename(os.path.join(workdir, "tr_in.txt"), os.path.join(exec_dir, f"tr_in_{proj_name}_{model}.txt"))
    groups = (
        "TRfs_min",
        "TRp_at_fs_min",
        "TRwater_depth",
        "TRz_at_fs_min",
        "TR_ijz_p_th",
        # "TRrunoffPer",
    )
    [os.makedirs(f"TRIGRS_1/outputs/{grp}", exist_ok=True) for grp in groups]
    idx = np.arange(1, trg_inputs["n_outputs"] + 1, 1)
    for grp, i in itertools.product(groups, idx):
        if grp == "TRrunoffPer":
            os.rename(
                f"TRIGRS_1/outputs/{grp}{i}{proj_name}.asc",
                f"TRIGRS_1/outputs/{grp}/{grp}_{proj_name}_{model}_{i}.asc",
            )
        elif grp == "TR_ijz_p_th":
            os.rename(
                f"TRIGRS_1/outputs/{grp}_{proj_name}_{i}.txt",
                f"TRIGRS_1/outputs/{grp}/{grp}_{proj_name}_{model}_{i}.txt",
            )
        else:
            os.rename(
                f"TRIGRS_1/outputs/{grp}_{proj_name}_{i}.asc",
                f"TRIGRS_1/outputs/{grp}/{grp}_{proj_name}_{model}_{i}.asc",
            )
    return


def get_param(zone, param):
    return trg_inputs['geoparams'][zone.iloc[0]][param]

def get_df(file):
    γ_w = 9.8e3  # [N/m3] unit weight of water
    # Reading file
    df = pd.read_csv(file,
                     names=['i_tr', 'j_tr', 'elev', 'ψ', 'θ'],
                     skiprows=7, sep='\s+')#delim_whitespace=True)
    # Correcting indices to match numpy notation
    df['i'] = df['j_tr'].max() - df['j_tr']
    df['j'] = df['i_tr'] - 1
    df.drop(columns=['i_tr', 'j_tr'], inplace=True)
    # Calculating depths from elevations
    elev_max = df.groupby(['i', 'j'])['elev'].transform('max')
    df['z'] = elev_max - df['elev'] + 0.001
    df = df[['i', 'j', 'elev', 'z', 'ψ', 'θ']]
    # Assigning zones and geotechnical parameters
    df['zone'] = zones[df['i'], df['j']]
    for param_name in trg_inputs['geoparams'][1].keys():
        df[param_name] = df.groupby('zone')['zone'].transform(
            get_param, param=param_name)
    # Correting rows with ψ==0 (watertable) due to numerical errors
    mask = (df['ψ'] == 0) & (df['z'] > 0.001) & (df['θ'] == df['θ_sat'])
    # df.loc[mask, 'z'] = df.loc[mask, 'z'] + 1/df.loc[mask, 'α']
    # df.loc[mask, 'elev'] = df.loc[mask, 'elev'] - 1/df.loc[mask, 'α']
    # df.sort_values(['i', 'j', 'z'], inplace=True)
    df.drop(df[mask].index, inplace=True)  # This eliminates the wt rows
    # Assigning slope
    df['δ'] = slope[df['i'], df['j']]
    δrad = np.deg2rad(df['δ'])
    # Calculating effective stress parameter, χ
    df['χ'] = (df['θ'] - df['θ_res']) / (df['θ_sat'] - df['θ_res'])
    # Specific gravity
    df['Gs'] = (df['γ_sat']/γ_w - df['θ_sat']) / (1 - df['θ_sat'])
    # unit weight at unsat and sat zones
    df['γ_nat'] = (df['Gs'] * (1 - df['θ_sat']) + df['θ']) * γ_w
    # average unit weight
    γ_cumul = df.groupby(['i', 'j'])['γ_nat'].transform('cumsum')
    idx = df.groupby(['i', 'j'])['γ_nat'].cumcount() + 1
    df['γ_avg'] = γ_cumul / idx
    # Factor of safety
    fs1 = np.tan(np.deg2rad(df['φ'])) / np.tan(δrad)
    fs2_num = df['c'] - df['ψ'] * γ_w * df['χ'] * np.tan(np.deg2rad(df['φ']))
    fs2_den = df['γ_avg'] * df['z'] * np.sin(δrad) * np.cos(δrad)
    fs = fs1 + fs2_num/fs2_den
    fs[fs > 3] = 3
    df['fs'] = fs
    return df

Loading executables and input files#

def download_file_from_github(file, url_path, output_path, make_executable=False):
    os.makedirs(output_path, exist_ok=True)
    destination = os.path.join(output_path, file)

    # ✅ Check if file already exists
    if os.path.exists(destination):
        print(f"🟡 File already exists at: {destination} — skipping download.")
        return

    url = f"{url_path.rstrip('/')}/{file}"

    try:
        print(f"⬇️ Downloading from: {url}")
        subprocess.run(["wget", "-q", "-O", destination, url], check=True)

        if make_executable:
            os.chmod(destination, 0o755)
            print(f"🔧 File marked as executable.")

        print(f"✅ File downloaded to: {destination}")
    except subprocess.CalledProcessError as e:
        print(f"❌ Download failed: {e}")


url_path = "https://raw.githubusercontent.com/eamontoyaa/data4testing/main/trigrs/"  # URL of the raw file on GitHub
output_path = "./TRIGRS_1/"  # Path where you want to save the downloaded file

TopoIndex#

file = "Exe_TopoIndex"  # Name of the file to download
download_file_from_github(file, url_path, output_path)
⬇️ Downloading from: https://raw.githubusercontent.com/eamontoyaa/data4testing/main/trigrs/Exe_TopoIndex
✅ File downloaded to: ./TRIGRS_1/Exe_TopoIndex

TRIGRS#

file = "Exe_TRIGRS_ser"  # Name of the file to download
download_file_from_github(file, url_path, output_path)
⬇️ Downloading from: https://raw.githubusercontent.com/eamontoyaa/data4testing/main/trigrs/Exe_TRIGRS_ser
✅ File downloaded to: ./TRIGRS_1/Exe_TRIGRS_ser
# Check the files in the executables folder
os.listdir(exec_dir)
['Exe_TRIGRS_ser', 'outputs', 'inputs', 'Exe_TopoIndex']

Running the TRIGRS model for a one-cell spatial domain#

General inputs#

proj_name = "ONE_CELL"
slope = 40  # [°]
zmax = 1.0  # [m]
depth_wt = 1.0  # [m]
model = "UF"  # Choose one among UF, UI, SF, SI → | U: unsaturated | S: saturated | F: finite | I: infinite |
rainfall_int =  [5.0] # [mm/h]  This is a vector-like structure; it may contain one or more values separated by commas
cumul_duration = [0, 24] # [h]  Always starts at 0 and contains n+1 elements, where n is the number of rainfall intensities values
output_times = [0, 3, 6, 9, 12, 15, 18, 21, 24] # [h]  It does not have to include the same values as cumul_duration

Units conversion#

# Do not edit this cell!
rainfall_int =  np.array(rainfall_int) / 36e5  # [mm/h] to [m/s]
cumul_duration = np.array(cumul_duration) * 3600  # [h] to [s]
output_times = np.array(output_times) * 3600  # [h] to [s]

TRIGRS inputs#

Do not edit those parameters with the → 🛇 ← symbol.

trg_inputs = {
    "proj_name": proj_name,  # Name of the project  → 🛇 ←
    "model": model,  # S-saturated or U-unsaturated + F-finite or I-infinite   → 🛇 ←
    "tx": 20, # [] tx*nper determines how many time steps are used in the computations  → 🛇 ←
    "nmax": 30,  # [] Ma number of roots 𝚲n and terms in series solutions for UNS.zone  → 🛇 ←
    "mmax": 100,  # [] Max number of terms in series solution for FIN depth. If <0: INF depth  → 🛇 ←
    "zones": 1,  # [] Number of zones  → 🛇 ←
    "nzs": 20,  # [] Vertical increments until zmax  → 🛇 ←
    "nper": len(rainfall_int),  # [-] Periods of rainfall (N)  → 🛇 ←
    "t": cumul_duration[-1],  # [s] Elapsed time since the start of the storm (t)  → 🛇 ←
    "zmax": zmax + .01,  # [m] Max. depth to compute Fs and Ψ (if <0 read from grid file)  → 🛇 ←
    "depth": depth_wt + 0.01,  # [m] Water table depth (d) (if <0 read from grid file)  → 🛇 ←
    "rizero": 1.5e-7,  # [m/s] Steady pre-storm and long-term infilt. (Izlt) (if <0 read from grid file)
    "geoparams":{
        1: {
            "c": 3e3,  # [Pa] soil cohesion for effective stress (c')
            "φ": 33.6,  # [deg] soil friction angle for effective stress (φ')
            "γ_sat": 20.0e3,  # [N/m3]  unit weight of soil (γs)
            "D_sat": 5.0e-5,  # [m2/s] Saturated hydraulic diffusivity (D0)
            "K_sat": 1.0e-5,  # [m/s] Saturated hydraulic conductivity (Ks)
            "θ_sat": 0.41,  # [-] Saturated water content (θs)
            "θ_res": 0.06,  # [-] Residual water content (θr)
            "α": 3,  # [1/m] Fitting parameter. If <0: saturated infiltration
        }
    },
    "cri": tuple(rainfall_int),  # [m/s] Rainfall intensities  (Inz)  → 🛇 ←
    "capt": tuple(cumul_duration),  # [s] Cumulative duration of rainfall cri[i]  → 🛇 ←
    "flowdir": "gener",  # Method for maximum allowed Ψ ("gener", "slope", or "hydro"))  → 🛇 ←
    "n_outputs": len(output_times),  # [-] No of raster outputs at different t  → 🛇 ←
    "t_n_outputs": tuple(output_times),#t,  # [s] Time at which to save raster files  → 🛇 ←
    "ψ_flag": -4,  # → 🛇 ←
    "parallel": False,  # [-] Run in parallel  → 🛇 ←
    "NP" : 4, # Number of processors  → 🛇 ←
    # "inz_n_outputs": inz_n_outputs #inz,  # [s] Rainfall intensities at which to save raster files
}

Creating .asc files#

for file in ['dem', 'zmax', 'depthwt', 'slope', 'directions', 'zones']:
    with open(f"{inputs_dir}/{file}.asc", "w") as asc:
        asc.write("ncols         1\n")
        asc.write("nrows         1\n")
        asc.write("xllcorner     0\n")
        asc.write("yllcorner     0\n")
        asc.write("cellsize      1\n")
        asc.write("NODATA_value  -9999\n")
        asc.write("0\n")

def mod_asc(file, new_val):
    with open(f'{inputs_dir}/{file}.asc', 'r') as asc:
        lines = asc.readlines()
    with open(f'{inputs_dir}/{file}.asc', 'w') as asc:
        lines[6] = str(new_val) + '\n'
        asc.writelines(lines)
    return

# Set the values for the ascii input files
mod_asc('dem', 0)
mod_asc('zmax', zmax)
mod_asc('depthwt', depth_wt)
mod_asc('slope', slope)
mod_asc('directions', 1)
mod_asc('zones', 1)

Runnig TopoIndex and TRIGRS#

# Running TopoIndex
tpx_inputs = {
    "flow_dir_scheme": 2,
    "exponent_weight_fact": -1,
    "iterarions": 10,
    "proj_name": proj_name,
}
run_topoidx(tpx_inputs)
   TopoIndex: Topographic Indexing and
  flow distribution factors for routing
  runoff through Digital Elevation Models
             By Rex L. Baum
        U.S. Geological Survey
      Version 1.0.14, 11May2015
 -----------------------------------------
 
 TopoIndex analysis - Project: ONE_CELL
           1  = number of data cells
           1  = total number of cells
 Reading elevation grid data
 Initial elevation indexing completed
 Finding D8 neighbor cells
 nxtcel() nodata (integer,floating)=        -9999  -9999.00000    
 Identifying downslope cells and grid mismatches
 No grid mismatch found!
 Correcting cell index numbers
 Computing weighting factors
 Saving results to disk
run_trigrs(trg_inputs)
 
 TRIGRS: Transient Rainfall Infiltration
 and Grid-based Regional Slope-Stability
                Analysis
        Version 2.1.00c, 02 Feb 2022
   By Rex L. Baum and William Z. Savage
        U.S. Geological Survey
 -----------------------------------------
 
 Opening default initialization file
 TRIGRS, version 2.1.00c, (meter-kilogram-second-degrees units)                                                                                                                                                                                                 
 Unsaturated infiltration model selected for cells in zone           1 .
 ******** Zone            1  *********
 Using unsaturated infiltration model.
 ********  ********  ********  *********
 TRIGRS_1/inputs/TIgrid_size.txt F
 TRIGRS_1/inputs/GMgrid_size.txt F
 TRIGRS_1/inputs/TRgrid_size.txt F
           1  = number of data cells
           1  = total number of cells
 Reading input grids
 One property zone, no grid required!
 Testing and adjusting steady infiltration rates
 Adjusted steady infiltration rate at            0  cells
 Skipped runoff-routing computations;
 Runoff routing input data did not exist.
 Initial size of time-steps    4320.0000000000000     
 outp_incr_min    10800.0000    
 Adjusted size of time-steps, tx    4320.0000000000000               20
 ******** Output times ********
 number, timestep #,  time
           1           1   0.00000000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           2           3   8640.00000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           3           6   21600.0000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           4           8   30240.0000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           5          11   43200.0000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           6          13   51840.0000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           7          16   64800.0000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           8          18   73440.0000    
 One or more specified output times unavailable, 
 Nearest available time substituted.
           9          21   86400.0000    
 Starting computations of pressure head and factor of safety
 Calling unsaturated finite-depth model
 Starting coupled saturated & unsaturated zone
 computations for finite-depth saturated zone
 Cells completed: 
           0  
           1  cells completed
 Saving results
 TRIGRS finished!
STOP 0

Output to dataframe#

zones = np.loadtxt(f'{inputs_dir}/zones.asc', skiprows=6)
zones = zones.reshape((1, 1)) if zones.ndim == 0 else zones
slope = np.loadtxt(f'{inputs_dir}/slope.asc', skiprows=6)
slope = slope.reshape((1, 1)) if slope.ndim == 0 else slope
trg_inputs['geoparams'][zones[0, 0]]
{'c': 3000.0,
 'φ': 33.6,
 'γ_sat': 20000.0,
 'D_sat': 5e-05,
 'K_sat': 1e-05,
 'θ_sat': 0.41,
 'θ_res': 0.06,
 'α': 3}
def get_param(zone, param):
    return trg_inputs['geoparams'][zone.iloc[0]][param]

def get_df(file):
    γ_w = 9.8e3  # [N/m3] unit weight of water
    # Reading file
    df = pd.read_csv(file, names=['i_tr', 'j_tr', 'elev', 'ψ', 'θ'], skiprows=7, sep='\s+')
    # Correcting indices to match numpy notation
    df['i'] = df['j_tr'].max() - df['j_tr']
    df['j'] = df['i_tr'] - 1
    df.drop(columns=['i_tr', 'j_tr'], inplace=True)
    # Calculating depths from elevations
    elev_max = df.groupby(['i', 'j'])['elev'].transform('max')
    df['z'] = elev_max - df['elev'] + 0.001
    df = df[['i', 'j', 'elev', 'z', 'ψ', 'θ']]
    # Assigning zones and geotechnical parameters
    df['zone'] = zones[df['i'], df['j']]
    for param_name in trg_inputs['geoparams'][1].keys():
        df[param_name] = df.groupby('zone')['zone'].transform(get_param, param=param_name)
    # Correting rows with ψ==0 (watertable) due to numerical errors
    mask = (df['ψ'] == 0) & (df['z'] > 0.001) & (df['θ'] == df['θ_sat'])
    # df.loc[mask, 'z'] = df.loc[mask, 'z'] + 1/df.loc[mask, 'α']
    # df.loc[mask, 'elev'] = df.loc[mask, 'elev'] - 1/df.loc[mask, 'α']
    # df.sort_values(['i', 'j', 'z'], inplace=True)
    df.drop(df[mask].index, inplace=True)  # This eliminates the wt rows
    # Assigning slope
    df['δ'] = slope[df['i'], df['j']]
    δrad = np.deg2rad(df['δ'])
    # Calculating effective stress parameter, χ
    df['χ'] = (df['θ'] - df['θ_res']) / (df['θ_sat'] - df['θ_res'])
    # Specific gravity
    df['Gs'] = (df['γ_sat']/γ_w - df['θ_sat']) / (1 - df['θ_sat'])
    # unit weight at unsat and sat zones
    df['γ_nat'] = (df['Gs'] * (1 - df['θ_sat']) + df['θ']) * γ_w
    # average unit weight
    γ_cumul = df.groupby(['i', 'j'])['γ_nat'].transform('cumsum')
    idx = df.groupby(['i', 'j'])['γ_nat'].cumcount() + 1
    df['γ_avg'] = γ_cumul / idx
    # Factor of safety
    fs1 = np.tan(np.deg2rad(df['φ'])) / np.tan(δrad)
    fs2_num = df['c'] - df['ψ'] * γ_w * df['χ'] * np.tan(np.deg2rad(df['φ']))
    fs2_den = df['γ_avg'] * df['z'] * np.sin(δrad) * np.cos(δrad)
    fs = fs1 + fs2_num/fs2_den
    fs[fs > 3] = 3
    df['fs'] = fs
    return df

def read_time(file):
    with open(file, 'r') as file:
        lines = file.readlines()
        time = float(lines[4].split()[-1])
    return time

Verification of FS and head pressure at a point#

t_verification = 4

time = read_time(f'{outputs_dir}/TR_ijz_p_th/TR_ijz_p_th_{proj_name}_{model}_{t_verification}.txt')
print(f'Time: {time} s, {time/3600} h')
Time: 30240.0 s, 8.4 h
cell = (0, 0)  # (i, j) indices of the unique cell in the domain

file = f'{outputs_dir}/TR_ijz_p_th/TR_ijz_p_th_{proj_name}_{model}_{t_verification}.txt'
df = get_df(file)
cell_df = df[(df['i']==0) & (df['j']==0)]
cell_df
i j elev z ψ θ zone c φ γ_sat ... K_sat θ_sat θ_res α δ χ Gs γ_nat γ_avg fs
0 0 0 -0.00100 0.00100 -0.53910 0.1955 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.387143 2.764095 17897.90 17897.900000 3.000000
1 0 0 -0.05145 0.05145 -0.52050 0.2000 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.400000 2.764095 17942.00 17919.950000 3.000000
2 0 0 -0.10190 0.10190 -0.50090 0.2049 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.414000 2.764095 17990.02 17943.306667 3.000000
3 0 0 -0.15235 0.15235 -0.48030 0.2103 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.429429 2.764095 18042.94 17968.215000 3.000000
4 0 0 -0.20280 0.20280 -0.45870 0.2161 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.446000 2.764095 18099.78 17994.528000 3.000000
5 0 0 -0.25325 0.25325 -0.43610 0.2224 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.464000 2.764095 18161.52 18022.360000 2.712909
6 0 0 -0.30370 0.30370 -0.41260 0.2293 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.483714 2.764095 18229.14 18051.900000 2.384476
7 0 0 -0.35415 0.35415 -0.38810 0.2368 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.505143 2.764095 18302.64 18083.242500 2.147928
8 0 0 -0.40460 0.40460 -0.36260 0.2448 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.528000 2.764095 18381.04 18116.331111 1.968377
9 0 0 -0.45505 0.45505 -0.33630 0.2536 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.553143 2.764095 18467.28 18151.426000 1.827216
11 0 0 -0.50550 0.50550 -0.30920 0.2631 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.580286 2.764095 18560.38 18188.603636 1.712486
12 0 0 -0.55595 0.55595 -0.28120 0.2733 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.609429 2.764095 18660.34 18227.915000 1.616624
13 0 0 -0.60640 0.60640 -0.25250 0.2844 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.641143 2.764095 18769.12 18269.546154 1.534961
14 0 0 -0.65685 0.65685 -0.22300 0.2964 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.675429 2.764095 18886.72 18313.630000 1.463844
15 0 0 -0.70730 0.70730 -0.19290 0.3092 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.712000 2.764095 19012.16 18360.198667 1.400807
16 0 0 -0.75775 0.75775 -0.16210 0.3231 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.751714 2.764095 19148.38 18409.460000 1.344054
17 0 0 -0.80820 0.80820 -0.13070 0.3381 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 0.794571 2.764095 19295.38 18461.572941 1.292165
18 0 0 -0.85865 0.85865 0.01583 0.4100 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 1.000000 2.764095 20000.00 18547.041111 1.161224
19 0 0 -0.90910 0.90910 0.04468 0.4100 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 1.000000 2.764095 20000.00 18623.512632 1.116758
20 0 0 -0.95955 0.95955 0.07353 0.4100 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 1.000000 2.764095 20000.00 18692.337000 1.077270
21 0 0 -1.01000 1.01000 0.10240 0.4100 1.0 3000.0 33.6 20000.0 ... 0.00001 0.41 0.06 3 40.0 1.000000 2.764095 20000.00 18754.606667 1.041956

21 rows × 21 columns

Plotting results at a point#

clrs = ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6', '#4E96BC',
        '#549EB3', '#59A5A9', '#60AB9E', '#69B190', '#77B77D', '#8CBC68',
        '#A6BE54', '#BEBC48', '#D1B541', '#DDAA3C', '#E49C39', '#E78C35', 
        '#E67932', '#E4632D', '#DF4828', '#DA2222']
cmap = mpl.colors.LinearSegmentedColormap.from_list('cmap_name', clrs)

palette = mpl.colormaps['gnuplot_r']
palette = mpl.colormaps['Spectral']
palette = cmap
# cmap
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(7, 4), sharey=True, layout='constrained')

for ax in axs:
    ax.grid(True, color='0.3', ls='--', lw=0.7)
    ax.spines["top"].set_linewidth(1.5)
    ax.spines["left"].set_linewidth(1.5)
    ax.xaxis.set_ticks_position('top')
    ax.xaxis.set_tick_params(top=True, bottom=False)
    ax.xaxis.set_label_position('top')
    ax.set_prop_cycle('color', palette(np.linspace(0, 1, len(output_times))))

axs[0].axvline(0, color='k', lw=1.5, ls='--')
axs[1].axvline(1, color='k', lw=1.5, ls='--')

for n in np.arange(len(output_times)) + 1:
    file = f'TRIGRS_1/outputs/TR_ijz_p_th/TR_ijz_p_th_{proj_name}_{model}_{n}.txt'
    t = read_time(file)
    df = get_df(file)
    df_cell = df[(df['i']==cell[0]) & (df['j']==cell[1])]
    axs[0].plot(df_cell['ψ'], df_cell['z'], label=f'$t={t/3600:.0f}$ h')
    axs[1].plot(df_cell['fs'], df_cell['z'], label=f'$t={t/3600:.0f}$ h')
    axs[0].set(xlabel='Pressure head, $\\psi$ [m]', ylabel='$Z$ [m]')
    axs[1].set(xlabel='FS')
    
axs[1].invert_yaxis()
handles, labels = axs[0].get_legend_handles_labels()
fig.legend(handles, labels, loc='outside right center')
fig.canvas.header_visible = False
fig.canvas.toolbar_position = 'bottom'
plt.show()
../../_images/b326bb4dd13e1d589b4899df549db0a0df3da7d8b327380dffcd3236ba6f116e.png