# -*- coding: utf-8 -*-
"""
* Copyright (C) 2023-2025 Alexandre Gauvain, Ronan Abhervé, Jean-Raynald de Dreuzy
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
"""
#%% LIBRAIRIES
# Python
import os
import sys
import flopy
import flopy.utils.binaryfile as fpu
import numpy as np
from os.path import dirname, abspath
import random
import warnings
import pickle
import geopandas as gpd
import rasterio
import flopy.utils.postprocessing as pp
import whitebox
from scipy.optimize import curve_fit
import matplotlib.pyplot as plt
wbt = whitebox.WhiteboxTools()
wbt.verbose = False
# Root
df = dirname(dirname(abspath(__file__)))
sys.path.append(df)
# HydroModPy
from hydromodpy.tools import toolbox, get_logger
logger = get_logger(__name__)
fontprop = toolbox.plot_params(8,15,18,20) # small, medium, interm, large
#%% CLASS
[docs]
class Modpath:
"""
Class Modpath.
To build, run particle traccking from modflow simulation.
"""
def __init__(self,
geographic: object,
model_modflow: object,
# Worflow settings
model_folder: str='HydroModPy_outputs',
model_name: str='Default_modpath',
bin_path: str=os.path.join(os.getcwd(),'bin'),
# Specific settings
zone_partic: str='domain',
track_dir: str='forward',
bore_depth: list=None,
cell_div: int=1,
zloc_div: bool=False,
sel_random: int=None,
sel_slice: int=None):
"""
Initialize method.
Parameters
----------
geographic : object
Geographic object build by HydroModPy.
model_modflow : object
Python object of the MODFLOW model.
model_folder : str, optional
Name of the folder. The default is 'HydroModPy_outputs'.
model_name : str, optional
Name of the model. The default is 'Default'.
bin_path : str, optional
Location folder of the modflow executables. The default is 'bin'.
zone_partic : str, optional
Path of the raster used to inject particles: where value > 0.
The default is 'domain', so the particles are injected where the model domain area > 0m.
track_dir: str
Choice 'forward' or 'backward' particle tracking method.
The default is 'forward'.
bore_depth: list
[Not stable, currently in development]
If not None, inject a particle in the z direction (vertical), at the center position of each lays.
cell_div: int
Fix the number of particles injected uniformly distributed for each cell.
If 3 is set, 9 particles will be inejcted (3x*3y)
The dault is 1.
zloc_div: bool
If True, 'cell_div' is also applied vetically for the cells.
If cell_div is 3 and zloc_div is True, 18 particles will be injected (3x*3y*3z).
The default is False.
sel_random: int
Select randomly where inject a total number of particles.
sel_random: int
Select with slicing value where particles.
"""
#%% Initialisation
self.geographic = geographic
self.model_modflow = model_modflow
self.model_name = model_name
self.model_folder = model_folder
self.full_path = os.path.join(model_folder, model_name)
if not os.path.isdir(self.full_path):
raise FileNotFoundError('Directory not found: {}'.format(self.full_path))
if (sys.platform == 'win32') or (sys.platform == 'win64'):
self.exe = os.path.join(bin_path, 'win' ,'mp6.exe')
if (sys.platform == 'linux'):
self.exe = os.path.join(bin_path, 'linux' ,'mp6')
if (sys.platform == 'darwin'):
self.exe = os.path.join(bin_path, 'mac' ,'mp6')
# Parameters for particles
if zone_partic == 'domain':
self.zone_partic = geographic.watershed_box_buff_dem
else:
self.zone_partic = zone_partic
self.track_dir = track_dir
self.bore_depth = bore_depth
self.cell_div = cell_div
self.zloc_div = zloc_div
self.sel_random = sel_random
self.sel_slice = sel_slice
#%% PRE-PROCESSING
[docs]
def pre_processing(self):
"""
Pre-processing to build the partickle tracking.
Returns
-------
None.
"""
#%% Load and import
prefix = os.path.join(self.full_path, self.model_name)
nam_file = '{}.nam'.format(prefix)
dis_file = '{}.dis'.format(prefix)
head_file = '{}.hds'.format(prefix)
bud_file = '{}.cbc'.format(prefix)
bas_file = '{}.bas'.format(prefix)
lpf_file = '{}.upw'.format(prefix)
# ---- flopy.modflow.Modflow.load
self.mf = flopy.modflow.Modflow.load(
nam_file,
model_ws=self.full_path,
verbose=False,
check=False,
exe_name=getattr(self.model_modflow, "exe", None) or "mfnwt",
)
# Avoid re-loading packages already attached to the model (prevents flopy duplicate warnings)
bas = self.mf.get_package('BAS6')
if bas is None:
bas = flopy.modflow.ModflowBas.load(bas_file, self.mf)
lpf = self.mf.get_package('UPW')
if lpf is None:
lpf = flopy.modflow.ModflowUpw.load(lpf_file, self.mf, check=False)
nlay = self.mf.nlay
ncol = self.mf.ncol
nrow = self.mf.nrow
laytype = lpf.laytyp.array
iboundData = bas.ibound.array
# ---- flopy.modpath.Modpath6
self.mp = flopy.modpath.Modpath6(modelname=self.mf.name,
model_ws=self.full_path,
simfile_ext='mpsim',
namefile_ext='mpnam',
version='modpath',
exe_name=self.exe,
modflowmodel=self.mf,
head_file=head_file,
dis_file=dis_file,
dis_unit=87,
budget_file=bud_file)
self.mp.array_free_format = True
cbb = fpu.CellBudgetFile(bud_file)
# cbb.list_records()
rec_drn = cbb.get_data(kstpkper=(0, 0), text='DRAINS')
rec_rch = cbb.get_data(kstpkper=(0, 0), text='RECHARGE')
self.mp.dis_file = dis_file
self.mp.head_file = head_file
self.mp.budget_file = bud_file
#%% Specific parametrization
if self.track_dir=='forward':
track = 1
zone_opt = 1
zone_inj = 1
if self.bore_depth==None:
drn = np.ones((nrow, ncol))
compti = 0
comptj = 0
for ii in range(0, rec_drn[0].shape[0]):
drn[compti, comptj] = -1 * rec_drn[0][ii][1]
comptj += 1
if comptj == ncol:
compti += 1
comptj = 0
rch = rec_rch[0][1]
b = drn / rch
b[np.isnan(b)]=0
szone = []
for i in range(0, nlay):
a = np.zeros((nrow, ncol), dtype=int)
if i == 0:
a[b >= 1] = 1
a[iboundData[i] == -1] = 1
szone.append(a)
zone_opt = 2
zone_inj = szone.copy()
if self.track_dir=='backward':
track = 2
zone_opt = 1
zone_inj = 1
flags = option_flags=[2, # SimulationType : 1 = Endpoint simulation; 2 = Pathline simulation; 3 = Timeseries simulation
track, # TrackingDirection : 1 = Forward tracking; 2 = Backward tracking
1, # WeakSinkOption : 1 = Allow particles to pass through cells that contain weak sinks; 2 = Stop particles when they enter cells that contain weak sinks.
1, # WeakSourceOption : 1 = Allow particles to pass through cells that contain weak sources; 2 = Stop particles when they enter cells that contain weak sources.
1, # ReferenceTimeOption : 1 = Specify a value for reference time; 2 = Specify a stress period, time step, and relative time position within the time step to use to compute the reference time.
2, # StopOption : 1 = For forward tracking simulations, stop at the end of the MODFLOW simulation. For backward tracking simulations, stop at the beginning of the MODFLOW simulation. 2 = Extend the initial or final steady-state MODFLOW time step as far as necessary to track all particles through to their termination points. For forward tracking simulations, this option would have an effect whenever the final MODFLOW stress period is steady-state. For backward tracking simulations, this option would have an effect whenever the first MODFLOW stress period is steady-state. If all MODFLOW stress periods are transient, option 2 produces the same result as option 1. 3 = Specify a value of tracking time at which to stop the particle-tracking computation.
2, # ParticleGenerationOption : 1 = Specify information to automatically generate particles for a collection of cells. 2 = Read particle locations from a starting locations file.
1, # TimePointOption : 1 = Time points are not specified. 2 = A specified number of time points are calculated for a fixed time increment. 3 = An array of time point values is specified.
1, # BudgetOutputOption : 1 = No budget checking 2 = A summary of cell-by-cell budgets is printed in the Listing File 3 = A list of cells is specified for which detailed budget information is summarized in the Listing File 4 = Trace mode is in effect
zone_opt, # ZoneArrayOption : 1 = No zone data are read. 2 = Zone data are read.
1, # RetardationOption : 1 = Retardataion factors are not read or used in the velocity calculations. 2 = An array of retardation factors is read and used in the velocity calculations.
1] # AdvectiveObservationsOption : 1 = Advective observations are not computed or saved. 2 = Advective observations are computed and saved for all time points. 3 = Advective observations are computed and saved only for the final time point.
logger.debug('Modpath settings - track: %s, zone_opt: %s, zone_inj: %s', track, zone_opt, type(zone_inj))
# ---- flopy.modpath.Modpath6
flopy.modpath.Modpath6Sim(model=self.mp, option_flags=flags,
group_placement=[[1, 1, 1, 0, 1, 1]], stop_zone=1, zone=zone_inj) # szone
with rasterio.open(self.zone_partic) as src:
mask_dem = src.read(1)
# ---- flopy.modpath.mp6sim.StartingLocationsFile
stl = flopy.modpath.mp6sim.StartingLocationsFile(model=self.mp, inputstyle=1)
prow = self.cell_div
pcol = self.cell_div
# if self.zloc_div == True:
# play = self.cell_div
# else:
# play = 1
if self.bore_depth != None:
# play = len(self.bore_depth)
play = nlay
else:
play = 1
stldata = stl.get_empty_starting_locations_data(npt=np.sum(mask_dem>0)*prow*pcol*play)
hds_1c = fpu.HeadFile(head_file)
# head_1c = hds_1c.get_alldata(mflay=None)
head_1c = hds_1c.get_data(totim=1)
wt = pp.get_water_table(head_1c, -100) # -9999
# wt = np.ones((nrow, ncol)) * wt
if self.track_dir == 'forward':
compt = 0
for i in range(0, nrow):
for j in range(0, ncol):
if mask_dem[i,j] > 0: # active or note
for r in range(prow):
for c in range(pcol):
for l in range(play):
stldata[compt]['label'] = 'p' + str(compt+1) + '-' + str(r) + '-' + str(c)
for k in range(0, nlay):
if (wt[i, j] > self.mf.dis.botm.array[k, i, j]):
stldata[compt]['k0'] = k
break
# Calculate the starting location for each sub-cell
stldata[compt]['j0'] = j
stldata[compt]['i0'] = i
# stldata[compt]['xloc0'] = (r +1) * 1/(prow +1)
# stldata[compt]['yloc0'] = (c +1) * 1/(pcol +1)
# stldata[compt]['xloc0'] = (r+0.1)/(prow+0.2) # old method
# stldata[compt]['yloc0'] = (c+0.1)/(pcol+0.2) # old method
stldata[compt]['xloc0'] = (r+0.5)/(prow) # new method
stldata[compt]['yloc0'] = (c+0.5)/(pcol) # new method
if k == 0:
ztop = self.mf.dis.top.array[i,j]
else:
ztop = self.mf.dis.botm.array[k-1, i, j]
zbot = self.mf.dis.botm.array[k, i, j]
thickness = ztop - zbot
if thickness <= 0:
aux_stl = 0.0
else:
# Normalize the water level between 0 and 1 using the local cell thickness
aux_stl = min(max((wt[i, j] - zbot) / thickness, 0.0), 1.0)
# ==> min((wt[i, j] - zbot)/(ztop - zbot), 1.)
val_z_wt = np.abs(aux_stl)
# if l == 0:
stldata[compt]['zloc0'] = val_z_wt
# else:
# stldata[compt]['zloc0'] = 0
compt = compt + 1
if self.track_dir == 'backward':
compt = 0
for i in range(0, nrow):
for j in range(0, ncol):
if mask_dem[i,j] > 0: # active or note
for r in range(prow):
for c in range(pcol):
for l in range(play):
stldata[compt]['label'] = 'p' + str(compt+1) + '-' + str(r) + '-' + str(c)
# for k in range(0, nlay):
# if (wt[i, j] > self.mf.dis.botm.array[k, i, j]):
# stldata[compt]['k0'] = k
# break
# Calculate the starting location for each sub-cell
stldata[compt]['j0'] = j
stldata[compt]['i0'] = i
# stldata[compt]['xloc0'] = (r +1) * 1/(prow +1)
# stldata[compt]['yloc0'] = (c +1) * 1/(pcol +1)
# stldata[compt]['xloc0'] = (r+0.1)/(prow+0.2) # old method
# stldata[compt]['yloc0'] = (c+0.1)/(pcol+0.2) # old method
stldata[compt]['xloc0'] = (r+0.5)/(prow) # new method
stldata[compt]['yloc0'] = (c+0.5)/(pcol) # new method
# stldata[compt]['xloc0'] = 0.5
# stldata[compt]['yloc0'] = 0.5
stldata[compt]['zloc0'] = 0.5
if self.bore_depth == True:
# z0 not exist at this step: need to find the good k (layer) to inject at different depth (create a loop)
# For example: stldata[compt]['z0'] = self.mf.dis.top.array[i,j] - self.bore_depth[l]
stldata[compt]['k0'] = l
else:
stldata[compt]['k0'] = 0
compt = compt + 1
#%% Select random particles to inject
# Random
if self.sel_random != None:
if self.sel_random >= len(stldata):
val_random = len(stldata) - 1
else:
val_random = self.sel_random
self.point_data = np.random.choice(stldata, val_random)
self.point_data = self.point_data.view(np.recarray)
self.point_data = self.point_data[np.argsort(self.point_data['particleid'])] # do not work for pathlines, bug sometimes
else:
self.point_data = stldata
# Slicing
if self.sel_slice != None:
self.point_data = stldata[::self.sel_slice]
#%% Finalize settings
stl.data = self.point_data
self.poro_modpath = self.model_modflow.sy
self.ss_modpath = self.model_modflow.ss
# ---- flflopy.modpath.Modpath6Basopy.modpath.mp6sim.StartingLocationsFile
flopy.modpath.Modpath6Bas(self.mp,
hnoflo=-9999,
hdry=-100,
# def_iface=[6, 6],
def_face_ct=0, # ifaces = [6] # top face:6 ; bottom face:5 ; row face:3-4 ; column face:1-2
laytyp=laytype,
ibound=iboundData,
prsity=self.poro_modpath,
prsityCB=self.ss_modpath,
extension='mpbas',
unitnumber=86)
# 1 gauche face Ouest (x– direction)
# 2 droite face Est (x+ direction)
# 3 avant face Sud (y– direction)
# 4 arrière face Nord (y+ direction)
# 5 bas face inférieure (z– direction)
# 6 haut face supérieure (z+ direction)
#%% PROCESSING
[docs]
def processing(self,
write_model:bool=True,
run_model:bool=False):
"""
Run the partickle tracking.
Parameters
----------
write_model : bool, optional
Flag to write input files or not. The default is True.
run_model : bool, optional
Flag to run model or not. The default is False.
Returns
-------
success_model : bool
Flag to know if the simulation finished correctly.
"""
# Create modflow files
if write_model == True:
self.mp.write_input()
# Run modflow files
success_model = False
if run_model == True:
verbose = True
success_model, tempo = self.mp.run_model(silent=not verbose) # True without msg
return success_model
#%% POST-PROCESSING
[docs]
def post_processing(self,
model_modpath:object,
starting_point:bool = True,
ending_point:bool = True,
pathlines_shp:bool = True,
particles_shp:bool = True,
random_id = None):
"""
Create outputs files.
Parameters
----------
model_modpath : object
MODPATH python object.
ending_point : bool, optional
Write ending point files. The default is True.
starting_point : bool, optional
Write starting point files. The default is True.
pathlines_shp : bool, optional
Write pathlines shapefiles. The default is True.
particles_shp : bool, optional
Write particles shapefiles. The default is True.
random_id : int, optional
Export random pathlines. The default is None.
"""
# The outputs to create
self.starting_point = starting_point
self.ending_point = ending_point
self.pathlines_shp = pathlines_shp
self.particles_shp = particles_shp
# Path and load
self.full_path = os.path.join(model_modpath.model_folder, model_modpath.model_name)
self.particles_file = os.path.join(self.full_path, '_postprocess', '_particles')
toolbox.create_folder(self.particles_file)
grid_model = model_modpath.mf.modelgrid
crs = model_modpath.geographic.crs_proj
if isinstance(crs, (int, float)):
epsg = crs
elif isinstance(crs, str) and crs[:4].upper() == 'EPSG':
epsg = int(crs.split(':')[-1])
else:
epsg = None
crs_for_write = crs if crs is not None else (f"EPSG:{epsg}" if epsg is not None else None)
def ensure_crs(gdf):
"""Attach CRS when missing to avoid warnings and mismatches."""
if gdf.crs is None and crs_for_write is not None:
return gdf.set_crs(crs_for_write, allow_override=True)
return gdf
# Import mpend file
path_mpend = os.path.join(model_modpath.model_folder, model_modpath.model_name, model_modpath.model_name)
# ---- flopy.utils.EndpointFile
endobj = flopy.utils.EndpointFile(path_mpend+'.mpend')
e = endobj.get_alldata()
# Create ending point file
if ending_point == True:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Truncating shapefile fieldname.*")
endobj.write_shapefile(endpoint_data=e,
shpname=os.path.join(self.particles_file, 'ending.shp'),
direction='ending',
mg=grid_model,
crs=crs_for_write)
# Create starting point file
if starting_point == True:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Truncating shapefile fieldname.*")
endobj.write_shapefile(endpoint_data=e,
shpname=os.path.join(self.particles_file, 'starting.shp'),
direction='starting',
mg=grid_model,
crs=crs_for_write)
# Import mppth file
if (pathlines_shp == True) or (particles_shp == True):
path_mppth = os.path.join(model_modpath.model_folder, model_modpath.model_name, model_modpath.model_name)
# ---- flopy.utils.PathlineFile
pthobj = flopy.utils.PathlineFile(path_mppth+'.mppth')
pth_data = pthobj.get_alldata()
if random_id != None:
shp_endpoint = gpd.read_file(os.path.join(self.particles_file, 'ending.shp'))
keep_id = shp_endpoint.particleid
keep_id = keep_id.tolist()
# if not os.path.exists(self.particles_file+'/_random_id.data'):
id_random_particles = random.sample(keep_id[:-1], random_id)
with open(self.particles_file+'/_random_id.data', 'wb') as f:
pickle.dump(id_random_particles, f)
pth_data_save = []
for o, i in enumerate(id_random_particles):
logger.debug('Processing random particle %d/%d (id: %s)', o, len(id_random_particles), i)
for j in pth_data:
if i == j.particleid[0]:
pth_data_save.append(j)
else:
pth_data_save = pth_data
# Create pathlines file
if pathlines_shp == True:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Truncating shapefile fieldname.*")
pthobj.write_shapefile(pathline_data=pth_data_save,
shpname=os.path.join(self.particles_file, 'pathlines.shp'),
one_per_particle=True,
direction='ending',
mg=grid_model,
crs=crs_for_write,
verbose=False)
# Create particles file
if particles_shp == True:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message="Truncating shapefile fieldname.*")
pthobj.write_shapefile(pathline_data=pth_data_save,
shpname=os.path.join(self.particles_file, 'particles.shp'),
one_per_particle=False,
direction='ending',
mg=grid_model,
crs=crs_for_write,
verbose=False)
[docs]
def filt_processing(self,
model_modpath:object,
norm_flux: bool=False, # weight time by fluxes (recharge)
filt_time: bool=True, # delete particles with time at 0, add a column with time divided by 365 (considering recharge in days)
filt_seep: bool=True, # only forward, keep only particles finishing in zone1 (seepage), keep only particles finishing in k1 (first layer)
filt_inout: bool=True, # delete particles in and out in the same cell (first layer)
calc_rtd: bool=True, # compute residence time distribution
random_id: int=None # select randomly to keep
):
# Convert days in years
def update_time(df, filt_time):
if filt_time == True:
df['time_y'] = df['time'] / 365 # convert in years
try:
df['time_win_y'] = df['time_win'] / 365 # convert in years
except:
pass
df = df[df['time']>0]
return df
# Keep particles ending in seepage and not in/out in the same cell
def update_locout(df, filt_seep, filt_inout):
if filt_seep == True:
if self.track_dir == 'forward':
df = df[df['k']<=1] # out in first layer
df = df[df['zone']==1] # out in seepage zone
if filt_inout == True:
df = df[df.i0.astype(str)+'-'+df.j0.astype(str)!=
df.i.astype(str)+'-'+df.j.astype(str)] # NOT IN AND OUT FOR SAME CELL
keep_particles = df['particleid']
return df, keep_particles
# Paths
self.full_path = os.path.join(model_modpath.model_folder, model_modpath.model_name)
self.particles_file = os.path.join(self.full_path, '_postprocess', '_particles')
crs = model_modpath.geographic.crs_proj
if isinstance(crs, (int, float)):
epsg = crs
elif isinstance(crs, str) and crs[:4].upper() == 'EPSG':
epsg = int(crs.split(':')[-1])
else:
epsg = None
crs_for_write = crs if crs is not None else (f"EPSG:{epsg}" if epsg is not None else None)
def ensure_crs(gdf):
"""Attach CRS when missing to avoid warnings and mismatches."""
if gdf.crs is None and crs_for_write is not None:
return gdf.set_crs(crs_for_write, allow_override=True)
return gdf
# Create a new shapefile named '_weighted'
if norm_flux == True:
modeldir = self.full_path+'/'
namepath = model_modpath.model_name
model_name = model_modpath.model_name
mymodel = model_modpath.mf
aux_rech = mymodel.get_package('RCH')
mybas = mymodel.get_package('BAS6')
mydis = mymodel.get_package('DIS')
ncol = np.unique(mydis.ncol)[0]
nrow = np.unique(mydis.nrow)[0]
nlay = np.unique(mydis.nlay)[0]
dcol = np.unique(mydis.delc)[0]
drow = np.unique(mydis.delr)[0]
period = 0
step = 0
Qx, Qy, Qz_rech = pp.get_extended_budget(modeldir+namepath+'.cbc', precision='single', idx=None,
kstpkper=(step, period), totim=None,boundary_ifaces={'RECHARGE': 6}, hdsfile=modeldir+namepath+'.hds',
model=mymodel)
Qx_2, Qy_2, Qz_drain = pp.get_extended_budget(modeldir+namepath+'.cbc', precision='single', idx=None,
kstpkper=(step, period), totim=None,boundary_ifaces={'DRAINS': 6}, hdsfile=modeldir+namepath+'.hds',
model=mymodel)
recharge_raw = aux_rech.rech.array[0,0]
recharge_list = recharge_raw.flatten()
recharge_matrix = recharge_raw * dcol * drow
drain_matrix = Qz_drain[0,:,:]
sflux = recharge_matrix - drain_matrix
sflows = sflux/drow/dcol
toolbox.export_tif(self.geographic.watershed_box_buff_dem,
sflows,
self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'sflows_weighted.tif',
-9999)
wbt.extract_raster_values_at_points(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'sflows_weighted.tif',
self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'starting.shp',
out_text=False)
wbt.extract_raster_values_at_points(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'sflows_weighted.tif',
self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'ending.shp',
out_text=False)
start = gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'starting.shp')
start = ensure_crs(start)
start_weighted = start.copy()
start_weighted.to_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'starting_weighted.shp')
end = gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'ending.shp')
end = ensure_crs(end)
end_weighted = end.copy()
end_weighted.to_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'ending_weighted.shp')
recharge_list = np.ones(len(end))*recharge_raw.mean()
start_process = ensure_crs(gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'starting_weighted.shp'))
end_process = ensure_crs(gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'ending_weighted.shp'))
if self.track_dir == 'forward':
end_process['VALUE1_in'] = start_weighted['VALUE1']
end_process['rchPerc'] = end_process['VALUE1_in'] / recharge_list
end_process.loc[end_process['rchPerc']<0, 'rchPerc'] = 0
time_win = (end_process['time'])*end_process['rchPerc']
if self.track_dir == 'backward':
start_process['VALUE1_in'] = end_weighted['VALUE1']
start_process['rchPerc'] = start_process['VALUE1_in'] / recharge_list
start_process.loc[start_process['rchPerc']<0, 'rchPerc'] = 0
time_win = (start_process['time'])*start_process['rchPerc']
start_process['time_win'] = time_win
end_process['time_win'] = time_win
end_up = update_time(end_process, filt_time)
end_up, keep_particles = update_locout(end_up, filt_seep, filt_inout)
end_up = ensure_crs(end_up)
end_up.to_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'ending_weighted.shp')
start_up = update_time(start_process, filt_time)
start_up, keep_particles = update_locout(start_up, filt_seep, filt_inout)
start_up = ensure_crs(start_up)
start_up.to_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'starting_weighted.shp')
if self.pathlines_shp == True:
pathlines_process = ensure_crs(gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'pathlines.shp'))
if self.track_dir == 'forward':
pathlines_process['time_win'] = (end_process['time'])*end_process['rchPerc']
if self.track_dir == 'backward':
pathlines_process['time_win'] = (start_process['time'])*start_process['rchPerc']
pathlines_up = update_time(pathlines_process, filt_time)
pathlines_up = pathlines_up[pathlines_up['particleid'].isin(keep_particles)]
if random_id != None:
if not os.path.exists(self.model_folder+'/'+'_id_particles_random.data'):
id_particles_random = random.sample(pathlines_up[:-1], random_id)
with open(self.model_folder+'/'+'_id_particles_random.data', 'wb') as f:
pickle.dump(id_particles_random, f)
else:
with open(self.model_folder+'/'+'_id_particles_random.data', 'rb') as f:
id_particles_random = pickle.load(f)
pathlines_up = pathlines_up[pathlines_up['particleid'].isin(id_particles_random)]
pathlines_up = ensure_crs(pathlines_up)
pathlines_up.to_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'pathlines_weighted.shp')
if self.particles_shp == True:
particles_process = ensure_crs(gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'particles.shp'))
particles_up = update_time(particles_process, filt_time)
if random_id != None:
if not os.path.exists(self.model_folder+'/'+'_id_particles_random.data'):
id_particles_random = random.sample(particles_up[:-1], random_id)
with open(self.model_folder+'/'+'_id_particles_random.data', 'wb') as f:
pickle.dump(id_particles_random, f)
else:
with open(self.model_folder+'/'+'_id_particles_random.data', 'rb') as f:
id_particles_random = pickle.load(f)
particles_up = particles_up[particles_up['particleid'].isin(id_particles_random)]
particles_up = ensure_crs(particles_up)
particles_up.to_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'particles_weighted.shp')
#%% PLOT
if calc_rtd == True:
if self.track_dir == 'forward':
end = ensure_crs(gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'ending_weighted.shp'))
if self.track_dir == 'backward':
end = ensure_crs(gpd.read_file(self.model_folder+'/'+model_name+'/'+'_postprocess/_particles/'+'starting_weighted.shp'))
try:
shp = gpd.read_file(self.geographic.watershed_shp)
end = end.clip(shp)
except:
pass
end.loc[end['time_win']==0, :] = np.nan
end = end.dropna()
try:
tau = np.average(end['time_win'], weights=end['rchPerc'])
def pdf_function(M, nbin, Weight):
bin_min = np.quantile(M, 0.01)
bin_max = np.quantile(M, 0.99)
bins = np.logspace(np.log10(bin_min),np.log10(bin_max), nbin)
pdf, binEdges = np.histogram(M, bins=bins,density=True, weights=Weight)
dx = np.diff(binEdges)
xh = (binEdges[1:] + binEdges[:-1])/2
xh = np.array(xh)
return (xh, pdf)
nbin = int(2*len(end['time_win'])**(2/5)) #Scott's Rules
[xh, yh] = pdf_function(end['time_win']/tau, nbin, end.rchPerc)
idzeros = np.where(yh != 0)
xfil = xh[idzeros]
yfil = yh[idzeros]
x_log = np.log10(xfil)
y_log = np.log10(yfil)
# x_log = (xfil)
# y_log = (yfil)
except:
pass
def func(x, a, b, c, d, e):
return a * x**4 + b * x**3 + c * x**2 + d * x + e
try:
params, covariance = curve_fit(func, x_log, y_log)
a, b, c, d, e = params
x_fit = np.linspace(min(x_log), max(x_log), 100)
y_fit = func(x_fit, a, b, c, d, e)
fig = plt.figure(figsize=(6,4))
ax = fig.add_subplot(111)
ax.plot(xfil, yfil, '-', lw=2, c='red', label='Binning on particles')
ax.plot(xh, yh, marker='o', markeredgecolor='none', lw=0, c='red')
ax.plot(10**x_fit, 10**y_fit, '-', lw=2, c='k', label='Fitting curve')
ax.set_ylabel("PDF")
ax.set_xlabel("t / "+r'$\tau$')
ax.set_xscale('log')
ax.set_title('Residence times distribution')
ax.legend(loc='upper right')
# ax.set_xlim(tmin, tmax)
# ax.set_ylim(-0.1, 13)
except:
pass
#%% NOTES
# logger.debug('Point data: %s', self.point_data)
# if sorted(self.point_data['particleid']) == list(self.point_data['particleid']):
# logger.debug("list1 is sorted")
# else:
# logger.debug("list is not sorted")