Source code for wntr.epanet.toolkit

"""
The wntr.epanet.toolkit module is a Python extension for the EPANET 
Programmers Toolkit DLLs.
"""
import ctypes
import logging
import os
import os.path
import platform
import sys
from ctypes import byref

from pkg_resources import resource_filename

from .exceptions import EN_ERROR_CODES, EpanetException
from .util import SizeLimits

logger = logging.getLogger(__name__)

epanet_toolkit = "wntr.epanet.toolkit"

if os.name in ["nt", "dos"]:
    libepanet = resource_filename(__name__, "Windows/epanet2.dll")
elif sys.platform in ["darwin"]:
    libepanet = resource_filename(__name__, "Darwin/libepanet.dylib")
else:
    libepanet = resource_filename(__name__, "Linux/libepanet2.so")


[docs] def ENgetwarning(code, sec=-1): if sec >= 0: hours = int(sec / 3600.0) sec -= hours * 3600 mm = int(sec / 60.0) sec -= mm * 60 header = "%3d:%.2d:%.2d" % (hours, mm, sec) else: header = "{}".format(code) if code < 100: msg = EN_ERROR_CODES.get(code, "Unknown warning %s") else: raise EpanetException(code) return msg % header
[docs] def runepanet(inpfile, rptfile=None, binfile=None): """Run an EPANET command-line simulation Parameters ---------- inpfile : str The input file name """ file_prefix, file_ext = os.path.splitext(inpfile) if rptfile is None: rptfile = file_prefix + ".rpt" if binfile is None: binfile = file_prefix + ".bin" enData = ENepanet() enData.ENopen(inpfile, rptfile, binfile) enData.ENsolveH() enData.ENsolveQ() try: enData.ENreport() except: pass enData.ENclose()
[docs] class ENepanet: """Wrapper class to load the EPANET DLL object, then perform operations on the EPANET object that is created when a file is loaded. This simulator is thread safe **only** for EPANET `version=2.2`. Parameters ---------- inpfile : str Input file to use rptfile : str Output file to report to binfile : str Results file to generate version : float EPANET version to use (either 2.0 or 2.2) """
[docs] def __init__(self, inpfile="", rptfile="", binfile="", version=2.2): self.ENlib = None self.errcode = 0 self.errcodelist = [] self.cur_time = 0 self.Warnflag = False self.Errflag = False self.fileLoaded = False self.inpfile = inpfile self.rptfile = rptfile self.binfile = binfile if float(version) == 2.0: libnames = ["epanet2_x86", "epanet2", "epanet"] if "64" in platform.machine(): libnames.insert(0, "epanet2_amd64") elif float(version) == 2.2: libnames = ["epanet22", "epanet22_win32"] if "64" in platform.machine(): libnames.insert(0, "epanet22_amd64") for lib in libnames: try: if os.name in ["nt", "dos"]: libepanet = resource_filename(epanet_toolkit, "Windows/%s.dll" % lib) self.ENlib = ctypes.windll.LoadLibrary(libepanet) elif sys.platform in ["darwin"]: libepanet = resource_filename(epanet_toolkit, "Darwin/lib%s.dylib" % lib) self.ENlib = ctypes.cdll.LoadLibrary(libepanet) else: libepanet = resource_filename(epanet_toolkit, "Linux/lib%s.so" % lib) self.ENlib = ctypes.cdll.LoadLibrary(libepanet) return except Exception as E1: if lib == libnames[-1]: raise E1 pass finally: if version >= 2.2 and "32" not in lib: self._project = ctypes.c_uint64() elif version >= 2.2: self._project = ctypes.c_uint32() else: self._project = None return
[docs] def isOpen(self): """Checks to see if the file is open""" return self.fileLoaded
def _error(self, *args): """Print the error text the corresponds to the error code returned""" if not self.errcode: return # errtxt = self.ENlib.ENgeterror(self.errcode) errtext = EN_ERROR_CODES.get(self.errcode, "unknown error") if "%" in errtext and len(args) == 1: errtext % args if self.errcode >= 100: self.Errflag = True logger.error("EPANET error {} - {}".format(self.errcode, errtext)) raise EpanetException(self.errcode) else: self.Warnflag = True # warnings.warn(ENgetwarning(self.errcode)) logger.warning("EPANET warning {} - {}".format(self.errcode, ENgetwarning(self.errcode, self.cur_time))) self.errcodelist.append(ENgetwarning(self.errcode, self.cur_time)) return
[docs] def ENopen(self, inpfile=None, rptfile=None, binfile=None): """ Opens an EPANET input file and reads in network data Parameters ---------- inpfile : str EPANET INP file (default to constructor value) rptfile : str Output file to create (default to constructor value) binfile : str Binary output file to create (default to constructor value) """ if self._project is not None: if self.fileLoaded: self.EN_close(self._project) if self.fileLoaded: raise RuntimeError("File is loaded and cannot be closed") if inpfile is None: inpfile = self.inpfile if rptfile is None: rptfile = self.rptfile if binfile is None: binfile = self.binfile inpfile = inpfile.encode("latin-1") rptfile = rptfile.encode("latin-1") binfile = binfile.encode("latin-1") self.ENlib.EN_createproject(ctypes.byref(self._project)) self.errcode = self.ENlib.EN_open(self._project, inpfile, rptfile, binfile) self._error() if self.errcode < 100: self.fileLoaded = True return else: if self.fileLoaded: self.ENclose() if self.fileLoaded: raise RuntimeError("File is loaded and cannot be closed") if inpfile is None: inpfile = self.inpfile if rptfile is None: rptfile = self.rptfile if binfile is None: binfile = self.binfile inpfile = inpfile.encode("latin-1") rptfile = rptfile.encode("latin-1") binfile = binfile.encode("latin-1") self.errcode = self.ENlib.ENopen(inpfile, rptfile, binfile) self._error() if self.errcode < 100: self.fileLoaded = True return
[docs] def ENclose(self): """Frees all memory and files used by EPANET""" if self._project is not None: self.errcode = self.ENlib.EN_close(self._project) self.ENlib.EN_deleteproject(self._project) self._project = None self._project = ctypes.c_uint64() else: self.errcode = self.ENlib.ENclose() self._error() if self.errcode < 100: self.fileLoaded = False return
[docs] def ENsolveH(self): """Solves for network hydraulics in all time periods""" if self._project is not None: self.errcode = self.ENlib.EN_solveH(self._project) else: self.errcode = self.ENlib.ENsolveH() self._error() return
[docs] def ENsaveH(self): """Solves for network hydraulics in all time periods Must be called before ENreport() if no water quality simulation made. Should not be called if ENsolveQ() will be used. """ if self._project is not None: self.errcode = self.ENlib.EN_saveH(self._project) else: self.errcode = self.ENlib.ENsaveH() self._error() return
[docs] def ENopenH(self): """Sets up data structures for hydraulic analysis""" if self._project is not None: self.errcode = self.ENlib.EN_openH(self._project) else: self.errcode = self.ENlib.ENopenH() self._error() return
[docs] def ENinitH(self, iFlag): """Initializes hydraulic analysis Parameters ----------- iFlag : 2-digit flag 2-digit flag where 1st (left) digit indicates if link flows should be re-initialized (1) or not (0) and 2nd digit indicates if hydraulic results should be saved to file (1) or not (0) """ if self._project is not None: self.errcode = self.ENlib.EN_initH(self._project, iFlag) else: self.errcode = self.ENlib.ENinitH(iFlag) self._error() return
[docs] def ENrunH(self): """Solves hydraulics for conditions at time t This function is used in a loop with ENnextH() to run an extended period hydraulic simulation. See ENsolveH() for an example. Returns -------- int Current simulation time (seconds) """ lT = ctypes.c_long() if self._project is not None: self.errcode = self.ENlib.EN_runH(self._project, byref(lT)) else: self.errcode = self.ENlib.ENrunH(byref(lT)) self._error() self.cur_time = lT.value return lT.value
[docs] def ENnextH(self): """Determines time until next hydraulic event This function is used in a loop with ENrunH() to run an extended period hydraulic simulation. See ENsolveH() for an example. Returns --------- int Time (seconds) until next hydraulic event (0 marks end of simulation period) """ lTstep = ctypes.c_long() if self._project is not None: self.errcode = self.ENlib.EN_nextH(self._project, byref(lTstep)) else: self.errcode = self.ENlib.ENnextH(byref(lTstep)) self._error() return lTstep.value
[docs] def ENcloseH(self): """Frees data allocated by hydraulics solver""" if self._project is not None: self.errcode = self.ENlib.EN_closeH(self._project) else: self.errcode = self.ENlib.ENcloseH() self._error() return
[docs] def ENsavehydfile(self, filename): """Copies binary hydraulics file to disk Parameters ------------- filename : str Name of hydraulics file to output """ if self._project is not None: self.errcode = self.ENlib.EN_savehydfile(self._project, filename.encode("latin-1")) else: self.errcode = self.ENlib.ENsavehydfile(filename.encode("latin-1")) self._error() return
[docs] def ENusehydfile(self, filename): """Opens previously saved binary hydraulics file Parameters ------------- filename : str Name of hydraulics file to use """ if self._project is not None: self.errcode = self.ENlib.EN_usehydfile(self._project, filename.encode("latin-1")) else: self.errcode = self.ENlib.ENusehydfile(filename.encode("latin-1")) self._error() return
[docs] def ENsolveQ(self): """Solves for network water quality in all time periods""" if self._project is not None: self.errcode = self.ENlib.EN_solveQ(self._project) else: self.errcode = self.ENlib.ENsolveQ() self._error() return
[docs] def ENopenQ(self): """Sets up data structures for water quality analysis""" if self._project is not None: self.errcode = self.ENlib.EN_openQ(self._project) else: self.errcode = self.ENlib.ENopenQ() self._error() return
[docs] def ENinitQ(self, iSaveflag): """Initializes water quality analysis Parameters ------------- iSaveflag : int EN_SAVE (1) if results saved to file, EN_NOSAVE (0) if not """ if self._project is not None: self.errcode = self.ENlib.EN_initQ(self._project, iSaveflag) else: self.errcode = self.ENlib.ENinitQ(iSaveflag) self._error() return
[docs] def ENrunQ(self): """Retrieves hydraulic and water quality results at time t This function is used in a loop with ENnextQ() to run an extended period water quality simulation. See ENsolveQ() for an example. Returns ------- int Current simulation time (seconds) """ lT = ctypes.c_long() if self._project is not None: self.errcode = self.ENlib.EN_runQ(self._project, byref(lT)) else: self.errcode = self.ENlib.ENrunQ(byref(lT)) self._error() return lT.value
[docs] def ENnextQ(self): """Advances water quality simulation to next hydraulic event This function is used in a loop with ENrunQ() to run an extended period water quality simulation. See ENsolveQ() for an example. Returns -------- int Time (seconds) until next hydraulic event (0 marks end of simulation period) """ lTstep = ctypes.c_long() if self._project is not None: self.errcode = self.ENlib.EN_nextQ(self._project, byref(lTstep)) else: self.errcode = self.ENlib.ENnextQ(byref(lTstep)) self._error() return lTstep.value
[docs] def ENcloseQ(self): """Frees data allocated by water quality solver""" if self._project is not None: self.errcode = self.ENlib.EN_closeQ(self._project) else: self.errcode = self.ENlib.ENcloseQ() self._error() return
[docs] def ENreport(self): """Writes report to report file""" if self._project is not None: self.errcode = self.ENlib.EN_report(self._project) else: self.errcode = self.ENlib.ENreport() self._error() return
[docs] def ENgetcount(self, iCode): """Retrieves the number of components of a given type in the network Parameters ------------- iCode : int Component code (see toolkit.optComponentCounts) Returns --------- int Number of components in network """ iCount = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_getcount(self._project, iCode, byref(iCount)) else: self.errcode = self.ENlib.ENgetcount(iCode, byref(iCount)) self._error() return iCount.value
[docs] def ENgetflowunits(self): """Retrieves flow units code Returns ----------- Code of flow units in use (see toolkit.optFlowUnits) """ iCode = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_getflowunits(self._project, byref(iCode)) else: self.errcode = self.ENlib.ENgetflowunits(byref(iCode)) self._error() return iCode.value
[docs] def ENgetnodeid(self, iIndex): """Gets the ID name of a node given its index. Parameters ---------- iIndex : int a node's index (starting from 1). Returns ------- str the node name """ fValue = ctypes.create_string_buffer(SizeLimits.EN_MAX_ID.value) if self._project is not None: self.errcode = self.ENlib.EN_getnodeid(self._project, iIndex, byref(fValue)) else: self.errcode = self.ENlib.ENgetnodeid(iIndex, byref(fValue)) self._error() return str(fValue.value, "UTF-8")
[docs] def ENgetnodeindex(self, sId): """Retrieves index of a node with specific ID Parameters ------------- sId : int Node ID Returns --------- Index of node in list of nodes """ iIndex = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_getnodeindex(self._project, sId.encode("latin-1"), byref(iIndex)) else: self.errcode = self.ENlib.ENgetnodeindex(sId.encode("latin-1"), byref(iIndex)) self._error() return iIndex.value
[docs] def ENgetnodetype(self, iIndex): """Retrieves a node's type given its index. Parameters ---------- iIndex : int The index of the node Returns ------- int the node type as an integer """ fValue = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_getnodetype(self._project, iIndex, byref(fValue)) else: self.errcode = self.ENlib.ENgetnodetype(iIndex, byref(fValue)) self._error() return fValue.value
[docs] def ENgetnodevalue(self, iIndex, iCode): """Retrieves parameter value for a node Parameters ------------- iIndex: int Node index iCode : int Node parameter code (see toolkit.optNodeParams) Returns --------- Value of node's parameter """ fValue = ctypes.c_float() if self._project is not None: fValue = ctypes.c_double() self.errcode = self.ENlib.EN_getnodevalue(self._project, iIndex, iCode, byref(fValue)) else: self.errcode = self.ENlib.ENgetnodevalue(iIndex, iCode, byref(fValue)) self._error() return fValue.value
[docs] def ENgetlinktype(self, iIndex): """Retrieves a link's type given its index. Parameters ---------- iIndex : int The index of the link Returns ------- int the link type as an integer """ fValue = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_getlinktype(self._project, iIndex, byref(fValue)) else: self.errcode = self.ENlib.EN_getlinktype(iIndex, byref(fValue)) self._error() return fValue.value
[docs] def ENgetlinkindex(self, sId): """Retrieves index of a link with specific ID Parameters ------------- sId : int Link ID Returns --------- Index of link in list of links """ iIndex = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_getlinkindex(self._project, sId.encode("latin-1"), byref(iIndex)) else: self.errcode = self.ENlib.ENgetlinkindex(sId.encode("latin-1"), byref(iIndex)) self._error() return iIndex.value
[docs] def ENgetlinkvalue(self, iIndex, iCode): """Retrieves parameter value for a link Parameters ------------- iIndex : int Link index iCode : int Link parameter code (see toolkit.optLinkParams) Returns --------- Value of link's parameter """ fValue = ctypes.c_float() if self._project is not None: fValue = ctypes.c_double() self.errcode = self.ENlib.EN_getlinkvalue(self._project, iIndex, iCode, byref(fValue)) else: self.errcode = self.ENlib.ENgetlinkvalue(iIndex, iCode, byref(fValue)) self._error() return fValue.value
[docs] def ENsetlinkvalue(self, iIndex, iCode, fValue): """Set the value on a link Parameters ---------- iIndex : int the link index iCode : int the parameter enum integer fValue : float the value to set on the link """ if self._project is not None: self.errcode = self.ENlib.EN_setlinkvalue( self._project, ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) ) else: self.errcode = self.ENlib.ENsetlinkvalue(ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue)) self._error()
[docs] def ENsetnodevalue(self, iIndex, iCode, fValue): """ Set the value on a node Parameters ---------- iIndex : int the node index iCode : int the parameter enum integer fValue : float the value to set on the node """ if self._project is not None: self.errcode = self.ENlib.EN_setnodevalue( self._project, ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) ) else: self.errcode = self.ENlib.ENsetnodevalue(ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue)) self._error()
[docs] def ENsettimeparam(self, eParam, lValue): """Set a time parameter value Parameters ---------- eParam : int the time parameter to set lValue : long the value to set, in seconds """ if self._project is not None: self.errcode = self.ENlib.EN_settimeparam(self._project, ctypes.c_int(eParam), ctypes.c_long(lValue)) else: self.errcode = self.ENlib.ENsettimeparam(ctypes.c_int(eParam), ctypes.c_long(lValue)) self._error()
[docs] def ENgettimeparam(self, eParam): """Get a time parameter value Parameters ---------- eParam : int the time parameter to get Returns ------- long the value of the time parameter, in seconds """ lValue = ctypes.c_long() if self._project is not None: self.errcode = self.ENlib.EN_gettimeparam(self._project, ctypes.c_int(eParam), byref(lValue)) else: self.errcode = self.ENlib.ENgettimeparam(ctypes.c_int(eParam), byref(lValue)) self._error() return lValue.value
[docs] def ENaddcontrol(self, iType: int, iLinkIndex: int, dSetting: float, iNodeIndex: int, dLevel: float) -> int: """Add a new simple control Parameters ---------- iType : int the type of control iLinkIndex : int the index of the link dSetting : float the new link setting value iNodeIndex : int Set to 0 for time of day or timer dLevel : float the level to compare against Returns ------- int the new control number """ lValue = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_addcontrol( self._project, ctypes.c_int(iType), ctypes.c_int(iLinkIndex), ctypes.c_double(dSetting), ctypes.c_int(iNodeIndex), ctypes.c_double(dLevel), byref(lValue), ) else: self.errcode = self.ENlib.ENaddcontrol( ctypes.c_int(iType), ctypes.c_int(iLinkIndex), ctypes.c_double(dSetting), ctypes.c_int(iNodeIndex), ctypes.c_double(dLevel), byref(lValue), ) self._error() return lValue.value
[docs] def ENgetcontrol(self, iIndex: int): """Get values defined by a control. Parameters ---------- iIndex : int the control number """ iType = ctypes.c_int() iLinkIndex = ctypes.c_int() dSetting = ctypes.c_double() iNodeIndex = ctypes.c_int() dLevel = ctypes.c_double() if self._project is not None: self.errcode = self.ENlib.EN_getcontrol( self._project, ctypes.c_int(iIndex), byref(iType), byref(iLinkIndex), byref(dSetting), byref(iNodeIndex), byref(dLevel), ) else: self.errcode = self.ENlib.ENgetcontrol( ctypes.c_int(iIndex), byref(iType), byref(iLinkIndex), byref(dSetting), byref(iNodeIndex), byref(dLevel) ) self._error() return dict( index=iIndex, type=iType.value, linkindex=iLinkIndex.value, setting=dSetting.value, nodeindex=iNodeIndex.value, level=dLevel.value, )
[docs] def ENsetcontrol(self, iIndex: int, iType: int, iLinkIndex: int, dSetting: float, iNodeIndex: int, dLevel: float): """Change values on a simple control Parameters ---------- iIndex : int the control index iType : int the type of control comparison iLinkIndex : int the link being changed dSetting : float the setting to change to iNodeIndex : int the node being compared against, Set to 0 for time of day or timer dLevel : float the level being checked Warning ------- There is an error in EPANET 2.2 that sets the `dLevel` parameter to 0.0 on Macs regardless of the value the user passes in. This means that to use this toolkit functionality on a Mac, the user must delete and create a new control to change the level. """ if self._project is not None: try: self.errcode = self.ENlib.EN_setcontrol( self._project, ctypes.c_int(iIndex), ctypes.c_int(iType), ctypes.c_int(iLinkIndex), ctypes.c_double(dSetting), ctypes.c_int(iNodeIndex), ctypes.c_double(dLevel), ) except: self.errcode = self.ENlib.EN_setcontrol( self._project, ctypes.c_int(iIndex), ctypes.c_int(iType), ctypes.c_int(iLinkIndex), ctypes.c_double(dSetting), ctypes.c_int(iNodeIndex), ctypes.c_float(dLevel), ) else: self.errcode = self.ENlib.ENsetcontrol( ctypes.c_int(iIndex), ctypes.c_int(iType), ctypes.c_int(iLinkIndex), ctypes.c_double(dSetting), ctypes.c_int(iNodeIndex), ctypes.c_double(dLevel), ) self._error()
[docs] def ENdeletecontrol(self, iControlIndex): """Delete a control. Parameters ---------- iControlIndex : int the simple control to delete """ if self._project is not None: self.errcode = self.ENlib.EN_deletecontrol(self._project, ctypes.c_int(iControlIndex)) else: self.errcode = self.ENlib.ENdeletecontrol(ctypes.c_int(iControlIndex)) self._error()
[docs] def ENsaveinpfile(self, inpfile): """Saves EPANET input file Parameters ------------- inpfile : str EPANET INP output file """ inpfile = inpfile.encode("latin-1") if self._project is not None: self.errcode = self.ENlib.EN_saveinpfile(self._project, inpfile) else: self.errcode = self.ENlib.ENsaveinpfile(inpfile) self._error() return