Skip to main content

Python wrapper for the CONTAM Simulation Engine, ContamX

Project description

Python Bindings for ContamX

NOTE: This package is not yet fully functional.

This is the initial implementation of a Python wrapper for contamx-lib.lib which is a statically linked library with an API wrapper around ContamX. contamx-lib enables control of CONTAM simulations via C-language API. This Python package, contamxpy, provides another layer of access, i.e., Python bindings, to contamx-lib.

The demonstration driver, test_cxffi.py that is included herein, imports contamxpy, obtains a state, gets properties of the simulation necessary to run from beginning to end, then steps through all the time steps.

Usage

Typically, one would work within a Python virtual environment which can be created and activated using the following commands.

$ python -m venv .venv
$ .venv\Scripts\activate  (on Windows)

Install contamxpy from PyPI.

$(.venv) python -m pip install contamxpy

Get help for the test module from the command line.

test_cxcffi.py -h

Usage: test_cxcffi.py [options] arg1  
    arg1=PRJ filename

Options:
  -h, --help    show this help message and exit
  -v VERBOSE, --verbose=VERBOSE
                define verbose output level: 0=Min, 1=Medium,
                2=Max

Run example case.

test_cxcffi.py testOneZoneWthCtm.prj

Example Test Module: test_cxcffi.py

from contamxpy import cxLib 
import os, sys
from optparse import OptionParser

#===================================================================================== main() =====
def main():
    #----- Manage option parser
    parser = OptionParser(usage="%prog [options] arg1\n\targ1=PRJ filename\n")
    parser.set_defaults(verbose=0)
    parser.add_option("-v", "--verbose", action="store", dest="verbose", type="int", default=0,
                        help="define verbose output level: 0=Min, 1=Medium, 2=Maximum.")
    (options, args) = parser.parse_args()

    #----- Process command line options -v
    verbose = options.verbose

    if len(args) != 1:
        parser.error("Need one argument:\n  arg1 = PRJ file.")
        return
    else:
        prj_name  = args[0]

    if ( not os.path.exists(prj_name) ):
        print("ERROR: PRJ file not found.")
        return

    msg_cmd = "Running test_cxcffi.py: arg1 = " + args[0] + " " + str(options)
    print(msg_cmd, "\n")

    if verbose > 1:
        print(f"cxLib attributes =>\n{chr(10).join(map(str, dir(cxLib)))}\n")

    #----- Initialize contamx-lib object w/ wpMode and cbOption.
    #      wpMode = 0 => use wind pressure profiles, WTH and CTM files or associated API calls.
    #      cbOption = True => set callback function to get PRJ INFO from the ContamXState.
    myPrj = cxLib(0, True)
    myPrj.setVerbosity(verbose)
    if verbose > 1:
        print(f"BEFORE setupSimulation()\n\tnCtms={myPrj.nContaminants}\n\tnZones={myPrj.nZones}\n\tnPaths={myPrj.nPaths}\n" )
    
    #----- Query State for Version info
    verStr = myPrj.getVersion()
    if verbose >= 0:
        print(f"getVersion() returned {verStr}.")

    #----- Setup Simulation for PRJ
    myPrj.setupSimulation(prj_name, 1)

    if verbose > 1:
        print(f"AFTER setupSimulation()\n\tnCtms={myPrj.nContaminants}\n\tnZones={myPrj.nZones}\n\tnPaths={myPrj.nPaths}\n" )

    dayStart = myPrj.getSimStartDate()
    dayEnd   = myPrj.getSimEndDate()
    secStart = myPrj.getSimStartTime()
    secEnd   = myPrj.getSimEndTime()
    tStep    = myPrj.getSimTimeStep()

    simBegin = (dayStart - 1) * 86400 + secStart
    simEnd = (dayEnd - 1) * 86400 + secEnd
 
    #----- Calculate the simulation duration in seconds and total time steps
    if (simBegin < simEnd):
        simDuration = simEnd - simBegin
    else:
        simDuration = 365 * 86400 - simEnd + simBegin
    numTimeSteps = int(simDuration / tStep)
 
    #----- Get the current date/time after initial steady state simulation
    currentDate = myPrj.getCurrentDayOfYear()
    currentTime = myPrj.getCurrentTimeInSec()
    if verbose > 0:
        print(f"Sim days = {dayStart}:{dayEnd}")
        print(f"Sim times = {secStart}:{secEnd}")
        print(f"Sim time step = {tStep}")
        print(f"Number of steps = {numTimeSteps}")

    #----- Run Simulation
    for i in range(numTimeSteps):
        currentDate = myPrj.getCurrentDayOfYear()
        currentTime = myPrj.getCurrentTimeInSec()
        if verbose > 1:
            print(f"{i}\t{currentDate},{currentTime}")
        myPrj.doSimStep(1)

    myPrj.endSimulation()

# --- End main() ---#

if __name__ == "__main__":
    main()

Developer Notes

These bindings were developed using the C Foreign Function Interface (CFFI). CFFI utilizes C header files that define the contamx-lib API. contamx-lib.lib is a build of ContamX that is statically linked build of contamx-lib. A dynamic Python module (.pyd) is built that incorporates the static build.

NIST Developer NOTE: The static build of contamx-lib.lib must include the following dependencies: WSock32.lib, WS2_32.lib, and user32.lib.

Steps to Develop Python Bindings

1. Create directory for contamxpy

Source files:

contamxpy\  
|
| setup.py
| setup.cfg
| MANIFEST.in 
| LICENSE.txt
| README.md
| contamxpy_build.py
| contamxpy.py
| contamx-lib.lib
|
├── include\
|   └── common-api.h
|       commonState.h
|       contam-x-cosim.h
|       contam-x-state.h
|       element-api.h
|       flags.h
|       library-api.h
|       project-api.h
|       string-len-max.h
|       types.h
|
└── demo_files\
    └── test_cxcffi.py
        testOneZoneWthCtm.prj/.wth/.ctm
        valThreeZonesWth.prj/.wth/.ctm

2. Create virtual environment

python -m venv .venv

3. Activate virtual environment

  • Windows => .venv\Scripts\activate.bat

4. Install cffi and wheel packages

$ python -m pip install cffi, wheel

5. Generate contamxpy

Run the build module.

contamxpy_build.py

This should generate _contamxpy.c, etc.

contamxpy\  
|
├── Release\
|   └── *.exp/.lib/.obj
|
└── _contamxpy.c
    _contamxpy.cp310-win_amd64.pyd

Most importantly it will generate a .pyd file, e.g., _contamxpy-0.0.3-abi3-cp310-win_amd64.pyd. This is a dynamic Python module to be imported into a driver program. It provides a wrapper to contamx-lib.lib.

6. Install the development version locally

$(.venv) pip install .

7. Generate Files for Distribution

Built distribution, i.e., wheel file:

$(.venv) python -m setup bdist_wheel

Source distribution, i.e., compressed archive (.gz, .zip):

$(.venv) python -m setup sdist
$(.venv) python -m setup sdist --format=zip

Development Files

cxcffi_build.py

The file shown below is a partial implementation of the API associated with contamx-lib.

from __future__ import annotations

# Using the "out-of-line", "API mode"
from cffi import FFI


CDEF = '''\
    // see types.h
    typedef int32_t IX;
    typedef float R4;
    typedef double R8;
    typedef uint16_t U2;
    
    typedef struct zone_cosim_dsc
    {
        int32_t nr;
        uint16_t flags;
        float Vol;
        char level_name[16]; // #define NAMELEN 16
        int32_t level_nr;
        char name[32];       // #define NMLN 32
    } ZONE_COSIM_DSC;

    typedef struct path_cosim_dsc 
    {
        IX nr;
        U2 flags;
        IX from_zone;
        IX to_zone;
        IX ahs_nr;
        R4 X;
        R4 Y;
        R4 Z;
        IX envIndex;
    } PATH_COSIM_DSC;

    // Callback function - extern => to be python function defined in contamxpy.py.
    extern "Python" void prjDataReadyFcnP(void *, void *);
    // {prjDataReadyFcnP} will set to function defined in {cxLib} class of {contamxpy}.
    void cxiRegisterCallback_PrjDataReady( void* contamXState, void* pUserData, void ( *prjDataReadyFcnP )( void*, void* ) );

    void* cxiGetContamState();
    void cxiSetWindPressureMode(void* contamXState, IX useWP);
    void cxiSetupSimulation(void* contamXState, char* projectPath, IX useCosim);
    IX cxiGetVersion(void* contamXState, char* strVersion);

    IX cxiGetSimulationStartDate(void* contamXState);
    IX cxiGetSimulationStartTime(void* contamXState);
    IX cxiGetSimulationEndDate(void* contamXState);
    IX cxiGetSimulationEndTime(void* contamXState);
    IX cxiGetSimulationTimeStep(void* contamXState);
    IX cxiGetCurrentDate(void* contamXState);
    IX cxiGetCurrentTime(void* contamXState);
    void cxiDoCoSimStep(void* contamXState, IX stepForward);
    void cxiEndSimulation(void* contamXState);

    // These functions will be utilized within the callback function, prjDataReadyFcnP(),
    //   to populate class variables for access by the calling/driver
    //   program which imports {contamxpy}
    IX cxiGetNumCtms(void* contamXState);
    IX cxiGetCtmName(void* contamXState, IX ctmNumber, char* strName);
    IX cxiGetNumZones(void* contamXState);
    IX cxiGetZoneInfo(void* contamXState, IX zoneNumber, ZONE_COSIM_DSC* pZoneInfo);
    IX cxiGetNumPaths(void* contamXState);
    IX cxiGetPathInfo(void* contamXState, IX pathNumber, PATH_COSIM_DSC* pPath);

    // Results
    IX cxiGetZoneMF(void* contamXState, IX zoneNumber, IX ctmNumber, R8* pMassFraction);
'''

SRC = '''\
    #include "include//contam-x-cosim.h"
'''

ffibuilder = FFI()
ffibuilder.cdef(CDEF)
ffibuilder.set_source(
    "_contamxpy", SRC,
    include_dirs=['.','include'],  # C header files for contam-x-lib
    libraries=['contamx-lib'],     # Library to link with (.lib, .dll)
)

if __name__ == "__main__":
    ffibuilder.compile(verbose=True)

contamxpy

contamxpy implements the cxLib class that provides the wrapper to contamx-lib. This module is imported for use by driver programs as demonstrated in test_cxcffi.py above.

from __future__ import annotations

import _contamxpy

_lib = _contamxpy.lib
_ffi = _contamxpy.ffi

#print(f"_contamxpy => \n\t{dir(_contamxpy)}")

'''
    cxiGetContamState() is defined in contam-x-cosim.c
'''
def getState():
    return _lib.cxiGetContamState()

def setWindPressureMode(state, mode):
    _lib.cxiSetWindPressureMode(state, mode)

def getVersion(state):
    bufStr = _ffi.new("char[]", 64)
    _lib.cxiGetVersion(state, bufStr)
    return _ffi.string(bufStr).decode('utf-8')

def setupSimulation(state, prjPath, useCosim):
    _lib.cxiSetupSimulation(state, prjPath.encode('ascii'), useCosim)

def getSimTimeStep(state):
    timeStep = _lib.cxiGetSimulationTimeStep(state)
    return timeStep

def getSimStartDate(state):
    dayOfYear = _lib.cxiGetSimulationStartDate(state)
    return dayOfYear

def getSimEndDate(state):
    dayOfYear = _lib.cxiGetSimulationEndDate(state)
    return dayOfYear

def getSimStartTime(state):
    timeOfDaySeconds = _lib.cxiGetSimulationStartTime(state)
    return timeOfDaySeconds

def getSimEndTime(state):
    timeOfDaySeconds = _lib.cxiGetSimulationEndTime(state)
    return timeOfDaySeconds

def getCurrentDayOfYear(state):
    return _lib.cxiGetCurrentDate(state)

def getCurrentTimeInSec(state):
    return _lib.cxiGetCurrentTime(state)

def doSimStep(state, stepForward):
    _lib.cxiDoCoSimStep(state, stepForward)

def endSimulation(state):
    _lib.cxiEndSimulation(state)

setup.py

from __future__ import annotations

import platform
import sys

from setuptools import setup

if platform.python_implementation() == 'CPython':
    try:
        import wheel.bdist_wheel
    except ImportError:
        cmdclass = {}
    else:
        class bdist_wheel(wheel.bdist_wheel.bdist_wheel):
            def finalize_options(self) -> None:
                self.py_limited_api = f'cp3{sys.version_info[1]}'
                super().finalize_options()

        cmdclass = {'bdist_wheel': bdist_wheel}
else:
    cmdclass = {}

setup(
    cffi_modules=['contamxpy_build.py:ffibuilder'], cmdclass=cmdclass
    )

setup.cfg

[metadata]
name = contamxpy
version = 0.0.3
description = Python wrapper for the CONTAM Simulation Engine, ContamX
long_description = file: README.md
long_description_content_type = text/markdown
author = W. Stuart Dols, Brian Polidoro, NIST
author_email = william.dols@nist.gov
license = Public Domain
license_files = LICENSE.txt
classifiers =
    License :: Public Domain 
    License :: OSI Approved :: BSD License
    Programming Language :: Python :: 3
    Programming Language :: Python :: 3 :: Only
    Programming Language :: Python :: Implementation :: CPython
    Programming Language :: Python :: Implementation :: PyPy
    Topic :: Scientific/Engineering
project_urls=
    Web Page= https://www.nist.gov/el/energy-and-environment-division-73200/nist-multizone-modeling/
    Source  = https://pypi.org/project/contamxpy/
[options]
py_modules = contamxpy
python_requires = >=3.7
install_requires =
    cffi>=1
setup_requires =
    cffi>=1

MANIFEST.in

The manifest file is used to add files to the source builds.

include include\*.h
include contamx-lib.*
include demo_files\*.*

REFERENCES

  1. https://www.youtube.com/watch?v=X5irxO5VCHw
  2. https://github.com/asottile/ukkonen
  3. https://cffi.readthedocs.io/en/latest/index.html
  4. https://docs.python.org/3.10/distutils/index.html
  5. https://setuptools.pypa.io/en/latest/setuptools.html
  6. https://packaging.python.org/en/latest/tutorials/packaging-projects/

TODO

  • Implement full API.
  • Test on Linux

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

contamxpy-0.0.4.zip (1.7 MB view hashes)

Uploaded Source

Built Distribution

contamxpy-0.0.4-cp310-abi3-win_amd64.whl (778.9 kB view hashes)

Uploaded CPython 3.10+ Windows x86-64

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page