diff --git a/docs/source/babel_heatcxx.toml b/docs/source/babel_heatcxx.toml new file mode 100644 index 0000000..c57961d --- /dev/null +++ b/docs/source/babel_heatcxx.toml @@ -0,0 +1,41 @@ +# See https://babelizer.readthedocs.io/ for more information + +# Describe the library being wrapped. +[library.HeatCxx] +language = "c++" +library = "bmiheatcxx" +header = "bmi_heat.hxx" +entry_point = "BmiHeat" + +# Describe compiler options need to build the library being wrapped. +[build] +undef_macros = [] +define_macros = [] +libraries = [] +library_dirs = [] +include_dirs = [] +extra_compile_args = [] + +# Describe the newly wrapped package. +[package] +name = "pymt_heatcxx" +requirements = [] + +[info] +github_username = "pymt-lab" +package_author = "csdms" +package_author_email = "csdms@colorado.edu" +package_license = "MIT License" +summary = "PyMT component for the C++ heat model" + +[ci] +python_version = [ + "3.10", + "3.11", + "3.12", +] +os = [ + "linux", + "mac", + "windows", +] diff --git a/docs/source/environment-cxx.yml b/docs/source/environment-cxx.yml new file mode 100644 index 0000000..f2eb796 --- /dev/null +++ b/docs/source/environment-cxx.yml @@ -0,0 +1,14 @@ +# A conda environment file for the babelizer C++ example +name: wrap-cxx +channels: + - conda-forge +dependencies: + - python=3 + - make + - cmake + - pkg-config + - cxx-compiler + - bmi-cxx + - babelizer + - bmi-tester + - pymt>=1.3 diff --git a/docs/source/example-c.rst b/docs/source/example-c.rst index ebdf256..35e3a64 100644 --- a/docs/source/example-c.rst +++ b/docs/source/example-c.rst @@ -352,7 +352,7 @@ Using the *babelizer*, we wrapped the *heat* model, which is written in C. It can now be called as a *pymt* component in Python. The steps for wrapping a model with the *babelizer* outlined in this example -can also be applied to models written in C++ and `Fortran`_. +can also be applied to models written in `C++`_ and `Fortran`_. .. @@ -365,4 +365,5 @@ can also be applied to models written in C++ and `Fortran`_. .. _Python Modeling Tool: https://pymt.readthedocs.io .. _CSDMS Model Metadata: https://github.com/csdms/model_metadata .. _Descriptions: https://github.com/csdms/model_metadata/blob/develop/README.rst +.. _C++: example-cxx.html .. _Fortran: example-fortran.html diff --git a/docs/source/example-cxx.rst b/docs/source/example-cxx.rst new file mode 100644 index 0000000..3b64982 --- /dev/null +++ b/docs/source/example-cxx.rst @@ -0,0 +1,369 @@ +Example: Wrapping a C++ model +============================= + +In this example, we'll use the *babelizer* +to wrap the *heat* model from the `bmi-example-cxx`_ repository, +allowing it to be run in Python. +The model and its BMI are written in C++. +To simplify package management in the example, +we'll use :term:`conda`. +We'll also use :term:`git` to obtain the model source code. + +Here are the steps we'll take to complete this example: + +#. Create a :term:`conda environment` that includes software to compile the + model and wrap it with the *babelizer* +#. Clone the `bmi-example-cxx`_ repository from GitHub and build the + *heat* model from source +#. Create a *babelizer* configuration file describing the *heat* model +#. Run the *babelizer* to generate a Python package, then build and install the package +#. Show the *heat* model running in Python through *pymt* + +Before we begin, +create a directory to hold our work: + +.. code:: bash + + mkdir example-cxx && cd example-cxx + +This directory is a starting point; +we'll add files and directories to it as we proceed through the example. +The final directory structure of ``example-c`` should look similar to that below. + +.. code:: bash + + example-cxx/ + ├── babel_heatcxx.toml + ├── bmi-example-cxx/ + ├── environment-cxx.yml + ├── pymt_heatcxx/ + └── test/ + + +Set up a conda environment +-------------------------- + +Start by setting up a :term:`conda environment` that includes the *babelizer*, +as well as a toolchain to build and install the model. +The necessary packages are listed in the conda environment file +:download:`environment-cxx.yml`: + +.. include:: environment-cxx.yml + :literal: + +:download:`Download ` this file +and place it in the ``example-cxx`` directory you created above. +Create the new environment with: + +.. code:: bash + + conda env create --file environment-cxx.yml + +When this command completes, +activate the environment +(on Linux and macOS, you may have to use ``source`` instead of ``conda``): + +.. code:: bash + + conda activate wrap-cxx + +The *wrap-cxx* environment now contains all the dependencies needed +to build, install, and wrap the *heat* model. + + +Build the *heat* model from source +---------------------------------- + +From the ``example-cxx`` directory, +clone the `bmi-example-cxx`_ repository from GitHub: + +.. code:: bash + + git clone https://github.com/csdms/bmi-example-cxx + +There are general `instructions`_ in the repository for building and installing +this package on Linux, macOS, and Windows. +We'll augment those instructions +with the note that we're installing into the *wrap-cxx* conda environment, +so the ``CONDA_PREFIX`` environment variable +should be used to specify the install path. + +Linux and macOS +............... + +On Linux and macOS, +use these commands to build and install the *heat* model: + +.. code:: bash + + cd bmi-example-cxx + mkdir build && cd build + cmake .. -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX + make install + +Verify the install by testing for the existence of the header +of the library containing the compiled *heat* model: + +.. code:: bash + + test -f $CONDA_PREFIX/include/bmi_heat.hxx ; echo $? + +A return of zero indicates success. + +Windows +....... + +Building on Windows requires +Microsoft Visual Studio 2019 or Microsoft Build Tools for Visual Studio 2019. +To build and install the *heat* model, +the following commands must be run in a `Developer Command Prompt`_: + +.. code:: bat + + cd bmi-example-cxx + mkdir build && cd build + cmake .. ^ + -G "NMake Makefiles" ^ + -DCMAKE_INSTALL_PREFIX=%CONDA_PREFIX% ^ + -DCMAKE_BUILD_TYPE=Release + cmake --build . --target install --config Release + +Verify the install by testing for the existence of the header +of the library containing the compiled *heat* model: + +.. code:: bat + + if not exist %LIBRARY_INC%\\bmi_heat.hxx exit 1 + +Create a *babelizer* configuration file +--------------------------------------- + +A *babelizer* configuration file provides information to the *babelizer* +about the model to be wrapped. + +Typically, we would use the ``babelize sample-config`` command +to create a sample configuration file that could then be edited. +However, to simplify this example, we have provided a completed +configuration file for the *heat* model. +:download:`Download ` the file +:download:`babel_heatcxx.toml` and copy it to the ``example-cxx`` directory. + +The configuration file looks like this: + +.. include:: babel_heatcxx.toml + :literal: + +For more information on the entries and sections of the *babelizer* configuration file, +see `Configuration file <./configuration.html>`_. + + +Wrap the model with the *babelizer* +----------------------------------- + +From the ``example-cxx`` directory, +generate a Python package for the model with the ``babelize init`` command: + +.. code:: bash + + babelize init babel_heatcxx.toml + +The results are placed in a new directory, ``pymt_heatcxx``, +under the current directory. + +Build and install the wrapped model +................................... + +Change to the ``pymt_heatcxx`` directory, +then build and install the Python package with *pip*: + +.. code:: bash + + cd pymt_heatcxx + pip install ."[dev]" + +The ``pip install`` command sets off a long list of messages, +at the end of which you'll hopefully see: + +.. code:: bash + + Successfully installed pymt-heatcxx + +Pause a moment to see what we've done. +Change back to the initial ``example-cxx`` directory, +make a new ``test`` directory, +and change to it: + +.. code:: bash + + cd .. + mkdir test && cd test + +Start a Python session (e.g., run ``python``) and try the following commands: + +.. code:: python + + from pymt_heatcxx import HeatCxx + m = HeatCxx() + m.get_component_name() + +You should see: + +.. code:: bash + + The 2D Heat Equation + +We've imported the *heat* model, +written in C++, +into Python! +Exit the Python session (e.g. type ``exit()``). + +Test the BMI +............ + +At this point, +it's a good idea to run the *bmi-tester* (`documentation `_) +over the model. +The *bmi-tester* exercises each BMI method exposed through Python, +ensuring it works correctly. +However, before running the *bmi-tester*, +one last piece of information is needed. +Like all models equipped with a BMI, +*heat* uses a :term:`configuration file` to specify initial parameter values. +Create a configuration file for *heat* at the command line with: + +.. code:: bash + + echo "1.5, 8.0, 6, 5" > config.txt + +or download the file :download:`config.txt `, +making sure to place it in the ``test`` directory. + +From the ``test`` directory, run the *bmi-tester*: + +.. code:: bash + + bmi-test pymt_heatcxx:HeatCxx --config-file=config.txt --root-dir=. -vvv + +This command sets off a long list of messages, +ending with + +.. code:: bash + + 🎉 All tests passed! + +if everything has been built correctly. + + +Make a *pymt* component +----------------------- + +The final step in wrapping the *heat* model +is to add metadata used by the `Python Modeling Tool`_, *pymt*. +CSDMS develops a set of standards, +the `CSDMS Model Metadata`_, +that provides a detailed and formalized description of a model. +The metadata allow *heat* to be run and be :term:`coupled ` +with other models that expose a BMI and have been similarly wrapped +with the *babelizer*. + +Recall the *babelizer* outputs the wrapped *heat* model +to the directory ``pymt_heatcxx``. +Under this directory, +the *babelizer* created a directory for *heat* model metadata, +``meta/HeatCxx``. +Change back to the ``pymt_heatcxx`` directory +and view the current metadata: + +.. code:: bash + + cd ../pymt_heatcxx + ls meta/HeatCxx/ + +which gives: + +.. code:: bash + + api.yaml + +The file ``api.yaml`` is automatically generated by the *babelizer*. +To complete the description of the *heat* model, +other metadata files are needed, including: + +* :download:`info.yaml ` +* :download:`run.yaml ` +* a :download:`templated model configuration file ` +* :download:`parameters.yaml ` + +`Descriptions`_ of these files and their roles +are given in the CSDMS Model Metadata repository. +Download each of the files using the links in the list above +and place them in the ``pymt_heatcxx/meta/HeatCxx`` directory +alongside ``api.yaml``. +The structure of the ``meta`` directory should look like: + +.. code:: bash + + meta/ + └── HeatCxx/ + ├── api.yaml + ├── heat.txt + ├── info.yaml + ├── parameters.yaml + └── run.yaml + +Run the babelized model in *pymt* +................................... + +Start a Python session and show that the *heat* model +can be called through *pymt*: + +.. code:: python + + from pymt.models import HeatCxx + m = HeatCxx() + m.name + +You should see: + +.. code:: bash + + The 2D Heat Equation + +A longer example, +:download:`pymt_heatcxx_ex.py `, +is included in the documentation. +For easy viewing, it's reproduced here verbatim: + +.. include:: examples/cxx/pymt_heatcxx_ex.py + :literal: + +:download:`Download ` this Python script, +then run it with: + +.. code:: bash + + python pymt_heatcxx_ex.py + + +Summary +------- + +Using the *babelizer*, we wrapped the *heat* model, which is written in C++. +It can now be called as a *pymt* component in Python. + +The steps for wrapping a model with the *babelizer* outlined in this example +can also be applied to models written in `C`_ and `Fortran`_. + + +.. + Links + +.. _bmi-example-cxx: https://github.com/csdms/bmi-example-cxx +.. _instructions: https://github.com/csdms/bmi-example-cxx/blob/master/README.md +.. _Developer Command Prompt: https://docs.microsoft.com/en-us/dotnet/framework/tools/developer-command-prompt-for-vs +.. _bmi-tester: https://bmi-tester.readthedocs.io +.. _Python Modeling Tool: https://pymt.readthedocs.io +.. _CSDMS Model Metadata: https://github.com/csdms/model_metadata +.. _Descriptions: https://github.com/csdms/model_metadata/blob/develop/README.rst +.. _C: example-c.html +.. _Fortran: example-fortran.html diff --git a/docs/source/example-fortran.rst b/docs/source/example-fortran.rst index 0d2c3c5..eb32ffc 100644 --- a/docs/source/example-fortran.rst +++ b/docs/source/example-fortran.rst @@ -383,7 +383,7 @@ It can now be called as a *pymt* component in Python. The steps for wrapping a model with the *babelizer* outlined in this example can also be applied to models written in C (:doc:`see the example `) -and C++. +and in C++ (:doc:`see the example `). .. diff --git a/docs/source/examples/cxx/config.txt b/docs/source/examples/cxx/config.txt new file mode 120000 index 0000000..f1a6cc6 --- /dev/null +++ b/docs/source/examples/cxx/config.txt @@ -0,0 +1 @@ +../config.txt \ No newline at end of file diff --git a/docs/source/examples/cxx/heat.txt b/docs/source/examples/cxx/heat.txt new file mode 100644 index 0000000..7ea565a --- /dev/null +++ b/docs/source/examples/cxx/heat.txt @@ -0,0 +1 @@ +{{thermal_diffusivity}}, {{run_duration}}, {{number_of_columns}}, {{number_of_rows}} diff --git a/docs/source/examples/cxx/heatcxx_ex.py b/docs/source/examples/cxx/heatcxx_ex.py new file mode 100644 index 0000000..7edb486 --- /dev/null +++ b/docs/source/examples/cxx/heatcxx_ex.py @@ -0,0 +1,67 @@ +"""An example of running the C++ heat model through its BMI.""" + +import numpy as np +from pymt_heatcxx import HeatCxx + +config_file = "config.txt" +np.set_printoptions(formatter={"float": "{: 6.1f}".format}) + + +# Instatiate an initialize the model. +m = HeatCxx() +print(m.get_component_name()) +m.initialize(config_file) + +# List the model's exchange items. +print("Input vars:", m.get_input_var_names()) +print("Output vars:", m.get_output_var_names()) + +# Get the grid_id for the plate_surface__temperature variable. +var_name = "plate_surface__temperature" +print(f"Variable {var_name}") +grid_id = m.get_var_grid(var_name) +print(" - grid id:", grid_id) + +# Get grid and variable info for plate_surface__temperature. +print(" - grid type:", m.get_grid_type(grid_id)) +grid_rank = m.get_grid_rank(grid_id) +print(" - rank:", grid_rank) +grid_shape = np.empty(grid_rank, dtype=np.int32) +m.get_grid_shape(grid_id, grid_shape) +print(" - shape:", grid_shape) +grid_size = m.get_grid_size(grid_id) +print(" - size:", grid_size) +grid_spacing = np.empty(grid_rank, dtype=np.float64) +m.get_grid_spacing(grid_id, grid_spacing) +print(" - spacing:", grid_spacing) +grid_origin = np.empty(grid_rank, dtype=np.float64) +m.get_grid_origin(grid_id, grid_origin) +print(" - origin:", grid_origin) +print(" - variable type:", m.get_var_type(var_name)) +print(" - units:", m.get_var_units(var_name)) +print(" - itemsize:", m.get_var_itemsize(var_name)) +print(" - nbytes:", m.get_var_nbytes(var_name)) + +# Get the initial temperature values. +val = np.empty(grid_shape, dtype=np.float64) +m.get_value(var_name, val) +print(" - initial values (gridded):") +print(val.reshape(np.roll(grid_shape, 1))) + +# Get time information from the model. +print("Start time:", m.get_start_time()) +print("End time:", m.get_end_time()) +print("Current time:", m.get_current_time()) +print("Time step:", m.get_time_step()) +print("Time units:", m.get_time_units()) + +# Advance the model by one time step. +m.update() +print("Updated time:", m.get_current_time()) + +# Advance the model until a later time. +m.update_until(5.0) +print("Later time:", m.get_current_time()) + +# Finalize the model. +m.finalize() diff --git a/docs/source/examples/cxx/info.yaml b/docs/source/examples/cxx/info.yaml new file mode 100644 index 0000000..5b89ed9 --- /dev/null +++ b/docs/source/examples/cxx/info.yaml @@ -0,0 +1,8 @@ +summary: | + This model solves the two-dimensional heat equation on a uniform + rectilinear grid. +url: https://github.com/csdms/bmi-example-cxx +author: CSDMS +email: csdms@colorado.edu +version: 0.1 +license: MIT diff --git a/docs/source/examples/cxx/parameters.yaml b/docs/source/examples/cxx/parameters.yaml new file mode 100644 index 0000000..3c1c5f2 --- /dev/null +++ b/docs/source/examples/cxx/parameters.yaml @@ -0,0 +1,42 @@ +run_duration: + description: Simulation run time + value: + type: float + default: 1.0 + units: s + range: + min: 0.0 + max: 1000000.0 + +thermal_diffusivity: + name: Thermal diffusivity + description: Thermal diffusivity + value: + type: float + default: 1.0 + range: + min: 0.0 + max: 10.0 + units: 'm2 s-1' + +number_of_rows: + name: Number of rows + description: Number of grid rows + value: + type: int + default: 10 + range: + min: 0 + max: 10000 + units: '1' + +number_of_columns: + name: Number of columns + description: Number of grid columns + value: + type: int + default: 10 + range: + min: 0 + max: 10000 + units: '1' diff --git a/docs/source/examples/cxx/pymt_heatcxx_ex.py b/docs/source/examples/cxx/pymt_heatcxx_ex.py new file mode 100644 index 0000000..f288c09 --- /dev/null +++ b/docs/source/examples/cxx/pymt_heatcxx_ex.py @@ -0,0 +1,61 @@ +"""Run the C++ heat model in pymt.""" + +from pymt.models import HeatCxx + +# Instantiate the component and get its name. +m = HeatCxx() +print(m.name) + +# Call setup, then initialize the model. +args = m.setup(".") +m.initialize(*args) + +# List the model's exchange items. +print("Number of input vars:", len(m.input_var_names)) +for var in m.input_var_names: + print(f" - {var}") +print("Number of output vars:", len(m.output_var_names)) +for var in m.output_var_names: + print(f" - {var}") + +# Get variable info. +var_name = m.output_var_names[0] +print(f"Variable {var_name}") +print(" - variable type:", m.var_type(var_name)) +print(" - units:", m.var_units(var_name)) +print(" - itemsize:", m.var_itemsize(var_name)) +print(" - nbytes:", m.var_nbytes(var_name)) +print(" - location:", m.var_location(var_name)) + +# Get grid info for variable. +grid_id = m.var_grid(var_name) +print(" - grid id:", grid_id) +print(" - grid type:", m.grid_type(grid_id)) +print(" - rank:", m.grid_ndim(grid_id)) +print(" - size:", m.grid_node_count(grid_id)) +print(" - shape:", m.grid_shape(grid_id)) + +# Get time information from the model. +print("Start time:", m.start_time) +print("End time:", m.end_time) +print("Current time:", m.time) +print("Time step:", m.time_step) +print("Time units:", m.time_units) + +# Get the initial values of the variable. +print(f"Get values of {var_name}...") +val = m.var[var_name].data +print(f" - values at time {m.time}:") +print(val) + +# Advance the model by one time step. +m.update() +print("Update: current time:", m.time) + +# Advance the model until a later time. +m.update_until(5.0) +print("Update: current time:", m.time) + +# Finalize the model. +m.finalize() +print("Done.") diff --git a/docs/source/examples/cxx/run.yaml b/docs/source/examples/cxx/run.yaml new file mode 100644 index 0000000..3b0457f --- /dev/null +++ b/docs/source/examples/cxx/run.yaml @@ -0,0 +1 @@ +config_file: heat.txt diff --git a/docs/source/index.rst b/docs/source/index.rst index 0a02505..312fe11 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -42,6 +42,7 @@ User Guide cli example-c example-fortran + example-cxx glossary diff --git a/news/105.doc b/news/105.doc new file mode 100644 index 0000000..25159f4 --- /dev/null +++ b/news/105.doc @@ -0,0 +1,3 @@ + +Added a :doc:`C++ example ` to the documentation similar to the C +and Fortran examples.