Functions.Feflow_Utils

View Source
__docformat__ = "google"
# build doc : pdoc -o D:\Dropbox\Run4_transitoire\Documentation D:\Dropbox\Run4_transitoire\Functions
# add project folder path to Anaconda\Lib\site-packages in python38.pth 

"""
    This module holds the code to prepare data and interact with Feflow through the official ifm38 library
"""


import gc
import time
from os import chdir, listdir, remove
import os.path
from os.path import isfile, join

import pandas as pd
import numpy as np

# Path to the FeFlow binaries has to bet set before importing ifm38
#chdir(r'C:\\Program Files\\DHI\\2021\\FEFLOW 7.4\\bin64')

#   To import ifm38 from anaconda prompt, it's needed to add in the folder
#   Anaconda\Lib\site-packages a file named python38.pth with the direct path
#   to feflow, in my case : C:\\Program Files\\DHI\\2021\\FEFLOW 7.4\\bin64
#import ifm38 as ifm


""" ------------------------- Defining Feflow Callbacks Function to act on simulator ------------------------- """


def preEnterSimulator(doc):

    """
    Assigning parameters to Feflow before launching any simulation. Parameters are passed
    by global variables has defined in Feflow documentation. In this code, we send permKx,
    permKz and recharge issued from geostatistical simulations. 

    Args:
        doc (PyFeflowDoc): Feflow Document imported with ifm.loadDocument(fem_file)

    Raises:
        ValueError: Wrong number of elements in _PERM_KX
        ValueError: Wrong number of elements in _PERM_KY
        ValueError: Wrong number of elements in _PERM_KZ
        ValueError: Wrong number of recharge in _RECHARGE
        ValueError: Wrong number of nodes while importing steady heads
    """
    
    if (_ELEMENT_NUMBER!=len(_PERM_KX)):
        raise ValueError("Wrong number of elements in permKx")

    # To modify when switching to full anisotropic k fields
    if (_ELEMENT_NUMBER!=len(_PERM_KX)):
        raise ValueError("Wrong number of elements in permKy")

    if (_ELEMENT_NUMBER!=len(_PERM_KZ)):
        raise ValueError("Wrong number of elements in permKz")

    #if (len(doc.getParamValues(ifm.Enum.P_IOFLOW))!=len(_RECHARGE)):
    #    raise ValueError("Wrong number of recharge zones")

    # In this case, Kx = Ky, _PERM_KX is assigned to P_CONDX and P_CONDY

    doc.setParamValues(ifm.Enum.P_CONDX,_PERM_KX,0)
    doc.setParamValues(ifm.Enum.P_CONDY,_PERM_KX,0)
    doc.setParamValues(ifm.Enum.P_CONDZ,_PERM_KZ,0)
    
    #doc.setParamValues(ifm.Enum.P_IOFLOW,_RECHARGE,0)
    doc.setParamValues(ifm.Enum.P_COMP,_SS,0)

    # In transient case, we want to set hydraulic head simulated in steady state simulation
    # as initial hydraulic heads to speed up simulation time.

    if _TRANSIENT == True :
        
        #steady_heads = np.load(_PATH_SAVE+"/temp/steady_heads_"+str(_MODEL_NUMBER)+".npy")
        steady_heads = _TEMP_HEAD 
        
        if (doc.getNumberOfNodes()!=len(steady_heads)):
            raise ValueError("Wrong number of nodes while importing steady heads")


        #doc.setParamValues(ifm.Enum.P_HEAD,steady_heads.tolist(),0) 
        doc.setParamValues(ifm.Enum.P_HEAD,steady_heads,0) 

        del steady_heads
        

def postTimeStep(doc):

    """
    Assigning parameters to Feflow before launching any simulation. Parameters are passed
    by global variables has defined in Feflow documentation. In this code, we send permKx,
    permKz and recharge issued from geostatistical simulations. 

    Args:
        doc (PyFeflowDoc): Feflow Document imported with ifm.loadDocument(fem_file)
    """

    # In steady state we stored the hydraulic heads simulated at each nodes to use them
    # as initial hydraulic head in transient simulation
    if _STEADY == True :
        global _TEMP_HEAD
        steady_heads = doc.getParamValues(ifm.Enum.P_HEAD)

        #np.save(_PATH_SAVE+"/temp/steady_heads_"+str(_MODEL_NUMBER),steady_heads)
        _TEMP_HEAD = steady_heads

        array_sum = np.sum(steady_heads)
        array_has_nan = np.isnan(array_sum)

        if np.max(steady_heads) > 100 or np.min(steady_heads) < 1 or array_has_nan :
            global _STEADY_CONVERGENCE
            _STEADY_CONVERGENCE = False
    

        del steady_heads


        global _STEADY_HEAD
        temp_heads = np.zeros(len(_WELL))

        for well in range(len(_WELL)) :
            temp_heads[well] = doc.getResultsFlowHeadValueAtXYZ(_WELL[well][0], _WELL[well][1], _WELL[well][2])

        _STEADY_HEAD.append(temp_heads)



    else :

        # well + 1 to store an additionnal columns with time of simulation
        temp_heads = np.zeros(len(_WELL)+1)


        _time = doc.getAbsoluteSimulationTime()

        print("Time step :"+str(np.round(_time,1))+"d, Total ellapsed time : "+str(int(time.time()-_TIME))+"s")

        global _HEAD
        for well in range(len(_WELL)) :
            temp_heads[0] = _time
            temp_heads[well+1] = doc.getResultsFlowHeadValueAtXYZ(_WELL[well][0], _WELL[well][1], _WELL[well][2])
        _HEAD.append(temp_heads)

        
        # Put a treshold to delete non converging simulations exceeding a given time (mean converging time on succesfull simulation + 200 sec buffer) 
        if (_time < 3 and (time.time()-_TIME)> 550) or (_time < 6 and (time.time()-_TIME)> 800) or (_time < 11 and (time.time()-_TIME)> 1000) or (_time < 20 and (time.time()-_TIME)> 1200) or (time.time()-_TIME> 1600) :

            doc.stopSimulator()
            doc.closeDocument()
            global _TRANSIENT_CONVERGENCE
            _TRANSIENT_CONVERGENCE = False
        

def check_launch_feflow(path_data: str,
                    fem_steady_model: str,
                    fem_transient_model: str,
                    assim_number: int,
                    model_number: int) -> None:

    """    
    To avoid redoing lengthy simulation on already done one, we check the list of simulation to do
    versus the one already done. We also checked the existence of folders.

    Args:
        path_data (str): Abolute path to the Data folder contained in the project
        fem_steady_model (str): Abolute path to the Feflow steady state model
        fem_transient_model (str): Abolute path to the Feflow transient state model
        assim_number (int): Actual assimilation step in the multiple data assimilation process
        model_number (int): Number of the model to be run in the actual simulation
        
    Raises:
        ValueError: Individual_Update folder missing in Data/Assim_X/ folder
        ValueError: Head folder missing in Data/Assim_X/ folder
    """

    str_model_number = str(model_number)
    str_assim_number = str(assim_number)

    temp_filename = "assim_"+str_assim_number+"_model_"+str_model_number+".npy"

    path = path_data + "Assim_"+str_assim_number+"/Individual_Update/"
    path_already_done = path_data + "Assim_"+str_assim_number+"/Head/"

    if not os.path.isdir(path):
        raise ValueError("Individual_Update folder missing in Assim_"+str_assim_number+"/Individual_Update/")

    if not os.path.isdir(path_already_done):
        raise ValueError("Head folder missing in Assim_"+str_assim_number+"/Head/")


    files = [f for f in listdir(path) if isfile(join(path, f))]
    files_already_done = [f for f in listdir(path_already_done) if isfile(join(path_already_done, f))]

    if "head_"+str(model_number)+".npy" in files_already_done :
        print("Loading succesfull, permeability field "+str_model_number+" already run")

    else :

        temp_filename = "assim_"+str_assim_number+"_model_"+str_model_number+".npy"

        if temp_filename in files :

            print("Loading succesfull, permeability field "+str_model_number+" being run")

            launch_feflow(path_data,
                                fem_steady_model,
                                fem_transient_model,
                                assim_number,
                                model_number)

        else :
            print("/!\ /!\ Loading unsuccesfull, permeability field assim_"+str_assim_number+"_model_"+\
                str_model_number+".npy is missing.\n Could be normal in case \
            of dropped non converging model /!\ /!\\")



def launch_feflow(path_data: str,
                    fem_steady_model: str,
                    fem_transient_model: str,
                    assim_number: int, 
                    model: int) -> None:
    """
    Functions to assign parameters to Feflow and run the simulation directly from Python.
    A lot of internal global variable are defined in accordance with Feflow documentation.
    They follow a naming scheme to minimize risk of interference with other python file :
    _NAME_OF_INTERNAL_GLOBAL.

    First step is to assign parameters to the steady state Feflow model and running it. Hydraulic 
    heads results are then stored as initial condition from the transient model run afterward. 

    Args:
        path_data (str): Abolute path to the Data folder contained in the project
        fem_steady_model (str): Abolute path to the Feflow steady state model
        fem_transient_model (str): Abolute path to the Feflow transient state model
        assim_number (int): Actual assimilation step in the multiple data assimilation process
        model_number (int): Number of the model to be run in the actual simulation

    Raises:
        ValueError: Mising steady state Feflow model, check the name or file
        ValueError: Mising transient state Feflow model, check the name or file
    """

    
    if not os.path.isfile(fem_steady_model):
        raise ValueError("Mising steady state Feflow model, check the name or file")

    if not os.path.isfile(fem_transient_model):
        raise ValueError("Mising transient state Feflow model, check the name or file")

    

    global _ELEMENT_NUMBER
    global _K_ZONE
    global _RECHARGE_ZONE
    global _UPDATED_ELEMENT
    global _PERM_KX
    global _PERM_KZ
    global _RECHARGE
    global _SS
    global _PATH
    global _PATH_SAVE
    global _FEM_FILE
    global _STEADY
    global _TRANSIENT
    global _HEAD
    global _STEADY_HEAD
    global _WELL
    global _TIME
    global _MODEL_NUMBER
    global _STEADY_CONVERGENCE
    global _TRANSIENT_CONVERGENCE
    global _TEMP_HEAD

    _MODEL_NUMBER = model

    _TIME = time.time()

    # Loading all relevant files, Feflow model, updated parameters, and the kZone and recharge zone
    # linking the global parameters to the element number in the Feflow model.
    _FEM_FILE = fem_steady_model
    doc = ifm.loadDocument(_FEM_FILE)

    _ELEMENT_NUMBER = doc.getNumberOfElements()
    

    _UPDATED_ELEMENT = np.load(path_data+"/FEFLOW_Model/updated_element.npy")
    
    # Elements number count starts at 1 in feflow, check the consitency with input updated_element.npy indexing
    _UPDATED_ELEMENT = (_UPDATED_ELEMENT - 1).astype(int)

    _PATH = path_data+"Assim_"+str(assim_number)+"/Individual_Update/"
    _PATH_SAVE = path_data+"Assim_"+str(assim_number)+"/"

    # _HEAD is fullfilled in the postTimeStep() function called by Feflow
    _HEAD = []
    _STEADY_HEAD = []  
    _TEMP_HEAD = []

    # Coordinates of all observations point used in the model for assimilation 
    # and calibration purpose. They are monitored at each time of simulation.
    _WELL = pd.read_csv(path_data+"/FEFLOW_Model/well_location.inp")

    _WELL = np.vstack((_WELL.X.values-doc.getOriginX(),
                       _WELL.Y.values-doc.getOriginY(),
                       _WELL.Z.values-doc.getOriginZ())).T

    # updated_parameter are coming from the assimilation process, in this model, all data except 
    # the last 47 are local element, with a value for each cell in the model. They are called 
    # local_updated_parameter for this reason. 
    # The last 47 parameters assimilated are global parameters, influencing several cells in 
    # Feflow model through kZones. They are treated separately to bring back kZones to elements.
    # On the 45 parameters, the first 15 are related to kx, the next 15 to kr (ratio of kz to kx),
    # the last 15 are saturation coefficient in different surface zones.


    elementsCoordinates = pd.read_pickle(path_data+"/FEFLOW_Model/feflow_element")
    
    _K_ZONE = elementsCoordinates.K_zone.values

    parameter_name = np.unique(elementsCoordinates.K_zone.values)


    updated_parameter = np.load(path_data+"Assim_"+str(assim_number)+"/Individual_Update/assim_"+\
            str(assim_number)+"_model_"+str(model)+".npy")

    global_parameters = len(updated_parameter) - _ELEMENT_NUMBER 
    
    local_updated_parameter = updated_parameter[:-global_parameters]
    global_updated_parameters = updated_parameter[-global_parameters:]

    kx = np.array(doc.getParamValues(ifm.Enum.P_CONDX))

    """
    We will let the kx value set outside of the local update

    # First we applied permeability coming from global variable, by applying global 
    # value to all elements, local elements will replace these global values where
    # they are defined.
    i = 0
    for name in parameter_name :
        # Feflow takes permeabilty as m/d, in case of m/s, multiplying by 3600 * 24.
        kx[np.where(_K_ZONE==name)] = global_updated_parameters[i] *3600*24
        i +=1


    """

    kx[_UPDATED_ELEMENT] = local_updated_parameter[_UPDATED_ELEMENT] 

    ### ky = kx, no need to add another vector in memory

    kz_temp = np.zeros(_ELEMENT_NUMBER)
    kz = np.array(doc.getParamValues(ifm.Enum.P_CONDZ))

    # First we applied permeability coming from global variable, by applying global 
    # value to all elements, local elements will replace these global values where
    # they are defined.
    i = 15
    for name in parameter_name :
        # Same logic, but kz is defined as a ratio of conductivity to kx. 
        kz_temp[np.where(_K_ZONE==name)] = kx[np.where(_K_ZONE==name)] / global_updated_parameters[i]
        i += 1

    kz[_UPDATED_ELEMENT] = kz_temp[_UPDATED_ELEMENT] 

    ss_temp = np.zeros(_ELEMENT_NUMBER)
    ss = np.array(doc.getParamValues(ifm.Enum.P_COMP))
    i = 30
    for name in parameter_name :
        ss_temp[np.where(_K_ZONE==name)] =  global_updated_parameters[i]
        i += 1

    ss[_UPDATED_ELEMENT] = ss_temp[_UPDATED_ELEMENT] 

    # Feflow is unable to deal with direct numpy array, he needs list
    _PERM_KX = kx.tolist()
    _PERM_KZ = kz.tolist()
    _SS = ss.tolist()

    # To reduce memory impact in case of big Feflow model, we free memory as much as possible
    del _K_ZONE
    del _UPDATED_ELEMENT
    del kx
    del kz
    del kz_temp
    del ss
    del ss_temp
    #del kr
    del updated_parameter
    del local_updated_parameter
    del global_updated_parameters
    del parameter_name

    
    # We need to specify in which kind of simulation we are
    _STEADY = True
    _TRANSIENT = False
    _STEADY_CONVERGENCE = True

    doc.startSimulator()
    doc.stopSimulator()

    doc.saveDocument(fem_steady_model[:-4]+"_temp.fem")

    doc.closeDocument()

    print("Steady simulation "+str(model)+" done in "+str(int(time.time()-_TIME))+"s")

    temp_time = _TIME
    # Reset timer for transient simulation
    _TIME = time.time()

    if _STEADY_CONVERGENCE :
        print("Model has converged")
        _FEM_FILE = fem_transient_model

        _STEADY_HEAD = np.array(_STEADY_HEAD)

        # Steady Head used to debug, to check the convergence of the model before next step
        np.save(_PATH_SAVE+"Steady_head/steady_head_"+str(model)+'.npy',_STEADY_HEAD)

        doc = ifm.loadDocument(_FEM_FILE)

        _STEADY = False
        _TRANSIENT = True
        _TRANSIENT_CONVERGENCE = True

        doc.startSimulator()

        if _TRANSIENT_CONVERGENCE :
            doc.stopSimulator()

            doc.closeDocument()

            _HEAD = np.array(_HEAD)

            np.save(_PATH_SAVE+"Head/head_"+str(model)+'.npy',_HEAD)

        else :
            print("Convergence failed with this permeability field")
            print("Deletion of the permeability field")

            os.remove(path_data+"Assim_"+str(assim_number)+"/Individual_Update/assim_"+\
                str(assim_number)+"_model_"+str(model)+".npy")

    else :
        print("Convergence failed with this permeability field")
        print("Deletion of the permeability field")

        os.remove(path_data+"Assim_"+str(assim_number)+"/Individual_Update/assim_"+\
            str(assim_number)+"_model_"+str(model)+".npy")

    del _HEAD
    del _WELL
    del _STEADY
    del _TRANSIENT
    del _ELEMENT_NUMBER
    del _TEMP_HEAD

    # Using a try before deleting these vectors, if Feflow hasn't deallocating them properly
    try:
        del _PERM_KX
    except :
        pass

    try:
        del _PERM_KZ
    except :
        pass

    try:
        del _SS
    except :
        pass
    # try:
    #     del _RECHARGE
    # except :
    #     pass
    

    print("Total simulation "+str(model)+" done in "+str(int((time.time()-temp_time)//60))+" minutes")

    del _TIME

    gc.collect()
#   def preEnterSimulator(doc):
View Source
def preEnterSimulator(doc):

    """
    Assigning parameters to Feflow before launching any simulation. Parameters are passed
    by global variables has defined in Feflow documentation. In this code, we send permKx,
    permKz and recharge issued from geostatistical simulations. 

    Args:
        doc (PyFeflowDoc): Feflow Document imported with ifm.loadDocument(fem_file)

    Raises:
        ValueError: Wrong number of elements in _PERM_KX
        ValueError: Wrong number of elements in _PERM_KY
        ValueError: Wrong number of elements in _PERM_KZ
        ValueError: Wrong number of recharge in _RECHARGE
        ValueError: Wrong number of nodes while importing steady heads
    """
    
    if (_ELEMENT_NUMBER!=len(_PERM_KX)):
        raise ValueError("Wrong number of elements in permKx")

    # To modify when switching to full anisotropic k fields
    if (_ELEMENT_NUMBER!=len(_PERM_KX)):
        raise ValueError("Wrong number of elements in permKy")

    if (_ELEMENT_NUMBER!=len(_PERM_KZ)):
        raise ValueError("Wrong number of elements in permKz")

    #if (len(doc.getParamValues(ifm.Enum.P_IOFLOW))!=len(_RECHARGE)):
    #    raise ValueError("Wrong number of recharge zones")

    # In this case, Kx = Ky, _PERM_KX is assigned to P_CONDX and P_CONDY

    doc.setParamValues(ifm.Enum.P_CONDX,_PERM_KX,0)
    doc.setParamValues(ifm.Enum.P_CONDY,_PERM_KX,0)
    doc.setParamValues(ifm.Enum.P_CONDZ,_PERM_KZ,0)
    
    #doc.setParamValues(ifm.Enum.P_IOFLOW,_RECHARGE,0)
    doc.setParamValues(ifm.Enum.P_COMP,_SS,0)

    # In transient case, we want to set hydraulic head simulated in steady state simulation
    # as initial hydraulic heads to speed up simulation time.

    if _TRANSIENT == True :
        
        #steady_heads = np.load(_PATH_SAVE+"/temp/steady_heads_"+str(_MODEL_NUMBER)+".npy")
        steady_heads = _TEMP_HEAD 
        
        if (doc.getNumberOfNodes()!=len(steady_heads)):
            raise ValueError("Wrong number of nodes while importing steady heads")


        #doc.setParamValues(ifm.Enum.P_HEAD,steady_heads.tolist(),0) 
        doc.setParamValues(ifm.Enum.P_HEAD,steady_heads,0) 

        del steady_heads

Assigning parameters to Feflow before launching any simulation. Parameters are passed by global variables has defined in Feflow documentation. In this code, we send permKx, permKz and recharge issued from geostatistical simulations.

Args
  • doc (PyFeflowDoc): Feflow Document imported with ifm.loadDocument(fem_file)
Raises
  • ValueError: Wrong number of elements in _PERM_KX
  • ValueError: Wrong number of elements in _PERM_KY
  • ValueError: Wrong number of elements in _PERM_KZ
  • ValueError: Wrong number of recharge in _RECHARGE
  • ValueError: Wrong number of nodes while importing steady heads
#   def postTimeStep(doc):
View Source
def postTimeStep(doc):

    """
    Assigning parameters to Feflow before launching any simulation. Parameters are passed
    by global variables has defined in Feflow documentation. In this code, we send permKx,
    permKz and recharge issued from geostatistical simulations. 

    Args:
        doc (PyFeflowDoc): Feflow Document imported with ifm.loadDocument(fem_file)
    """

    # In steady state we stored the hydraulic heads simulated at each nodes to use them
    # as initial hydraulic head in transient simulation
    if _STEADY == True :
        global _TEMP_HEAD
        steady_heads = doc.getParamValues(ifm.Enum.P_HEAD)

        #np.save(_PATH_SAVE+"/temp/steady_heads_"+str(_MODEL_NUMBER),steady_heads)
        _TEMP_HEAD = steady_heads

        array_sum = np.sum(steady_heads)
        array_has_nan = np.isnan(array_sum)

        if np.max(steady_heads) > 100 or np.min(steady_heads) < 1 or array_has_nan :
            global _STEADY_CONVERGENCE
            _STEADY_CONVERGENCE = False
    

        del steady_heads


        global _STEADY_HEAD
        temp_heads = np.zeros(len(_WELL))

        for well in range(len(_WELL)) :
            temp_heads[well] = doc.getResultsFlowHeadValueAtXYZ(_WELL[well][0], _WELL[well][1], _WELL[well][2])

        _STEADY_HEAD.append(temp_heads)



    else :

        # well + 1 to store an additionnal columns with time of simulation
        temp_heads = np.zeros(len(_WELL)+1)


        _time = doc.getAbsoluteSimulationTime()

        print("Time step :"+str(np.round(_time,1))+"d, Total ellapsed time : "+str(int(time.time()-_TIME))+"s")

        global _HEAD
        for well in range(len(_WELL)) :
            temp_heads[0] = _time
            temp_heads[well+1] = doc.getResultsFlowHeadValueAtXYZ(_WELL[well][0], _WELL[well][1], _WELL[well][2])
        _HEAD.append(temp_heads)

        
        # Put a treshold to delete non converging simulations exceeding a given time (mean converging time on succesfull simulation + 200 sec buffer) 
        if (_time < 3 and (time.time()-_TIME)> 550) or (_time < 6 and (time.time()-_TIME)> 800) or (_time < 11 and (time.time()-_TIME)> 1000) or (_time < 20 and (time.time()-_TIME)> 1200) or (time.time()-_TIME> 1600) :

            doc.stopSimulator()
            doc.closeDocument()
            global _TRANSIENT_CONVERGENCE
            _TRANSIENT_CONVERGENCE = False

Assigning parameters to Feflow before launching any simulation. Parameters are passed by global variables has defined in Feflow documentation. In this code, we send permKx, permKz and recharge issued from geostatistical simulations.

Args
  • doc (PyFeflowDoc): Feflow Document imported with ifm.loadDocument(fem_file)
#   def check_launch_feflow( path_data: str, fem_steady_model: str, fem_transient_model: str, assim_number: int, model_number: int ) -> None:
View Source
def check_launch_feflow(path_data: str,
                    fem_steady_model: str,
                    fem_transient_model: str,
                    assim_number: int,
                    model_number: int) -> None:

    """    
    To avoid redoing lengthy simulation on already done one, we check the list of simulation to do
    versus the one already done. We also checked the existence of folders.

    Args:
        path_data (str): Abolute path to the Data folder contained in the project
        fem_steady_model (str): Abolute path to the Feflow steady state model
        fem_transient_model (str): Abolute path to the Feflow transient state model
        assim_number (int): Actual assimilation step in the multiple data assimilation process
        model_number (int): Number of the model to be run in the actual simulation
        
    Raises:
        ValueError: Individual_Update folder missing in Data/Assim_X/ folder
        ValueError: Head folder missing in Data/Assim_X/ folder
    """

    str_model_number = str(model_number)
    str_assim_number = str(assim_number)

    temp_filename = "assim_"+str_assim_number+"_model_"+str_model_number+".npy"

    path = path_data + "Assim_"+str_assim_number+"/Individual_Update/"
    path_already_done = path_data + "Assim_"+str_assim_number+"/Head/"

    if not os.path.isdir(path):
        raise ValueError("Individual_Update folder missing in Assim_"+str_assim_number+"/Individual_Update/")

    if not os.path.isdir(path_already_done):
        raise ValueError("Head folder missing in Assim_"+str_assim_number+"/Head/")


    files = [f for f in listdir(path) if isfile(join(path, f))]
    files_already_done = [f for f in listdir(path_already_done) if isfile(join(path_already_done, f))]

    if "head_"+str(model_number)+".npy" in files_already_done :
        print("Loading succesfull, permeability field "+str_model_number+" already run")

    else :

        temp_filename = "assim_"+str_assim_number+"_model_"+str_model_number+".npy"

        if temp_filename in files :

            print("Loading succesfull, permeability field "+str_model_number+" being run")

            launch_feflow(path_data,
                                fem_steady_model,
                                fem_transient_model,
                                assim_number,
                                model_number)

        else :
            print("/!\ /!\ Loading unsuccesfull, permeability field assim_"+str_assim_number+"_model_"+\
                str_model_number+".npy is missing.\n Could be normal in case \
            of dropped non converging model /!\ /!\\")

To avoid redoing lengthy simulation on already done one, we check the list of simulation to do versus the one already done. We also checked the existence of folders.

Args
  • path_data (str): Abolute path to the Data folder contained in the project
  • fem_steady_model (str): Abolute path to the Feflow steady state model
  • fem_transient_model (str): Abolute path to the Feflow transient state model
  • assim_number (int): Actual assimilation step in the multiple data assimilation process
  • model_number (int): Number of the model to be run in the actual simulation
Raises
  • ValueError: Individual_Update folder missing in Data/Assim_X/ folder
  • ValueError: Head folder missing in Data/Assim_X/ folder
#   def launch_feflow( path_data: str, fem_steady_model: str, fem_transient_model: str, assim_number: int, model: int ) -> None:
View Source
def launch_feflow(path_data: str,
                    fem_steady_model: str,
                    fem_transient_model: str,
                    assim_number: int, 
                    model: int) -> None:
    """
    Functions to assign parameters to Feflow and run the simulation directly from Python.
    A lot of internal global variable are defined in accordance with Feflow documentation.
    They follow a naming scheme to minimize risk of interference with other python file :
    _NAME_OF_INTERNAL_GLOBAL.

    First step is to assign parameters to the steady state Feflow model and running it. Hydraulic 
    heads results are then stored as initial condition from the transient model run afterward. 

    Args:
        path_data (str): Abolute path to the Data folder contained in the project
        fem_steady_model (str): Abolute path to the Feflow steady state model
        fem_transient_model (str): Abolute path to the Feflow transient state model
        assim_number (int): Actual assimilation step in the multiple data assimilation process
        model_number (int): Number of the model to be run in the actual simulation

    Raises:
        ValueError: Mising steady state Feflow model, check the name or file
        ValueError: Mising transient state Feflow model, check the name or file
    """

    
    if not os.path.isfile(fem_steady_model):
        raise ValueError("Mising steady state Feflow model, check the name or file")

    if not os.path.isfile(fem_transient_model):
        raise ValueError("Mising transient state Feflow model, check the name or file")

    

    global _ELEMENT_NUMBER
    global _K_ZONE
    global _RECHARGE_ZONE
    global _UPDATED_ELEMENT
    global _PERM_KX
    global _PERM_KZ
    global _RECHARGE
    global _SS
    global _PATH
    global _PATH_SAVE
    global _FEM_FILE
    global _STEADY
    global _TRANSIENT
    global _HEAD
    global _STEADY_HEAD
    global _WELL
    global _TIME
    global _MODEL_NUMBER
    global _STEADY_CONVERGENCE
    global _TRANSIENT_CONVERGENCE
    global _TEMP_HEAD

    _MODEL_NUMBER = model

    _TIME = time.time()

    # Loading all relevant files, Feflow model, updated parameters, and the kZone and recharge zone
    # linking the global parameters to the element number in the Feflow model.
    _FEM_FILE = fem_steady_model
    doc = ifm.loadDocument(_FEM_FILE)

    _ELEMENT_NUMBER = doc.getNumberOfElements()
    

    _UPDATED_ELEMENT = np.load(path_data+"/FEFLOW_Model/updated_element.npy")
    
    # Elements number count starts at 1 in feflow, check the consitency with input updated_element.npy indexing
    _UPDATED_ELEMENT = (_UPDATED_ELEMENT - 1).astype(int)

    _PATH = path_data+"Assim_"+str(assim_number)+"/Individual_Update/"
    _PATH_SAVE = path_data+"Assim_"+str(assim_number)+"/"

    # _HEAD is fullfilled in the postTimeStep() function called by Feflow
    _HEAD = []
    _STEADY_HEAD = []  
    _TEMP_HEAD = []

    # Coordinates of all observations point used in the model for assimilation 
    # and calibration purpose. They are monitored at each time of simulation.
    _WELL = pd.read_csv(path_data+"/FEFLOW_Model/well_location.inp")

    _WELL = np.vstack((_WELL.X.values-doc.getOriginX(),
                       _WELL.Y.values-doc.getOriginY(),
                       _WELL.Z.values-doc.getOriginZ())).T

    # updated_parameter are coming from the assimilation process, in this model, all data except 
    # the last 47 are local element, with a value for each cell in the model. They are called 
    # local_updated_parameter for this reason. 
    # The last 47 parameters assimilated are global parameters, influencing several cells in 
    # Feflow model through kZones. They are treated separately to bring back kZones to elements.
    # On the 45 parameters, the first 15 are related to kx, the next 15 to kr (ratio of kz to kx),
    # the last 15 are saturation coefficient in different surface zones.


    elementsCoordinates = pd.read_pickle(path_data+"/FEFLOW_Model/feflow_element")
    
    _K_ZONE = elementsCoordinates.K_zone.values

    parameter_name = np.unique(elementsCoordinates.K_zone.values)


    updated_parameter = np.load(path_data+"Assim_"+str(assim_number)+"/Individual_Update/assim_"+\
            str(assim_number)+"_model_"+str(model)+".npy")

    global_parameters = len(updated_parameter) - _ELEMENT_NUMBER 
    
    local_updated_parameter = updated_parameter[:-global_parameters]
    global_updated_parameters = updated_parameter[-global_parameters:]

    kx = np.array(doc.getParamValues(ifm.Enum.P_CONDX))

    """
    We will let the kx value set outside of the local update

    # First we applied permeability coming from global variable, by applying global 
    # value to all elements, local elements will replace these global values where
    # they are defined.
    i = 0
    for name in parameter_name :
        # Feflow takes permeabilty as m/d, in case of m/s, multiplying by 3600 * 24.
        kx[np.where(_K_ZONE==name)] = global_updated_parameters[i] *3600*24
        i +=1


    """

    kx[_UPDATED_ELEMENT] = local_updated_parameter[_UPDATED_ELEMENT] 

    ### ky = kx, no need to add another vector in memory

    kz_temp = np.zeros(_ELEMENT_NUMBER)
    kz = np.array(doc.getParamValues(ifm.Enum.P_CONDZ))

    # First we applied permeability coming from global variable, by applying global 
    # value to all elements, local elements will replace these global values where
    # they are defined.
    i = 15
    for name in parameter_name :
        # Same logic, but kz is defined as a ratio of conductivity to kx. 
        kz_temp[np.where(_K_ZONE==name)] = kx[np.where(_K_ZONE==name)] / global_updated_parameters[i]
        i += 1

    kz[_UPDATED_ELEMENT] = kz_temp[_UPDATED_ELEMENT] 

    ss_temp = np.zeros(_ELEMENT_NUMBER)
    ss = np.array(doc.getParamValues(ifm.Enum.P_COMP))
    i = 30
    for name in parameter_name :
        ss_temp[np.where(_K_ZONE==name)] =  global_updated_parameters[i]
        i += 1

    ss[_UPDATED_ELEMENT] = ss_temp[_UPDATED_ELEMENT] 

    # Feflow is unable to deal with direct numpy array, he needs list
    _PERM_KX = kx.tolist()
    _PERM_KZ = kz.tolist()
    _SS = ss.tolist()

    # To reduce memory impact in case of big Feflow model, we free memory as much as possible
    del _K_ZONE
    del _UPDATED_ELEMENT
    del kx
    del kz
    del kz_temp
    del ss
    del ss_temp
    #del kr
    del updated_parameter
    del local_updated_parameter
    del global_updated_parameters
    del parameter_name

    
    # We need to specify in which kind of simulation we are
    _STEADY = True
    _TRANSIENT = False
    _STEADY_CONVERGENCE = True

    doc.startSimulator()
    doc.stopSimulator()

    doc.saveDocument(fem_steady_model[:-4]+"_temp.fem")

    doc.closeDocument()

    print("Steady simulation "+str(model)+" done in "+str(int(time.time()-_TIME))+"s")

    temp_time = _TIME
    # Reset timer for transient simulation
    _TIME = time.time()

    if _STEADY_CONVERGENCE :
        print("Model has converged")
        _FEM_FILE = fem_transient_model

        _STEADY_HEAD = np.array(_STEADY_HEAD)

        # Steady Head used to debug, to check the convergence of the model before next step
        np.save(_PATH_SAVE+"Steady_head/steady_head_"+str(model)+'.npy',_STEADY_HEAD)

        doc = ifm.loadDocument(_FEM_FILE)

        _STEADY = False
        _TRANSIENT = True
        _TRANSIENT_CONVERGENCE = True

        doc.startSimulator()

        if _TRANSIENT_CONVERGENCE :
            doc.stopSimulator()

            doc.closeDocument()

            _HEAD = np.array(_HEAD)

            np.save(_PATH_SAVE+"Head/head_"+str(model)+'.npy',_HEAD)

        else :
            print("Convergence failed with this permeability field")
            print("Deletion of the permeability field")

            os.remove(path_data+"Assim_"+str(assim_number)+"/Individual_Update/assim_"+\
                str(assim_number)+"_model_"+str(model)+".npy")

    else :
        print("Convergence failed with this permeability field")
        print("Deletion of the permeability field")

        os.remove(path_data+"Assim_"+str(assim_number)+"/Individual_Update/assim_"+\
            str(assim_number)+"_model_"+str(model)+".npy")

    del _HEAD
    del _WELL
    del _STEADY
    del _TRANSIENT
    del _ELEMENT_NUMBER
    del _TEMP_HEAD

    # Using a try before deleting these vectors, if Feflow hasn't deallocating them properly
    try:
        del _PERM_KX
    except :
        pass

    try:
        del _PERM_KZ
    except :
        pass

    try:
        del _SS
    except :
        pass
    # try:
    #     del _RECHARGE
    # except :
    #     pass
    

    print("Total simulation "+str(model)+" done in "+str(int((time.time()-temp_time)//60))+" minutes")

    del _TIME

    gc.collect()

Functions to assign parameters to Feflow and run the simulation directly from Python. A lot of internal global variable are defined in accordance with Feflow documentation. They follow a naming scheme to minimize risk of interference with other python file : _NAME_OF_INTERNAL_GLOBAL.

First step is to assign parameters to the steady state Feflow model and running it. Hydraulic heads results are then stored as initial condition from the transient model run afterward.

Args
  • path_data (str): Abolute path to the Data folder contained in the project
  • fem_steady_model (str): Abolute path to the Feflow steady state model
  • fem_transient_model (str): Abolute path to the Feflow transient state model
  • assim_number (int): Actual assimilation step in the multiple data assimilation process
  • model_number (int): Number of the model to be run in the actual simulation
Raises
  • ValueError: Mising steady state Feflow model, check the name or file
  • ValueError: Mising transient state Feflow model, check the name or file