diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cbdb9d9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: Docker Hub + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - '!v[0-9]+.[0-9]+.[0-9]+[ab][0-9]+' + +env: + REPO: csdms/cem-grpc4bmi + +jobs: + + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Get version + id: vars + run: echo "version=${GITHUB_REF:11}" >> $GITHUB_OUTPUT + + - name: Set up Docker buildx + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ env.REPO }}:latest,${{ env.REPO }}:${{ steps.vars.outputs.version }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..39e49b9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Test + +on: + push: + pull_request: + schedule: + - cron: '47 4 3 * *' # 4:47a 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 diff --git a/README.md b/README.md index e9bab73..5cbdced 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,23 @@ If the image isn't found locally, it's pulled from Docker Hub For more in-depth examples of running the CEM model through grpc4bmi, see the [examples](./examples) directory. +## Developer notes + +A versioned, multiplatform image is hosted on Docker Hub +at [csdms/cem-grpc4bmi](https://hub.docker.com/r/csdms/cem-grpc4bmi). +This image is automatically built and pushed to Docker Hub +with the [release](./.github/workflows/release.yml) CI workflow. +The workflow is only run when the repository is tagged. +To manually build and push an update, run: +``` +docker buildx build --platform linux/amd64,linux/arm64 -t csdms/cem-grpc4bmi:latest --push . +``` +A user can pull this image from Docker Hub with: +``` +docker pull csdms/cem-grpc4bmi +``` +optionally with the `latest` tag or with a version tag. + ## Acknowledgment This work is supported by the U.S. National Science Foundation under Award No. [2103878](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2103878), *Frameworks: Collaborative Research: Integrative Cyberinfrastructure for Next-Generation Modeling Science*. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..41550b5 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,24 @@ +# 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 CEM 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, +as well as two image files, +`shoreline_initial.png` and `shoreline_final.png`. + +Start JupyterLab and run the example notebook: +```sh +jupyter lab run-model-through-grpc4bmi.ipynb +``` diff --git a/examples/cem.txt b/examples/cem.txt new file mode 100644 index 0000000..4f4d4b8 --- /dev/null +++ b/examples/cem.txt @@ -0,0 +1,2 @@ +50, 1000, 1000.0, 1 +0.01, 10.0, 0.001 \ No newline at end of file diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..1aa7d84 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1,4 @@ +grpc4bmi +jupyter +matplotlib +tqdm diff --git a/examples/run-model-through-grpc4bmi.ipynb b/examples/run-model-through-grpc4bmi.ipynb new file mode 100644 index 0000000..ca378eb --- /dev/null +++ b/examples/run-model-through-grpc4bmi.ipynb @@ -0,0 +1,450 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a79c2779-9015-40b7-9a53-aa41c87880f5", + "metadata": {}, + "source": [ + "# Run the CEM model through *grpc4bmi*" + ] + }, + { + "cell_type": "markdown", + "id": "e4049ff7-5c9e-4479-81ea-1bba1fe4810c", + "metadata": {}, + "source": [ + "Run the [Coastline Evolution Model](https://csdms.colorado.edu/wiki/Model:CEM) (CEM) in Python through [grpc4bmi](https://grpc4bmi.readthedocs.io).\n", + "\n", + "CEM addresses predominately sandy, wave-dominated coastlines on time-scales ranging from years to millenia and on spatial scales ranging from kilometers to hundreds of kilometers. Shoreline evolution results from gradients in wave-driven alongshore sediment transport. At its most basic level, the model follows the standard 'one-line' modeling approach, where the cross-shore dimension is collapsed into a single data point. However, the model allows the plan-view shoreline to take on arbitrary local orientations, and even fold back upon itself, as complex shapes such as capes and spits form under some wave climates (distributions of wave influences from different approach angles). The model can also represent the geology underlying the sandy coastline and shoreface in a simplified manner and enables the simulation of coastline evolution when sediment supply from an eroding shoreface may be constrained. CEM also supports the simulation of human manipulations to coastline evolution through beach nourishment or hard structures.\n", + "\n", + "View the model source code and its BMI at https://github.com/csdms-contrib/cem/tree/v0." + ] + }, + { + "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\n", + "import math\n", + "import matplotlib.pyplot as plt\n", + "from tqdm import trange" + ] + }, + { + "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 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/cem-grpc4bmi:latest\"\n", + "BMI_PORT = 55555\n", + "CONFIG_FILE = pathlib.Path(\"cem.txt\")" + ] + }, + { + "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 the 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 CEM through its BMI with 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 model." + ] + }, + { + "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 time information provided by 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": [ + "The main output variable for this model is sea water depth\n", + "(using the CSDMS Standard Name `sea_water__depth`).\n", + "Get the identifier for the grid on which this variable is defined." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7ffb63-48a8-452e-a023-1141209ef204", + "metadata": {}, + "outputs": [], + "source": [ + "grid_id = m.get_var_grid('sea_water__depth')\n", + "print(\"Grid id:\", grid_id)" + ] + }, + { + "cell_type": "markdown", + "id": "f1852344-0819-4484-966c-80c6ffdb2b65", + "metadata": {}, + "source": [ + "Get attributes of the grid." + ] + }, + { + "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": "f395ce25-fe4e-494d-b22f-903dc6d17060", + "metadata": {}, + "source": [ + "Allocate memory for the sea water depth variable and get its current values from CEM.\n", + "Note that *get_value* expects a one-dimensional array to receive output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0800b70-c3f1-46c7-b13f-f1ee13597505", + "metadata": {}, + "outputs": [], + "source": [ + "z = np.empty(shape, dtype=float).flatten()\n", + "m.get_value('sea_water__depth', z)\n", + "\n", + "z.reshape(shape)" + ] + }, + { + "cell_type": "markdown", + "id": "c236ebab-984c-422c-af8b-6cbb629bbafe", + "metadata": {}, + "source": [ + "Define a convenience function for plotting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab934f6e-4d95-445d-85a2-71ad266ae20d", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_coast(depth, spacing=(1000,1000)):\n", + " xmin, xmax = 0., depth.shape[1] * spacing[0] * 1e-3\n", + " ymin, ymax = 0., depth.shape[0] * spacing[1] * 1e-3\n", + "\n", + " plt.imshow(depth, extent=[xmin, xmax, ymin, ymax], origin='lower', cmap='ocean', aspect=\"auto\")\n", + " plt.colorbar().ax.set_ylabel('Water Depth (m)')\n", + " plt.xlabel('Along shore (km)')\n", + " plt.ylabel('Cross shore (km)')" + ] + }, + { + "cell_type": "markdown", + "id": "e3eb0fb8-9bbd-44ef-9900-6951f65d4cb7", + "metadata": {}, + "source": [ + "This function generates plots that look like the one below. We begin with a flat delta (green) and a linear coastline at `y` = 30 km. The bathymetry drops off linearly to the top of the domain." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a014c547-8aa9-4d3d-a49a-6d91ec99573d", + "metadata": {}, + "outputs": [], + "source": [ + "plot_coast(z.reshape(shape))" + ] + }, + { + "cell_type": "markdown", + "id": "199d2c74-31e7-47b4-9a3c-d44c34af8c60", + "metadata": {}, + "source": [ + "Before running the model, set a few input parameters.\n", + "These parameters represent the wave height, wave period, and wave angle of the incoming waves to the coastline." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "415358a4-6bfc-4f1f-98c5-6a33d6d107dc", + "metadata": {}, + "outputs": [], + "source": [ + "_one = np.ones(1, dtype=float)\n", + "\n", + "m.set_value(\"sea_surface_water_wave__height\", _one * 2.0)\n", + "m.set_value(\"sea_surface_water_wave__period\", _one * 7.0)\n", + "m.set_value(\"sea_surface_water_wave__azimuth_angle_of_opposite_of_phase_velocity\", _one * math.radians(45))" + ] + }, + { + "cell_type": "markdown", + "id": "b65d22a0-f61a-4930-831b-46ecd76701ca", + "metadata": {}, + "source": [ + "Add sediment discharge to the ocean at a set of 10 cells on the shoreline.\n", + "Allocate memory for the sediment discharge array and set the discharge at the coastal cells to some value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b591703f-0366-49f4-a6f5-1787b024305c", + "metadata": {}, + "outputs": [], + "source": [ + "qs = np.zeros_like(z.reshape(shape))\n", + "qs[0, 295:305] = 5000\n", + "qs" + ] + }, + { + "cell_type": "markdown", + "id": "ef7d8a83-deed-452f-a00d-da1928de8b82", + "metadata": {}, + "source": [ + "Run the model, updating the bedload flux at each time step." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "514a592b-15cb-4372-b8fb-feb0d549e396", + "metadata": {}, + "outputs": [], + "source": [ + "n_days = 360\n", + "n_time_steps = int(n_days / m.get_time_step())\n", + "for _ in trange(n_time_steps):\n", + " m.set_value(\"land_surface_water_sediment~bedload__mass_flow_rate\", qs)\n", + " m.update()" + ] + }, + { + "cell_type": "markdown", + "id": "f7bdf185-fb8d-4e30-b0cb-e8e2c10c9f60", + "metadata": {}, + "source": [ + "Get the final values of sea water depth and display them." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68016487-369f-4f50-9e84-33cf6b6f3e01", + "metadata": {}, + "outputs": [], + "source": [ + "m.get_value(\"sea_water__depth\", z)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6e918711-5e3a-4088-b4ba-09f225da1ef7", + "metadata": {}, + "outputs": [], + "source": [ + "plot_coast(z.reshape(shape))" + ] + }, + { + "cell_type": "markdown", + "id": "e138e3b0-8cee-461c-ae40-39400a81432d", + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/run-model-through-grpc4bmi.py b/examples/run-model-through-grpc4bmi.py new file mode 100644 index 0000000..db04ab1 --- /dev/null +++ b/examples/run-model-through-grpc4bmi.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# Run the [Coastline Evolution Model](https://csdms.colorado.edu/wiki/Model:CEM) +# (CEM) in Python through [grpc4bmi](https://grpc4bmi.readthedocs.io). +# +# CEM addresses predominately sandy, wave-dominated coastlines on time-scales +# ranging from years to millenia and on spatial scales ranging from kilometers +# to hundreds of kilometers. Shoreline evolution results from gradients in +# wave-driven alongshore sediment transport. At its most basic level, the model +# follows the standard 'one-line' modeling approach, where the cross-shore +# dimension is collapsed into a single data point. However, the model allows the +# plan-view shoreline to take on arbitrary local orientations, and even fold +# back upon itself, as complex shapes such as capes and spits form under some +# wave climates (distributions of wave influences from different approach +# angles). The model can also represent the geology underlying the sandy +# coastline and shoreface in a simplified manner and enables the simulation of +# coastline evolution when sediment supply from an eroding shoreface may be +# constrained. CEM also supports the simulation of human manipulations to +# coastline evolution through beach nourishment or hard structures. +# +# View the model source code and its BMI at +# https://github.com/csdms-contrib/cem/tree/v0. + +# Start by importing some helper libraries. +import os +import pathlib +import numpy as np +import math +import matplotlib.pyplot as plt +from tqdm import trange + +# Next, import the grpc4bmi Docker client. +from grpc4bmi.bmi_client_docker import BmiClientDocker + +# Set variables: +# * which Docker image to use, +# * the port exposed through the image, and +# * the location of the configuration file used for the model. +DOCKER_IMAGE = "csdms/cem-grpc4bmi:latest" +BMI_PORT = 55555 +CONFIG_FILE = pathlib.Path("cem.txt") + +# Create a model instance, `m`, through the grpc4bmi Docker client. +m = BmiClientDocker(image=DOCKER_IMAGE, image_port=BMI_PORT, work_dir=".") + +# Show the name of the model. +m.get_component_name() + +# Start CEM through its BMI with a configuration file. +m.initialize(str(CONFIG_FILE)) + +# Show the input and output variables for the model. +m.get_input_var_names(), m.get_output_var_names() + +# Check time information provided by 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()) + +# The main output variable for this model is sea water depth +# (using the CSDMS Standard Name `sea_water__depth`). +# Get the identifier for the grid on which this variable is defined. +grid_id = m.get_var_grid('sea_water__depth') +print("Grid id:", grid_id) + +# Get attributes of the grid. +print("Grid type:", m.get_grid_type(grid_id)) +rank = m.get_grid_rank(grid_id) +print("Grid rank:", rank) +shape = np.ndarray(rank, dtype=int) +m.get_grid_shape(grid_id, shape) +print("Grid shape:", shape) +spacing = np.ndarray(rank, dtype=float) +m.get_grid_spacing(grid_id, spacing) +print("Grid spacing:", spacing) + +# Allocate memory for the sea water depth variable and get its current values from CEM. +# Note that *get_value* expects a one-dimensional array to receive output. +z = np.empty(shape, dtype=float).flatten() +m.get_value('sea_water__depth', z) +z.reshape(shape) + +# Define a convenience function for plotting. +def plot_coast(depth, spacing=(1000,1000)): + xmin, xmax = 0., depth.shape[1] * spacing[0] * 1e-3 + ymin, ymax = 0., depth.shape[0] * spacing[1] * 1e-3 + + plt.imshow(depth, extent=[xmin, xmax, ymin, ymax], origin='lower', cmap='ocean', aspect="auto") + plt.colorbar().ax.set_ylabel('Water Depth (m)') + plt.xlabel('Along shore (km)') + plt.ylabel('Cross shore (km)') + +# This function generates plots that look like the one below. We begin with a flat delta (green) and a linear coastline at `y` = 30 km. The bathymetry drops off linearly to the top of the domain. +plot_coast(z.reshape(shape)) +plt.savefig("shoreline_initial.png", dpi=96) +plt.close() + +# Before running the model, set a few input parameters. +# These parameters represent the wave height, wave period, and wave angle of the incoming waves to the coastline. +_one = np.ones(1, dtype=float) +m.set_value("sea_surface_water_wave__height", _one * 2.0) +m.set_value("sea_surface_water_wave__period", _one * 7.0) +m.set_value("sea_surface_water_wave__azimuth_angle_of_opposite_of_phase_velocity", _one * math.radians(45)) + +# Add sediment discharge to the ocean at a set of 10 cells on the shoreline. +# Allocate memory for the sediment discharge array and set the discharge at the coastal cells to some value. +qs = np.zeros_like(z.reshape(shape)) +qs[0, 295:305] = 5000 +qs + +# Run the model, updating the bedload flux at each time step. +n_days = 360 +n_time_steps = int(n_days / m.get_time_step()) +for _ in trange(n_time_steps): + m.set_value("land_surface_water_sediment~bedload__mass_flow_rate", qs) + m.update() + +# Get the final values of sea water depth and display them. +m.get_value("sea_water__depth", z) +plot_coast(z.reshape(shape)) +plt.savefig("shoreline_final.png", dpi=96) +plt.close() + +# Stop the model and clean up any resources it allocates. +m.finalize() + +# Stop the container running through grpc4bmi. +# This is needed by grpc4bmi to properly deallocate the resources it uses. +# It may take a few moments. +del m