From 6cbb6602f76ea3bd2301ad925f1f0039cb2fc727 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Wed, 27 Aug 2025 15:40:14 -0600 Subject: [PATCH 1/8] Add requirements file and install/run directions --- examples/README.md | 22 ++++++++++++++++++++++ examples/requirements.txt | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/requirements.txt diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..3e87455 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,22 @@ +# examples + +Two examples--a [Python script](./run-model-through-grpc4bmi.py) +and a [Jupyter notebook](./run-model-through-grpc4bmi.ipynb)--that demonstrate how to run +the containerized Python `Heat` model through the grpc4bmi server built in this project. + +In a virtual environment, +install grpc4bmi and other dependencies: +```sh +pip install -r requirements.txt +``` + +Run the Python example: +```sh +python run-model-through-grpc4bmi.py +``` +It'll produce output in the terminal. + +Start JupyterLab and run the example notebook: +```sh +jupyter lab run-model-through-grpc4bmi.ipynb +``` diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..9885329 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1,2 @@ +grpc4bmi +jupyter From f65488f2b7e93d922cb12ce6e69dea10fd43da66 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Wed, 27 Aug 2025 15:41:19 -0600 Subject: [PATCH 2/8] Rename script to match notebook --- .../{run-model-from-grpc4bmi.py => run-model-through-grpc4bmi.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{run-model-from-grpc4bmi.py => run-model-through-grpc4bmi.py} (100%) diff --git a/examples/run-model-from-grpc4bmi.py b/examples/run-model-through-grpc4bmi.py similarity index 100% rename from examples/run-model-from-grpc4bmi.py rename to examples/run-model-through-grpc4bmi.py From 531310e10265110dafde2df28776ea65b266b275 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Wed, 27 Aug 2025 15:48:43 -0600 Subject: [PATCH 3/8] Update Python script to match cxx example --- examples/run-model-through-grpc4bmi.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/examples/run-model-through-grpc4bmi.py b/examples/run-model-through-grpc4bmi.py index 01949db..254305f 100644 --- a/examples/run-model-through-grpc4bmi.py +++ b/examples/run-model-through-grpc4bmi.py @@ -1,22 +1,26 @@ #!/usr/bin/env python -# Run the Python BMI example `Heat` model through its BMI. +# Run the containerized Python `Heat` model through grpc4bmi. # -# `Heat` models the diffusion of temperature on a uniform rectangular plate with -# Dirichlet boundary conditions. View the model source code and its BMI at -# https://github.com/csdms/bmi-example-python. +# `Heat` models the diffusion of temperature on a uniform rectangular plate with Dirichlet boundary conditions. +# View the model source code and its BMI at https://github.com/csdms/bmi-example-python. +# Start by importing some helper libraries. import os import pathlib import numpy as np +# Next, import the grpc4bmi Docker client. from grpc4bmi.bmi_client_docker import BmiClientDocker -DOCKER_IMAGE = "csdms/bmi-example-python-grpc4bmi" +# Set variables: +# * which Docker image to use, +# * the port exposed through the image, and +# * the location in the image of the configuration file used for the model. +DOCKER_IMAGE = "csdms/bmi-example-python-grpc4bmi:latest" BMI_PORT = 55555 REPO_PATH = pathlib.Path("/opt/bmi-example-python") CONFIG_FILE = REPO_PATH / "examples" / "heat.yaml" - # Create a model instance from the container. x = BmiClientDocker(image=DOCKER_IMAGE, image_port=BMI_PORT, work_dir=".") @@ -73,7 +77,7 @@ print(temperature_flat.reshape(shape)) # Advance the model to some distant time. -distant_time = 2.0 +distant_time = 10*x.get_time_step() while x.get_current_time() < distant_time: x.update() From fca233ed4bce4bb96488ec901ce87956d45cb878 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Wed, 27 Aug 2025 16:41:46 -0600 Subject: [PATCH 4/8] Add notebook example --- examples/run-model-through-grpc4bmi.ipynb | 425 ++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 examples/run-model-through-grpc4bmi.ipynb diff --git a/examples/run-model-through-grpc4bmi.ipynb b/examples/run-model-through-grpc4bmi.ipynb new file mode 100644 index 0000000..a90a6c8 --- /dev/null +++ b/examples/run-model-through-grpc4bmi.ipynb @@ -0,0 +1,425 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a79c2779-9015-40b7-9a53-aa41c87880f5", + "metadata": {}, + "source": [ + "# Run the Python `Heat` model through *grpc4bmi*" + ] + }, + { + "cell_type": "markdown", + "id": "e4049ff7-5c9e-4479-81ea-1bba1fe4810c", + "metadata": {}, + "source": [ + "Run the containerized `Heat` model in Python through [grpc4bmi](https://grpc4bmi.readthedocs.io).\n", + "\n", + "`Heat` models the diffusion of temperature on a uniform rectangular plate with Dirichlet boundary conditions.\n", + "View the model source code and its BMI at https://github.com/csdms/bmi-example-cxx." + ] + }, + { + "cell_type": "markdown", + "id": "2ec7d6db-db56-4772-90df-34f3639cad4b", + "metadata": {}, + "source": [ + "Start by importing some helper libraries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46b137fd-a63c-4bbf-be9e-bf9dc24e9084", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import pathlib\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "f324505d-234e-4c78-b0a3-bfbccc098f6e", + "metadata": {}, + "source": [ + "Next, import the grpc4bmi Docker client." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d903ed6-77e7-4b49-93f2-04c9ec6df813", + "metadata": {}, + "outputs": [], + "source": [ + "from grpc4bmi.bmi_client_docker import BmiClientDocker" + ] + }, + { + "cell_type": "markdown", + "id": "06be2075-9643-43ca-bfde-d1067bc4c27d", + "metadata": {}, + "source": [ + "Set variables:\n", + "* which Docker image to use,\n", + "* the port exposed through the image, and\n", + "* the location in the image of the configuration file used for the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35bee1ec-0263-4f04-ac7a-04f74b775791", + "metadata": {}, + "outputs": [], + "source": [ + "DOCKER_IMAGE = \"csdms/bmi-example-python-grpc4bmi:latest\"\n", + "BMI_PORT = 55555\n", + "CONFIG_FILE = pathlib.Path(\"/opt/bmi-example-cxx\") / \"testing\" / \"heat.yaml\"" + ] + }, + { + "cell_type": "markdown", + "id": "f76b4e13-1fbc-4f87-be30-3e4b4e3016d3", + "metadata": {}, + "source": [ + "Create a model instance (`m`) through the grpc4bmi Docker client." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a2bb4d5a-3e10-4de9-a30c-b6cabfa48c0f", + "metadata": {}, + "outputs": [], + "source": [ + "m = BmiClientDocker(image=DOCKER_IMAGE, image_port=BMI_PORT, work_dir=\".\")" + ] + }, + { + "cell_type": "markdown", + "id": "80f0787f-c5c7-4e42-b46e-b0acd1b4f90f", + "metadata": {}, + "source": [ + "Show the name of this model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2097c3b3-eb2b-4f1b-b79d-936b198dd439", + "metadata": {}, + "outputs": [], + "source": [ + "m.get_component_name()" + ] + }, + { + "cell_type": "markdown", + "id": "ab6887ca-eaf0-46b5-8666-8e27264c752f", + "metadata": {}, + "source": [ + "Start the `Heat` model through its BMI using a configuration file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba740f4c-e458-44b9-b600-2b1a76f0e978", + "metadata": {}, + "outputs": [], + "source": [ + "m.initialize(str(CONFIG_FILE))" + ] + }, + { + "cell_type": "markdown", + "id": "21e50db1-fc88-4627-992d-f7ab390e7101", + "metadata": {}, + "source": [ + "Show the input and output variables for the component." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e48268ad-7abe-4572-9967-555053e4fecf", + "metadata": {}, + "outputs": [], + "source": [ + "m.get_input_var_names(), m.get_output_var_names()" + ] + }, + { + "cell_type": "markdown", + "id": "f5608782-0445-43f6-99b7-51e40cead446", + "metadata": {}, + "source": [ + "Check the time information for the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bb0af6d-fa67-443e-af80-6b32eadb76b5", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Start time:\", m.get_start_time())\n", + "print(\"End time:\", m.get_end_time())\n", + "print(\"Current time:\", m.get_current_time())\n", + "print(\"Time step:\", m.get_time_step())\n", + "print(\"Time units:\", m.get_time_units())" + ] + }, + { + "cell_type": "markdown", + "id": "fa6efccb-adb1-484e-be85-1dfa336d8381", + "metadata": {}, + "source": [ + "Get the identifier for the grid on which the temperature variable is defined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7ffb63-48a8-452e-a023-1141209ef204", + "metadata": {}, + "outputs": [], + "source": [ + "grid_id = m.get_var_grid(\"plate_surface__temperature\")\n", + "print(\"Grid id:\", grid_id)" + ] + }, + { + "cell_type": "markdown", + "id": "f1852344-0819-4484-966c-80c6ffdb2b65", + "metadata": {}, + "source": [ + "Get grid attributes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "082776fa-e87f-4c8f-89e8-5bfdd05e72da", + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Grid type:\", m.get_grid_type(grid_id))\n", + "\n", + "rank = m.get_grid_rank(grid_id)\n", + "print(\"Grid rank:\", rank)\n", + "\n", + "shape = np.ndarray(rank, dtype=int)\n", + "m.get_grid_shape(grid_id, shape)\n", + "print(\"Grid shape:\", shape)\n", + "\n", + "spacing = np.ndarray(rank, dtype=float)\n", + "m.get_grid_spacing(grid_id, spacing)\n", + "print(\"Grid spacing:\", spacing)" + ] + }, + { + "cell_type": "markdown", + "id": "bddd7f4e-4d04-4ba5-a944-1c93202fb390", + "metadata": {}, + "source": [ + "Through the model's BMI, zero out the initial temperature field, except for an\n", + "impulse near the middle. Note that *set_value* expects a one-dimensional array\n", + "for input." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44601779-4b97-4051-965b-9f67b77203e2", + "metadata": {}, + "outputs": [], + "source": [ + "temperature = np.zeros(shape)\n", + "temperature[3, 4] = 100.0\n", + "m.set_value(\"plate_surface__temperature\", temperature)" + ] + }, + { + "cell_type": "markdown", + "id": "974c47d8-f412-41bd-81e4-7b61f192050e", + "metadata": {}, + "source": [ + "Check that the temperature field has been updated. Note that *get_value*\n", + "expects a one-dimensional array to receive output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40bbeb5f-b932-4438-871a-2fed6782f1f2", + "metadata": {}, + "outputs": [], + "source": [ + "temperature_flat = np.empty_like(temperature).flatten()\n", + "m.get_value(\"plate_surface__temperature\", temperature_flat)\n", + "\n", + "temperature_flat.reshape(shape)" + ] + }, + { + "cell_type": "markdown", + "id": "f7497b70-a73f-470e-aa4d-1899a5d4a2ab", + "metadata": {}, + "source": [ + "Advance the model by a single time step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50e9f1e4-834d-4f0e-a0b3-e6f28d74866b", + "metadata": {}, + "outputs": [], + "source": [ + "m.update()" + ] + }, + { + "cell_type": "markdown", + "id": "a29e7d17-3726-4e7e-8926-6cb618332788", + "metadata": {}, + "source": [ + "View the new state of the temperature field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e03e2141-e016-455a-bb28-7c1f11d17b9f", + "metadata": {}, + "outputs": [], + "source": [ + "m.get_value(\"plate_surface__temperature\", temperature_flat)\n", + "\n", + "temperature_flat.reshape(shape)" + ] + }, + { + "cell_type": "markdown", + "id": "762bfd03-b219-47f8-968a-df21cde51342", + "metadata": {}, + "source": [ + "Advance the model to some distant time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0fb7a8a6-fd85-4694-80a4-c27d0965bf8c", + "metadata": {}, + "outputs": [], + "source": [ + "distant_time = 10*m.get_time_step()\n", + "while m.get_current_time() < distant_time:\n", + " m.update()" + ] + }, + { + "cell_type": "markdown", + "id": "1079d0c6-9bee-4e53-b21f-413d9dcde3a3", + "metadata": {}, + "source": [ + "View the final state of the temperature field." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "016b854b-bf73-406b-96d9-1b4e32604a0e", + "metadata": {}, + "outputs": [], + "source": [ + "m.get_value(\"plate_surface__temperature\", temperature_flat)\n", + "\n", + "temperature_flat.reshape(shape)" + ] + }, + { + "cell_type": "markdown", + "id": "b8d91aee-ead3-4121-979e-cb8939c1a07b", + "metadata": {}, + "source": [ + "Note that temperature isn't conserved on the plate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4a2c639-c286-4624-8844-18c174657cf2", + "metadata": {}, + "outputs": [], + "source": [ + "temperature_flat.sum()" + ] + }, + { + "cell_type": "markdown", + "id": "e5a25b9f-9006-4901-937b-9246041faaca", + "metadata": {}, + "source": [ + "Stop the model and clean up any resources it allocates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1668446a-0afa-4b7e-8d3b-137d8d4c4d4f", + "metadata": {}, + "outputs": [], + "source": [ + "m.finalize()" + ] + }, + { + "cell_type": "markdown", + "id": "291cb5ce-c081-490d-8256-dd90e1586a64", + "metadata": {}, + "source": [ + "Stop the container running through grpc4bmi.\n", + "This is needed by grpc4bmi to properly deallocate the resources it uses.\n", + "It may take a few moments." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22039bd2-704b-4fb5-a7bc-8ed8eb6e4bff", + "metadata": {}, + "outputs": [], + "source": [ + "del m" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From de19c4965532f02e61a62e7f96079aad10491393 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 28 Aug 2025 09:58:33 -0600 Subject: [PATCH 5/8] Set correct path to model config file --- examples/run-model-through-grpc4bmi.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/run-model-through-grpc4bmi.ipynb b/examples/run-model-through-grpc4bmi.ipynb index a90a6c8..d10e508 100644 --- a/examples/run-model-through-grpc4bmi.ipynb +++ b/examples/run-model-through-grpc4bmi.ipynb @@ -77,7 +77,7 @@ "source": [ "DOCKER_IMAGE = \"csdms/bmi-example-python-grpc4bmi:latest\"\n", "BMI_PORT = 55555\n", - "CONFIG_FILE = pathlib.Path(\"/opt/bmi-example-cxx\") / \"testing\" / \"heat.yaml\"" + "CONFIG_FILE = pathlib.Path(\"/opt/bmi-example-python\") / \"examples\" / \"heat.yaml\"" ] }, { @@ -417,7 +417,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.7" + "version": "3.12.11" } }, "nbformat": 4, From e24fa2804736382e6922d7310f785c9aae1c83d9 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 28 Aug 2025 10:02:02 -0600 Subject: [PATCH 6/8] Add a gha workflow to check examples --- .github/workflows/test.yml | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..462b60b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Test + +on: + push: + pull_request: + schedule: + - cron: '31 4 3 * *' # 4:31a on third day of the month + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + + test: + name: Run tests + if: + github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository + + runs-on: ${{ matrix.os }} + + defaults: + run: + shell: bash -l {0} + + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Install Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install requirements + run: pip install pytest nbmake -r examples/requirements.txt + + - name: Check example script + working-directory: ${{ github.workspace }}/examples + run: | + python run-model-through-grpc4bmi.py + + - name: Check example notebook + run: | + pytest examples --nbmake --nbmake-timeout=3000 -v From b96f2eb42ebc0448238cf7a3f9cc588409196c08 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 28 Aug 2025 10:10:48 -0600 Subject: [PATCH 7/8] Add status badges --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c976c1d..3ed3148 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +[![Basic Model Interface](https://img.shields.io/badge/CSDMS-Basic%20Model%20Interface-green.svg)](https://bmi.readthedocs.io/) +[![Test](https://github.com/csdms/bmi-example-python-grpc4bmi/actions/workflows/test.yml/badge.svg)](https://github.com/csdms/bmi-example-python-grpc4bmi/actions/workflows/test.yml) +[![Docker Hub](https://github.com/csdms/bmi-example-python-grpc4bmi/actions/workflows/release.yml/badge.svg)](https://github.com/csdms/bmi-example-python-grpc4bmi/actions/workflows/release.yml) +![Docker Image Version](https://img.shields.io/docker/v/csdms/bmi-example-python-grpc4bmi) + # bmi-example-python-grpc4bmi Set up a [grpc4bmi](https://grpc4bmi.readthedocs.io) server From 73fb08a8961fb67ad3a4183e0700ae93a05d45c0 Mon Sep 17 00:00:00 2001 From: Mark Piper Date: Thu, 28 Aug 2025 10:18:35 -0600 Subject: [PATCH 8/8] Update text and check links --- README.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 3ed3148..095d25a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Set up a [grpc4bmi](https://grpc4bmi.readthedocs.io) server to run a containerized version -of the [Basic Model Interface](https://bmi.readthedocs.io) +of the [Basic Model Interface](https://bmi.readthedocs.io) (BMI) [Python example](https://github.com/csdms/bmi-example-python). ## Build @@ -16,19 +16,18 @@ Build the server image locally with: ``` docker build --tag bmi-example-python-grpc4bmi . ``` -The image is built on the [csdms/bmi-example-python](https://hub.docker.com/r/csdms/bmi-example-python) image. +The image is built on the [csdms/grpc4bmi](https://hub.docker.com/r/csdms/grpc4bmi) base image, +which is built on the [condaforge/miniforge3](https://hub.docker.com/r/condaforge/miniforge3) base image. The OS is Linux/Ubuntu. -`conda` and `mamba` are installed in `CONDA_DIR=opt/conda`, -and the *base* environment is activated, The grpc4bmi Python server, -as well as the Python BMI specification and example +as well as the Python BMI mappings and example (consisting of the *Heat* model and a BMI implementation, *BmiHeat*) -are installed into it. +are installed in `CONDA_DIR=/opt/conda`. ## Run -Use the grpc4bmi [Docker client](https://grpc4bmi.readthedocs.io/en/latest/container/usage.html#docker) -to access the BMI methods of the containerized model. +Use the grpc4bmi Docker client to access the BMI methods of the containerized model. + Install grpc4bmi with *pip*: ``` pip install grpc4bmi @@ -47,7 +46,7 @@ del m # stop container cleanly If the image isn't found locally, it's pulled from Docker Hub (e.g., try substituting `IMAGE_NAME="csdms/bmi-example-python-grpc4bmi"` above). -For a more in-depth example of running the *Heat* model from grpc4bmi, +For more in-depth examples of running the *Heat* model from grpc4bmi, see the [examples](./examples) directory. ## Developer notes @@ -70,7 +69,7 @@ optionally with the `latest` tag or with a version tag. ## What are the Basic Model Interface and grpc4bmi? The Basic Model Interface (BMI) is a set of functions for querying, modifying, running, and coupling models. -Learn more at https://bmi.readthedocs.io/. +Learn more at https://bmi.csdms.io/. grpc4bmi is a [gRPC](https://grpc.io/) wrapper for a model with a BMI. Learn more at https://grpc4bmi.readthedocs.io/.