diff --git a/README.md b/README.md index b5bc4eee892..1c23d63552a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ MetPy [![Conda Tests](https://github.com/Unidata/MetPy/workflows/Conda%20Tests/badge.svg)](https://github.com/Unidata/MetPy/actions?query=workflow%3A%22Conda+Tests%22) [![Code Coverage Status](https://codecov.io/github/Unidata/MetPy/coverage.svg?branch=main)](https://codecov.io/github/Unidata/MetPy?branch=main) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2e64843f595c42e991457cb76fcfa769)](https://www.codacy.com/gh/Unidata/MetPy/dashboard) +[![asv](https://img.shields.io/badge/benchmarked%20by-asv-blue.svg?style=flat)](https://unidata.github.io/MetPy-benchmark) [![Maintainability](https://qlty.sh/gh/Unidata/projects/MetPy/maintainability.svg)](https://qlty.sh/gh/Unidata/projects/MetPy) MetPy is a collection of tools in Python for reading, visualizing and diff --git a/benchmarks/Dockerfile b/benchmarks/Dockerfile new file mode 100644 index 00000000000..a14d9b38c50 --- /dev/null +++ b/benchmarks/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.12 + +RUN pip install --no-cache-dir netcdf4 asv pysu metpy + +COPY --chmod=700 entrypoint.sh / + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/benchmarks/Jenkinsfile b/benchmarks/Jenkinsfile new file mode 100644 index 00000000000..4b02f7115e9 --- /dev/null +++ b/benchmarks/Jenkinsfile @@ -0,0 +1,86 @@ +pipeline { + agent { label 'main' } + environment { + CLONE_DIR = "temp_repo_results" + } + stages { + // checks out the results repo using secret stored in Jenkins + stage('Checkout results repo') { + steps { + sh 'git config --global credential.helper cache' + sh 'git config --global push.default simple' + checkout scmGit(branches: [[name: '*/main']], extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: 'temp_repo_results'], cloneOption(depth: 1, noTags: false, reference: '', shallow: true)], userRemoteConfigs: [[credentialsId: 'GH_DEPLOY_KEY_METPY_BENCH_RESULTS', url: 'https://github.com/unidata/metpy-benchmark.git']]) + } + } + // copies past results into the asv/results folder on the main repo + stage('Copy past results') { + steps { + sh ''' + if [ -d ${CLONE_DIR}/results ]; then + echo "-------Copying results--------" + cp -r ${CLONE_DIR}/results/* benchmarks/asv/results + fi + ''' + } + } + // generates the hashes to run and stores them in a text file + stage('Setup for ASV run') { + steps { + sh ''' + cd benchmarks + bash generate_hashes.sh + cd .. + ''' + } + } + // Runs ASV in the docker container + // The catch error ensures that the build works even if some ASV fail + stage('Run ASV') { + steps { + catchError(buildResult: 'SUCCESS') { + sh ''' + cd benchmarks + docker build -t metpy-benchmarks:latest . + cd .. + docker run --rm -v .:/container-benchmarks --hostname Docker_Container -e DUID=$(id -u) -e DGID=$(id -g) metpy-benchmarks:latest benchmark + ''' + } + } + } + // Copies results from the asv/results into the results repo + stage('Copy results') { + steps{ + sh ''' + if [ -d "${CLONE_DIR}/results" ]; then + echo "--------results repo exist-------" + else + mkdir ${CLONE_DIR}/results + fi + cp -r benchmarks/asv/results/* ${CLONE_DIR}/results + ''' + } + } + // Pushes to the git repo if there have been changes + stage('Update results repo') { + steps { + withCredentials([gitUsernamePassword(credentialsId: 'ASV_RESULTS_REPO_PAT', gitToolName: 'Default')]) { + sh ''' + if [ -n "$(git status --porcelain)" ]; then + cd ${CLONE_DIR} + git add --all + git commit -m "Jenkins Updating Benchmark Results BUILD-NUMBER:${BUILD_NUMBER}" || echo "-----no changes to commit-----" + git push origin HEAD:main --force + fi + ''' + } + } + } + } + post { + // always removes the temporary repo regardless of build status + always { + echo "---Cleaning up temporary repo---" + sh 'rm -rf "${CLONE_DIR}"' + } + } +} diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json new file mode 100644 index 00000000000..cdafee21a53 --- /dev/null +++ b/benchmarks/asv.conf.json @@ -0,0 +1,208 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "metpy", + + // The project's homepage + "project_url": "https://unidata.github.io/MetPy/latest/", + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": "..", + + // The Python project's subdirectory in your repo. If missing or + // the empty string, the project is assumed to be located at the root + // of the repository. + //"repo_subdir": "benchmarks", + + // Customizable commands for building the project. + // See asv.conf.json documentation. + // To build the package using pyproject.toml (PEP518), uncomment the following lines + "build_command": [ + "python -m pip install build", + "python -m build", + "python -mpip wheel -w {build_cache_dir} {build_dir}" + ], + // To build the package using setuptools and a setup.py file, uncomment the following lines + // "build_command": [ + // "python setup.py build", + // "python -mpip wheel -w {build_cache_dir} {build_dir}" + // ], + + // Customizable commands for installing and uninstalling the project. + // See asv.conf.json documentation. + "install_command": ["in-dir={env_dir} python -mpip install {build_dir}"], + "uninstall_command": ["return-code=any python -mpip uninstall -y {project}"], + + // List of branches to benchmark. If not provided, defaults to "main" + // (for git) or "default" (for mercurial). + "branches": ["HEAD"], // for git + // "branches": ["default"], // for mercurial + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + // "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv", "mamba" (above 3.8) + // or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "virtualenv", + + // timeout in seconds for installing any dependencies in environment + // defaults to 10 min + //"install_timeout": 600, + + // the base URL to show a commit for the project. + "show_commit_url": "http://github.com/unidata/metpy/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + //"pythons": ["3.8", "3.12"], + + // The list of conda channel names to be searched for benchmark + // dependency packages in the specified order + "conda_channels": ["conda-forge"], + + // A conda environment file that is used for environment creation. + // "conda_environment_file": "environment.yml", + + // The matrix of dependencies to test. Each key of the "req" + // requirements dictionary is the name of a package (in PyPI) and + // the values are version numbers. An empty list or empty string + // indicates to just test against the default (latest) + // version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed + // via pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // + // The ``@env`` and ``@env_nobuild`` keys contain the matrix of + // environment variables to pass to build and benchmark commands. + // An environment will be created for every combination of the + // cartesian product of the "@env" variables in this matrix. + // Variables in "@env_nobuild" will be passed to every environment + // during the benchmark phase, but will not trigger creation of + // new environments. A value of ``null`` means that the variable + // will not be set for the current combination. + // + "matrix": { + "req": { + "matplotlib": [ + ], + "numpy": [ + ], + "pandas": [ + ], + "pint": [ + ], + "pooch": [ + ], + "pyproj": [ + ], + "scipy": [ + ], + "traitlets": [ + ], + "xarray": [ + ], + "netcdf4": [ + ], + } + }, + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // - req + // Required packages + // - env + // Environment variables + // - env_nobuild + // Non-build environment variables + // + // "exclude": [ + // {"python": "3.2", "sys_platform": "win32"}, // skip py3.2 on windows + // {"environment_type": "conda", "req": {"six": null}}, // don't run without six on conda + // {"env": {"ENV_VAR_1": "val2"}}, // skip val2 for ENV_VAR_1 + // ], + // + // "include": [ + // // additional env for python3.12 + // {"python": "3.12", "req": {"numpy": "1.26"}, "env_nobuild": {"FOO": "123"}}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "3.12", "req": {"libpython": ""}}, + // ], + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + // "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + "env_dir": "asv/env", + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": "asv/results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + "html_dir": "asv/html", + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache results of the recent builds in each + // environment, making them faster to install next time. This is + // the number of builds to keep, per environment. + "build_cache_size": 52 + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // }, + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // }, +} diff --git a/benchmarks/asv_run_script.sh b/benchmarks/asv_run_script.sh new file mode 100644 index 00000000000..3e132eb3c41 --- /dev/null +++ b/benchmarks/asv_run_script.sh @@ -0,0 +1,11 @@ +#!/bin/bash +#Run asv + +# Generate artificial data file for benchmarks +python3 data_array_generate.py + +#Set up asv machine +asv machine --yes + +# Runs asv on the commits in the hash file but skips ones that already have results +asv run --skip-existing-successful HASHFILE:no_bot_merge_commits.txt diff --git a/benchmarks/benchmarks/__init__.py b/benchmarks/benchmarks/__init__.py new file mode 100644 index 00000000000..7de985941cf --- /dev/null +++ b/benchmarks/benchmarks/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""initialization file required for ASV to run.""" diff --git a/benchmarks/benchmarks/apparent_temp_benchmarks.py b/benchmarks/benchmarks/apparent_temp_benchmarks.py new file mode 100644 index 00000000000..9fbe5135304 --- /dev/null +++ b/benchmarks/benchmarks/apparent_temp_benchmarks.py @@ -0,0 +1,66 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.02' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + + def time_apparent_temperature(self, pressureslice): + """Benchmarking calculating apparent temperature on a 2d grid.""" + mpcalc.apparent_temperature(self.pressureslice.temperature, + self.pressureslice.relative_humidity, + self.pressureslice.windspeed) + + def time_heat_index(self, timeslice): + """Benchmarking calculating heat index on a 3d cube.""" + mpcalc.heat_index(self.timeslice.temperature, self.timeslice.relative_humidity) + + def time_windchill(self, timeslice): + """Benchmarking calculating windchill on a 3d cube.""" + mpcalc.windchill(self.timeslice.temperature, self.timeslice.windspeed) diff --git a/benchmarks/benchmarks/bound_layer_turbulence_benchmarks.py b/benchmarks/benchmarks/bound_layer_turbulence_benchmarks.py new file mode 100644 index 00000000000..4f675deffc1 --- /dev/null +++ b/benchmarks/benchmarks/bound_layer_turbulence_benchmarks.py @@ -0,0 +1,70 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.02' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.timeslice = ds.isel(time=0) + + def time_brunt_vaisala_frequency(self, timeslice): + """Benchmark Brunt Vaisala frequency calculation on a cube.""" + mpcalc.brunt_vaisala_frequency(self.timeslice.height, self.timeslice.theta) + + def time_gradient_richardson_number(self, timeslice): + """Benchmark Gradient Richardson Number on a cube.""" + mpcalc.gradient_richardson_number(self.timeslice.height, self.timeslice.theta, + self.timeslice.uwind, self.timeslice.vwind) + + def time_tke(self, ds): + """Benchmarking turbulent kinetic energy calculation on a cube.""" + mpcalc.tke(ds.uwind.values * units('m/s'), ds.vwind.values * units('m/s'), + ds.wwind.values * units('m/s')) + + def time_brunt_vaisala_period(self, timeslice): + """Benchmark Brunt Vaisala frequency calculation on a cube.""" + mpcalc.brunt_vaisala_period(self.timeslice.height, self.timeslice.theta) diff --git a/benchmarks/benchmarks/dry_thermo_benchmarks.py b/benchmarks/benchmarks/dry_thermo_benchmarks.py new file mode 100644 index 00000000000..95bd5ed7cc7 --- /dev/null +++ b/benchmarks/benchmarks/dry_thermo_benchmarks.py @@ -0,0 +1,117 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.07' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + self.profileslice = ds.isel(time=0, lat=0, lon=0) + + def time_density(self, pressureslice): + """Benchmarking density calculation on a 2d surface.""" + mpcalc.density(self.pressureslice.pressure, self.pressureslice.temperature, + self.pressureslice.mixing_ratio) + + def time_height_to_geopotential(self, timeslice): + """Benchmarking the height to geopotenial calculation on a 3d cube.""" + mpcalc.height_to_geopotential(self.timeslice.height) + + def time_potential_temperature(self, timeslice): + """Benchmarking the potential temperature calculation on a 3d cube.""" + mpcalc.potential_temperature(self.timeslice.pressure, self.timeslice.temperature) + + def time_static_stability(self, timeslice): + """Benchmarking static stability calculation on a 3d cube.""" + mpcalc.static_stability(self.timeslice.pressure, self.timeslice.temperature) + + def time_thickness_hydrostatic(self, timeslice): + """Benchmarking hydrostatic thickness calculation on a 3d cube.""" + mpcalc.thickness_hydrostatic(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.mixing_ratio) + + def time_dry_lapse(self, timeslice): + """Benchmarking the dry lapse calculation on a 3d cube.""" + mpcalc.dry_lapse(self.timeslice.pressure, self.timeslice.temperature) + + def time_sigma_to_pressure(self, timeslice): + """Benchmarking the sigma to pressure calculation on a 3d cube.""" + mpcalc.sigma_to_pressure(self.timeslice.sigma, self.timeslice.pressure[0], + self.timeslice.pressure[49]) + + def time_geopotential_to_height(self, timeslice): + """Benchmarking the geopotential to height calculation on a 3d cube.""" + mpcalc.geopotential_to_height(self.timeslice.geopotential) + + def time_add_pressure_to_height(self, timeslice): + """Benchmarking adding pressure to height on a 3d cube.""" + mpcalc.add_pressure_to_height(self.timeslice.height, self.timeslice.pressure) + + def time_add_height_to_pressure(self, timeslice): + """Benchmarking adding height to pressure on a 3d cube.""" + mpcalc.add_height_to_pressure(self.timeslice.pressure.values * units('hPa'), + self.timeslice.height.values * units('km')) + + def time_temperature_from_potential_temperature(self, timeslice): + """Benchmarking calculating temperature from potential temperature on a 3d cube.""" + mpcalc.temperature_from_potential_temperature(self.timeslice.pressure, + self.timeslice.theta) + + def time_mean_pressure_weighted(self, profileslice): + """Benchmarking calculating weighted mean of pressure with temp on one profile.""" + mpcalc.mean_pressure_weighted(self.profileslice.pressure, + self.profileslice.temperature) + + def time_weighted_continuous_average(self, profileslice): + """Bencharmking calculating weighted continuous average on one profile.""" + mpcalc.weighted_continuous_average(self.profileslice.pressure, + self.profileslice.temperature) + + def time_dry_static_energy(self, timeslice): + """Benchmarking dry static energy calculation on a 3d cube.""" + mpcalc.dry_static_energy(self.timeslice.height, self.timeslice.temperature) diff --git a/benchmarks/benchmarks/dyn_kin_benchmarks.py b/benchmarks/benchmarks/dyn_kin_benchmarks.py new file mode 100644 index 00000000000..5837a767260 --- /dev/null +++ b/benchmarks/benchmarks/dyn_kin_benchmarks.py @@ -0,0 +1,159 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +import metpy.interpolate as mpinter + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.03' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + ds = ds.metpy.parse_cf() + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + self.profileslice = ds.isel(time=0, lat=0, lon=0) + start = (30., 260.) + end = (40., 270.) + self.cross = mpinter.cross_section(self.timeslice, start, end).set_coords(('lat', 'lon' + )) + + def time_absolute_vorticity(self, pressureslice): + """Benchmarking absolute momentum calculation on a 2d surface.""" + mpcalc.absolute_vorticity(self.pressureslice.uwind, self.pressureslice.vwind) + + def time_advection(self, timeslice): + """Benchmarking the advection calculation of t on a 3d cube.""" + mpcalc.advection(self.timeslice.temperature, self.timeslice.uwind, + self.timeslice.vwind) + + def time_ageostrophic_wind(self, pressureslice): + """Benchmarking ageostrophic wind calculation on a 2d surface.""" + mpcalc.ageostrophic_wind(self.pressureslice.height, self.pressureslice.uwind, + self.pressureslice.vwind) + + def time_frontogenesis(self, pressureslice): + """Benchmarking the calculation of frontogenesis of a 2d field.""" + mpcalc.frontogenesis(self.pressureslice.theta, self.pressureslice.uwind, + self.pressureslice.vwind) + + def time_potential_vorticity_barotropic(self, timeslice): + """Benchmarking the barotropic potential vorticity calculation on a cube.""" + mpcalc.potential_vorticity_barotropic(self.timeslice.height, self.timeslice.uwind, + self.timeslice.vwind) + + def time_q_vector(self, pressureslice): + """Benchmarking q vector calculation on a 2d slice.""" + mpcalc.q_vector(self.pressureslice.uwind, self.pressureslice.vwind, + self.pressureslice.temperature, self.pressureslice.pressure) + + def time_total_deformation(self, pressureslice): + """Benchmarking total deformation calculation on a 2d slice.""" + mpcalc.total_deformation(self.pressureslice.uwind, self.pressureslice.vwind) + + def time_vorticity(self, pressureslice): + """Benchmarking vorticity calculation on a 2d slice.""" + mpcalc.vorticity(self.pressureslice.uwind, self.pressureslice.vwind) + + def time_shear_vorticity(self, pressureslice): + """Benchmarking shear vorticity on a 2d slice.""" + mpcalc.shear_vorticity(self.pressureslice.uwind, self.pressureslice.vwind) + + def time_absolute_momentum(self, cross): + """Benchmarking absolute momentum calculation.""" + mpcalc.absolute_momentum(self.cross.uwind, self.cross.vwind) + + def time_potential_vorticity_baroclinic(self, timeslice): + """Benchmarking potential vorticity baroclinic on a 3d cube.""" + mpcalc.potential_vorticity_baroclinic(self.timeslice.theta, self.timeslice.pressure, + self.timeslice.uwind, self.timeslice.vwind) + + def time_inertal_advective_wind(self, timeslice): + """Benchmarking inertal advective wind calculation on a 3d cube.""" + mpcalc.inertial_advective_wind(self.timeslice.uwind, self.timeslice.vwind, + self.timeslice.uwind, self.timeslice.vwind) + + def time_curvature_vorticity(self, timeslice): + """Benchmarking the curvature vorticity calculation on a 3d cube.""" + mpcalc.curvature_vorticity(self.timeslice.uwind, self.timeslice.vwind) + + def time_montgomery_streamfunction(self, pressureslice): + """Benchmarking the montgomery streamfunction calculation on a 2d grid.""" + mpcalc.montgomery_streamfunction(self.pressureslice.height, + self.pressureslice.temperature) + + def time_wind_direction(self, timeslice): + """Benchmarking the wind direction calculation on a 3d cube.""" + mpcalc.wind_direction(self.timeslice.uwind, self.timeslice.vwind) + + def time_wind_components(self, timeslice): + """Benchmarking the wind components calculation on a 3d cube.""" + mpcalc.wind_components(self.timeslice.windspeed, self.timeslice.winddir) + + def time_divergence(self, timeslice): + """Benchmarking divergence on a 3d cube.""" + mpcalc.divergence(self.timeslice.uwind, self.timeslice.vwind) + + def time_stretching_deformation(self, timeslice): + """Benchmarking stretching deformation on a 3d cube.""" + mpcalc.stretching_deformation(self.timeslice.uwind, self.timeslice.vwind) + + def time_shearing_deformation(self, timeslice): + """Benchmarking shearing deformation on a 3d cube.""" + mpcalc.shearing_deformation(self.timeslice.uwind, self.timeslice.vwind) + + def time_geostrophic_wind(self, timeslice): + """Benchmarking the geostrophic wind calculation on a 3d cube.""" + mpcalc.geostrophic_wind(self.timeslice.height, latitude=self.timeslice.lat) + + def time_coriolis_parameter(self, timeslice): + """Benchmarking coriolis parameter calculation on a 3d cube.""" + mpcalc.coriolis_parameter(self.timeslice.lat) + + def time_wind_speed(self, timeslice): + """Benchmarking wind speed calculation on a 3d cube.""" + mpcalc.wind_speed(self.timeslice.uwind, self.timeslice.vwind) + + def time_exner_function(self, timeslice): + """Benchmark exner function calculation on a cube.""" + mpcalc.exner_function(self.timeslice.pressure) diff --git a/benchmarks/benchmarks/math_fctn_benchmarks.py b/benchmarks/benchmarks/math_fctn_benchmarks.py new file mode 100644 index 00000000000..358b6222949 --- /dev/null +++ b/benchmarks/benchmarks/math_fctn_benchmarks.py @@ -0,0 +1,86 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +import metpy.interpolate as mpinter + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.03' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + ds = ds.metpy.parse_cf() + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + start = (30., 260.) + end = (40., 270.) + self.cross = mpinter.cross_section(self.timeslice, + start, end).set_coords(('lat', 'lon')) + + def time_geospatial_gradient(self, pressureslice): + """Benchmarking calculating the geospatial gradient of temp on a 2d array.""" + mpcalc.geospatial_gradient(self.pressureslice.temperature) + + def time_geospatial_laplacian(self, pressureslice): + """Benchmarking calculating the geospatial laplacian of temp on a 2d array.""" + mpcalc.geospatial_laplacian(self.pressureslice.temperature) + + def time_gradient(self, timeslice): + """Benchmarking calculating the gradient of temp on a 3d cube.""" + mpcalc.gradient(self.timeslice.temperature) + + def time_vector_derivative(self, pressureslice): + """Benchmarking calculating the vector derivative of wind on a 2d slice.""" + mpcalc.vector_derivative(self.pressureslice.uwind, self.pressureslice.vwind) + + def time_tangential_component(self, cross): + """Benchmarking calculation of the tangential component of wind on a slice.""" + mpcalc.tangential_component(self.cross.uwind, self.cross.vwind) + + def time_cross_section_components(self, cross): + """Benchmarking the cross section components of a wind grid.""" + mpcalc.cross_section_components(self.cross.uwind, self.cross.vwind) + + def time_normal_component(self, cross): + """Benchmarking the calculating normal components times.""" + mpcalc.normal_component(self.cross.uwind, self.cross.vwind) diff --git a/benchmarks/benchmarks/moist_thermo_benchmarks.py b/benchmarks/benchmarks/moist_thermo_benchmarks.py new file mode 100644 index 00000000000..376fd4b9181 --- /dev/null +++ b/benchmarks/benchmarks/moist_thermo_benchmarks.py @@ -0,0 +1,220 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.02' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + self.upperslice = ds.isel(pressure=49, time=0) + self.profileslice = ds.isel(time=0, lat=25, lon=25) + + def time_virtual_temperature(self, timeslice): + """Benchmark virtual temperature on a 3d cube.""" + mpcalc.virtual_temperature(self.timeslice.temperature, self.timeslice.mixing_ratio) + + def time_dewpoint(self, timeslice): + """Benchmarking dewpoint from vapor pressure on a 3d cube.""" + mpcalc.dewpoint(self.timeslice.vapor_pressure) + + def time_rh_from_mixing_ratio(self, timeslice): + """Benchmarking relative humidity from mixing ratio on a 3d cube.""" + mpcalc.relative_humidity_from_mixing_ratio(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.mixing_ratio) + + def time_dewpoint_from_rh(self, timeslice): + """Benchmarking dewpoint from calculated on a 3d cube.""" + mpcalc.dewpoint_from_relative_humidity(self.timeslice.temperature, + self.timeslice.relative_humidity) + + def time_precipitable_water(self, timeslice): + """Benchmarking precipitable water calculation for one column.""" + mpcalc.precipitable_water(self.timeslice.pressure, self.timeslice.dewpoint[0][0]) + + def time_wet_bulb_temperature(self, pressureslice): + """Benchmarking wet bulb temperature calculation on on a slice.""" + mpcalc.wet_bulb_temperature(self.pressureslice.pressure, + self.pressureslice.temperature, + self.pressureslice.dewpoint) + + def time_scale_height(self, pressureslice): + """Benchmarking the calculation for the scale height of a layer for 2 surfaces.""" + mpcalc.scale_height(self.upperslice.temperature, self.pressureslice.temperature) + + def time_moist_lapse(self, profileslice): + """Benchmarking the calculation for the moist lapse rate for one profile.""" + mpcalc.moist_lapse(self.profileslice.pressure.values * units('hPa'), + self.profileslice.temperature[0].values * units('K')) + + def time_saturation_vapor_pressure(self, timeslice): + """Benchmarking the saturation vapor pressure calculation for a 3d cube.""" + mpcalc.saturation_vapor_pressure(self.timeslice.temperature) + + def time_water_latent_heat_vaporization(self, timeslice): + """Benchmarking the vaporization latent heat calculation on a 3d cube.""" + mpcalc.water_latent_heat_vaporization(self.timeslice.temperature) + + def time_water_latent_heat_sublimation(self, timeslice): + """Benchmarking the sublimation latent heat calculation on a 3d cube.""" + mpcalc.water_latent_heat_sublimation(self.timeslice.temperature) + + def time_water_latent_heat_melting(self, timeslice): + """Benchmarking the melting latent heat calculation on a 3d cube.""" + mpcalc.water_latent_heat_melting(self.timeslice.temperature) + + def time_specific_humidity_from_dewpoint(self, timeslice): + """Benchmarking specific humidity from dewpoint calculation on a 3d cube.""" + mpcalc.specific_humidity_from_dewpoint(self.timeslice.pressure, + self.timeslice.temperature) + + def time_relative_humidity_from_dewpoint(self, timeslice): + """Benchmarking relative humidity from dewpoint calculation on a 3d cube.""" + mpcalc.relative_humidity_from_dewpoint(self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_moist_static_energy(self, timeslice): + """Benchmarking moist static energy calculation on a 3d cube.""" + mpcalc.moist_static_energy(self.timeslice.height, self.timeslice.temperature, + self.timeslice.specific_humidity) + + def time_dewpoint_from_specific_humidity(self, timeslice): + """Benchmarking dewpoint from specific humidity calculation on a 3d cube.""" + mpcalc.dewpoint_from_specific_humidity(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.specific_humidity) + + def time_moist_air_specific_heat_pressure(self, timeslice): + """Benchmarking moist air specific heat pressure calculation on a 3d cube.""" + mpcalc.moist_air_specific_heat_pressure(self.timeslice.specific_humidity) + + def time_moist_air_poisson_exponent(self, timeslice): + """Benchmarking moist air poisson exponent calculation on a cube.""" + mpcalc.moist_air_poisson_exponent(self.timeslice.specific_humidity) + + def time_relative_humidity_wet_psychrometric(self, timeslice): + """Benchmarking the relative humidity from psychometric calculation on a cube.""" + mpcalc.relative_humidity_wet_psychrometric(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.wet_bulb_temperature) + + def time_thickness_hydrostatic_from_relative_humidity(self, profileslice): + """Benchmarking thickness calculation from relative humidity on one profile.""" + mpcalc.thickness_hydrostatic_from_relative_humidity(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.relative_humidity + ) + + def time_relative_humidity_from_specific_humidity(self, timeslice): + """Benchmarking relative humidity from specific humidity calculation on a 3d cube.""" + mpcalc.relative_humidity_from_specific_humidity(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.specific_humidity) + + def time_wet_bulb_potential_temperature(self, timeslice): + """Benchmarking the wet bulb potential temperature calculation on a 3d cube.""" + mpcalc.wet_bulb_potential_temperature(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_vertical_velocity_pressure(self, timeslice): + """Benchmarking vertical velocity wrt pressure calculation on a 3d cube.""" + mpcalc.vertical_velocity_pressure(self.timeslice.wwind, self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.mixing_ratio) + + def time_vertical_velocity(self, timeslice): + """Benchmarking vertical velocity calculation on a 3d cube.""" + mpcalc.vertical_velocity(self.timeslice.omega, self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.mixing_ratio) + + def time_saturation_equivalent_potential_temperature(self, timeslice): + """Benchmarking saturation equivalent potential temperature on 3d cube.""" + mpcalc.saturation_equivalent_potential_temperature(self.timeslice.pressure, + self.timeslice.temperature) + + def time_virtual_potential_temperature(self, timeslice): + """Benchmarking virtual potential temperature calculation on a 3d cube.""" + mpcalc.virtual_potential_temperature(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.mixing_ratio) + + def time_psychrometric_vapor_pressure_wet(self, timeslice): + """Benchmarking psychrometric vapor pressure calculation on a 3d cube.""" + mpcalc.psychrometric_vapor_pressure_wet(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.wet_bulb_temperature) + + def time_mixing_ratio_from_relative_humidity(self, timeslice): + """Benchmarking mixing ratio from relative humidity calculation on a 3d cube.""" + mpcalc.mixing_ratio_from_relative_humidity(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.relative_humidity) + + def time_mixing_ratio_from_specific_humidity(self, timeslice): + """Benchmarking calculating mixing rato from specific humidity on a 3d cube.""" + mpcalc.mixing_ratio_from_specific_humidity(self.timeslice.specific_humidity) + + def time_relative_humidity_from_mixing_ratio(self, timeslice): + """Benchmarking relative humidity from mixing ratio calculation on a 3d cube.""" + mpcalc.relative_humidity_from_mixing_ratio(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.mixing_ratio) + + def time_equivalent_potential_temperature(self, timeslice): + """Benchmarking equivalent potential temperature calculation on 3d cube.""" + mpcalc.equivalent_potential_temperature(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_virtual_temperature_from_dewpoint(self, timeslice): + """Benchmarking virtual temperature from dewpoint calculation on 3d cube.""" + mpcalc.virtual_temperature_from_dewpoint(self.timeslice.pressure, + self.timeslice.temperature, + self.timeslice.dewpoint) diff --git a/benchmarks/benchmarks/other_benchmarks.py b/benchmarks/benchmarks/other_benchmarks.py new file mode 100644 index 00000000000..16e39a51437 --- /dev/null +++ b/benchmarks/benchmarks/other_benchmarks.py @@ -0,0 +1,83 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.02' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.ds = ds + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + self.lineslice = ds.isel(pressure=0, time=0, lat=0) + self.profileslice = ds.isel(time=0, lat=0, lon=0) + + def time_find_intersections(self, lineslice): + """Benchmarking finding intersections calculation.""" + mpcalc.find_intersections(self.lineslice.lon, self.lineslice.temperature, + self.lineslice.dewpoint) + + def time_find_peaks(self, pressureslice): + """Benchmarking finding peaks of 2d dewpoint slice.""" + mpcalc.find_peaks(self.pressureslice.dewpoint) + + def time_get_perturbation(self, ds): + """Benchmarking getting the perturbation of a time series.""" + mpcalc.get_perturbation(self.ds.temperature) + + def time_peak_persistence(self, pressureslice): + """Benchmarking calculating persistence of of maxima point in 3d.""" + mpcalc.peak_persistence(self.pressureslice.dewpoint) + + def time_isentropic_interpolation_as_dataset(self, timeslice): + """Benchmarking the isentropic interpolation as dataset calculation on a 3d cube.""" + mpcalc.isentropic_interpolation_as_dataset([265.] * units.kelvin, + self.timeslice.temperature) + + def time_isentropic_interpolation(self, timeslice): + """Bencharking the isentropic interpolation calculation on a 3d cube.""" + mpcalc.isentropic_interpolation([265.] * units.kelvin, self.timeslice.pressure, + self.timeslice.temperature) diff --git a/benchmarks/benchmarks/smoothing_benchmarks.py b/benchmarks/benchmarks/smoothing_benchmarks.py new file mode 100644 index 00000000000..088a26e0d20 --- /dev/null +++ b/benchmarks/benchmarks/smoothing_benchmarks.py @@ -0,0 +1,77 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import numpy as np +import xarray as xr + +import metpy.calc as mpcalc + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.02' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + + def time_smooth_gaussian(self, pressureslice): + """Benchmarking the gaussian smoothing of a 2d grid.""" + mpcalc.smooth_gaussian(self.pressureslice.relative_humidity, 5) + + def time_smooth_window(self, pressureslice): + """Benchmarking the window smoothing of a 2d grid.""" + mpcalc.smooth_window(self.pressureslice.relative_humidity, np.diag(np.ones(5))) + + def time_smooth_rectangular(self, pressureslice): + """Benchmarking the rectangular smoothing of a 2d grid.""" + mpcalc.smooth_rectangular(self.pressureslice.relative_humidity, (3, 7)) + + def time_smooth_circular(self, pressureslice): + """Benchmarking the circular smoothing of a 2d grid.""" + mpcalc.smooth_circular(self.pressureslice.relative_humidity, 2) + + def time_smooth_n_point(self, pressureslice): + """Benchmarking the 5 point smoothing of a 2d grid.""" + mpcalc.smooth_n_point(self.pressureslice.relative_humidity) + + def time_zoom_xarray(self, pressureslice): + """Benchmarking the zoom xarray function.""" + mpcalc.zoom_xarray(self.pressureslice.temperature, zoom=3.0) diff --git a/benchmarks/benchmarks/soundings_benchmarks.py b/benchmarks/benchmarks/soundings_benchmarks.py new file mode 100644 index 00000000000..82c589829c7 --- /dev/null +++ b/benchmarks/benchmarks/soundings_benchmarks.py @@ -0,0 +1,225 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.21' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.timeslice = ds.isel(time=0) + self.pressureslice = ds.isel(time=0, pressure=0) + self.profileslice = ds.isel(lat=25, lon=25, time=0) + self.parcelprofile = mpcalc.parcel_profile(self.profileslice.pressure, + self.profileslice.temperature[0], + self.profileslice.dewpoint[0]) + self.sbcape, _ = mpcalc.surface_based_cape_cin(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.dewpoint) + self.sblcl, _ = mpcalc.lcl(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.dewpoint) + self.sblclheight = mpcalc.pressure_to_height_std(self.sblcl) + _, _, self.relhel = mpcalc.storm_relative_helicity(self.profileslice.height, + self.profileslice.uwind, + self.profileslice.vwind, + 1 * units('km')) + self.shearu, self.shearv = mpcalc.bulk_shear(self.profileslice.pressure, + self.profileslice.uwind, + self.profileslice.vwind) + self.shear = mpcalc.wind_speed(self.shearu, self.shearv) + + def time_bulk_shear(self, profileslice): + """Benchmarking calculating the bulk shear of a profile.""" + mpcalc.bulk_shear(self.profileslice.pressure, self.profileslice.uwind, + self.profileslice.vwind) + + def time_ccl(self, profileslice): + """Benchmarking calculating the convective condensation level of a profile.""" + mpcalc.ccl(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_parcel_profile(self, profileslice): + """Benchmarking the atmospheric parcel profile for one profile.""" + mpcalc.parcel_profile(self.profileslice.pressure, self.profileslice.temperature[0], + self.profileslice.dewpoint[0]) + + def time_most_unstable_parcel(self, profileslice): + """Benchmarking the calculation to find the most unstable parcel for one profile.""" + mpcalc.most_unstable_parcel(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_cape_cin(self, profileslice): + """Benchmarking cape_cin calculation for one profile.""" + mpcalc.cape_cin(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint, self.parcelprofile) + + def time_lcl(self, timeslice): + """Benchmarks lcl on a 3d cube - many profiles.""" + mpcalc.lcl(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_el(self, profileslice): + """Benchmarks el calculation on one profile.""" + mpcalc.el(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_storm_relative_helicity(self, profileslice): + """Benchmarks storm relative helicity over one profile.""" + mpcalc.storm_relative_helicity(self.profileslice.height, self.profileslice.uwind, + self.profileslice.vwind, 1 * units('km')) + + def time_vertical_totals(self, timeslice): + """Benchmarking vertical totals for many profiles.""" + mpcalc.vertical_totals(self.timeslice.pressure, self.timeslice.temperature) + + def time_supercell_composite(self, profileslice): + """Benchmarks supercell composite calculation for one calculation.""" + mpcalc.supercell_composite(2500 * units('J/kg'), 125 * units('m^2/s^2'), + 50 * units.knot) + + def time_critical_angle(self, profileslice): + """Benchmarking critical angle on one profile.""" + mpcalc.critical_angle(self.profileslice.pressure, self.profileslice.uwind, + self.profileslice.vwind, self.profileslice.height, + 0 * units('m/s'), 0 * units('m/s')) + + def time_bunkers_storm_motion(self, profileslice): + """Benchmarking bunkers storm motion on one profile.""" + mpcalc.bunkers_storm_motion(self.profileslice.pressure, self.profileslice.uwind, + self.profileslice.vwind, self.profileslice.height) + + def time_corfidi_storm_motion(self, profileslice): + """Benchmarking corfidi storm motion on one profile.""" + mpcalc.corfidi_storm_motion(self.profileslice.pressure, self.profileslice.uwind, + self.profileslice.vwind) + + def time_sweat_index(self, timeslice): + """Benchmarking SWEAT index on many profiles.""" + mpcalc.sweat_index(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.dewpoint, self.timeslice.windspeed, + self.timeslice.winddir) + + def time_most_unstable_cape_cin(self, profileslice): + """Benchmarking most unstable cape cin calculation on one profile.""" + mpcalc.most_unstable_cape_cin(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_surface_based_cape_cin(self, profileslice): + """Benchmarking surface based cape cin calculation on one profile.""" + mpcalc.surface_based_cape_cin(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_lifted_index(self, profileslice): + """Benchmarking lifted index calculation on one profile.""" + mpcalc.lifted_index(self.profileslice.pressure, self.profileslice.temperature, + self.parcelprofile) + + def time_k_index(self, timeslice): + """Benchmarking k index calculation on many profiles.""" + mpcalc.k_index(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_mixed_layer_cape_cin(self, profileslice): + """Benchmarking mixed layer cape cin calculation for one profile.""" + mpcalc.mixed_layer_cape_cin(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_cross_totals(self, timeslice): + """Benchmarking cross totals calculation on many profiles.""" + mpcalc.cross_totals(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_downdraft_cape(self, profileslice): + """Benchmarking downdraft cape calculation on one profile.""" + mpcalc.downdraft_cape(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_parcel_profile_with_lcl_as_dataset(self, profileslice): + """Benchmarking parcel profile with lcl as dataset one on profile.""" + mpcalc.parcel_profile_with_lcl_as_dataset(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_showalter_index(self, profileslice): + """Benchmarking calculating the showalter index on one profiles.""" + mpcalc.showalter_index(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_galvez_davison_index(self, timeslice): + """Benchmarking calculating the galvez davison index on many profiles.""" + mpcalc.galvez_davison_index(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.mixing_ratio, self.timeslice.pressure[0]) + + def time_significant_tornado(self, profileslice): + """Benchmarking significant tornado param for one profile.""" + mpcalc.significant_tornado(self.sbcape, self.sblclheight, self.relhel, self.shear) + + def time_total_totals_index(self, timeslice): + """Benchmarking total totals index for many profiles.""" + mpcalc.total_totals_index(self.timeslice.pressure, self.timeslice.temperature, + self.timeslice.dewpoint) + + def time_lfc(self, profileslice): + """Benchmarking level of free convection calculation for one profile.""" + mpcalc.lfc(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_mixed_parcel(self, profileslice): + """Benchmarking mixed parcel for one profile.""" + mpcalc.mixed_parcel(self.profileslice.pressure, self.profileslice.temperature, + self.profileslice.dewpoint) + + def time_mixed_layer(self, profileslice): + """Benchmarking mixed layer of temperature for one profile.""" + mpcalc.mixed_layer(self.profileslice.pressure, self.profileslice.temperature) + + def time_parcel_profile_with_lcl(self, profileslice): + """Benchmarking parcel profile with lcl calculation.""" + mpcalc.parcel_profile_with_lcl(self.profileslice.pressure, + self.profileslice.temperature, + self.profileslice.dewpoint) diff --git a/benchmarks/benchmarks/std_atm_benchmarks.py b/benchmarks/benchmarks/std_atm_benchmarks.py new file mode 100644 index 00000000000..3a739170021 --- /dev/null +++ b/benchmarks/benchmarks/std_atm_benchmarks.py @@ -0,0 +1,67 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Benchmark the functions in the moist thermo section of metpy's calc module. + +Uses Airspeed Velocity for benchmarking and uses artificial dataset to ensure consistent and +reliable data for results. + +""" + +import os + +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + + +class TimeSuite: + """Benchmark moist thermo functions in time using Airspeed Velocity and xarray datasets. + + Uses ASV's benchmarking format to load in data and run benchmarks to measure time + performance + + """ + + # NOTE: I'm using CalVer https://calver.org/ YYYY.MM.DD + version = '2025.07.07' + + def setup_cache(self): + """Collect the sample dataset from the filepath and opens it as an xarray. + + Returns + ------- + ds + Dataset with artificial meteorology data for testing + """ + base_path = os.path.dirname(__file__) # path to current file + file_path = os.path.join(base_path, '..', 'data_array_compressed.nc') + file_path = os.path.abspath(file_path) + ds = xr.open_dataset(file_path) + return ds + + def setup(self, ds): + """Set up the appropriate slices from the sample dataset for testing. + + Parameters + ---------- + ds : dataset + The dataset made in setup_cache which contains the testing data + """ + self.pressureslice = ds.isel(pressure=0, time=0) + self.timeslice = ds.isel(time=0) + + def time_height_to_pressure_std(self, timeslice): + """Benchmarking the height to pressure calculation in a std atm on a 3d cube.""" + mpcalc.height_to_pressure_std(self.timeslice.height) + + def time_pressure_to_height_std(self, timeslice): + """Benchmarking the pressure to height calculation in a std atm on a 3d cube.""" + mpcalc.pressure_to_height_std(self.timeslice.pressure) + + def time_altimeter_to_sea_level_pressure(self, timeslice): + """Benchmarking altimeter to slp on a 3d cube.""" + mpcalc.altimeter_to_sea_level_pressure(self.timeslice.pressure.values * units('hPa'), + self.timeslice.height.values * units('km'), + self.timeslice.temperature * units('K')) diff --git a/benchmarks/data_array_generate.py b/benchmarks/data_array_generate.py new file mode 100644 index 00000000000..ada7bb011e1 --- /dev/null +++ b/benchmarks/data_array_generate.py @@ -0,0 +1,258 @@ +# Copyright (c) 2025 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Create a sample xarray Dataset with 3D variables. + +The generated dataset is used as consistent data for benchmarking +""" +import os + +import numpy as np +import pandas as pd +import xarray as xr + +import metpy.calc as mpcalc +from metpy.units import units + +# Make lat/lon data over the mid-latitudes +lats = np.linspace(30, 40, 50) +lons = np.linspace(360 - 100, 360 - 90, 50) +pressure = np.linspace(1000, 250, 50) * units.hPa +p_3d = pressure[:, np.newaxis, np.newaxis] + +times = pd.date_range('2024/01/01', '2024/06/01', freq='ME') + +# Initialize a random number generator +rng = np.random.default_rng() + +# Adding height +z = mpcalc.pressure_to_height_std(p_3d) +height = np.tile(z, (1, len(lats), len(lons))) + + +# make data based on Matplotlib example data for wind barbs +x, y = np.meshgrid(np.linspace(-3, 3, 51), np.linspace(-3, 3, 51)) +z = (1 - x / 2 + x**5 + y**3) * np.exp(-x**2 - y**2) + + +# make u and v out of the z equation +u = -np.diff(z[:, 1:], axis=0) * 100 + 10 +v = np.diff(z[1:, :], axis=1) * 100 + 10 +w = np.full([50, 50], 1) + +# Make them 3D +# Note: to make some of the linters happy you have to use the second variable in the loop +# which is why there are some random self / self things going on +u_3d = np.zeros((len(pressure), 50, 50)) +for i, p in enumerate(pressure): + u_3d[i, :, :] = u * (1002 - p.magnitude)**.3 # 1002 ensures the entire lower layer isn't 0 + +v_3d = np.zeros((len(pressure), 50, 50)) +for i, p in enumerate(pressure): + v_3d[i, :, :] = v * (1002 - p.magnitude)**.3 + +w_3d = np.zeros((len(pressure), 50, 50)) +for i, _p in enumerate(pressure): + w_3d[i, :, :] = w * rng.random() + +# Then make them 4D +u_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + u_4d[:, :, :, i] = u_3d * rng.uniform(-2, 2) + +v_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + v_4d[:, :, :, i] = v_3d * rng.uniform(-2, 2) + +w_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + w_4d[:, :, :, i] = w_3d * rng.uniform(-2, 2) + + +windspeed = mpcalc.wind_speed(u_4d * units('m/s'), v_4d * units('m/s')) +winddir = mpcalc.wind_direction(u_4d * units('m/s'), v_4d * units('m/s')) + +# setting up for annual temperature cycle +days = np.arange(len(times)) +annual_cycle = np.sin(2 * np.pi * days / len(times)) + +seasonal_amplitude = 10 + 273.15 # K difference from summer to winter +seasonal_variation = seasonal_amplitude * annual_cycle + + +lapse_rate = 6.5 * units('K/km') # avg env gamma + +# make t as colder air to the north and 3d +t_sfc = (np.linspace(15, 5, 50) * np.ones((50, 50))) +t_sfc = (t_sfc + 273.15) + +t_3d = np.zeros((len(pressure), 50, 50)) # (pressure, lat, lon) +for i, _p in enumerate(pressure): + t_3d[i, :, :] = (t_sfc * units.K) - (lapse_rate * height[i, :, :]) + + +# Make t colder in the winter, warmer in the summer +t_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + t_4d[:, :, :, i] = t_3d + seasonal_variation[i] + +t_4d = t_4d * units.K + + +# Generate potential temperature +theta_4d = mpcalc.potential_temperature(p, t_4d[::-1, :, :, :]) + +# Generate mixing ratio +surface_w = .015 # dimensionless +top_w = .001 # dimensionless (kg/kg) + +# constants for mixing ratio calculation +a = (surface_w - top_w) / (pressure[0] - pressure[49]) +b = surface_w - a * pressure[49] + +w_profile = a * pressure + b + + +mixingratio_3d = np.zeros((50, len(lats), len(lons))) +for i, _lat in enumerate(lats): + for j, _lon in enumerate(lons): + mixingratio_3d[:, i, j] = w_profile + +mixingratio_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + mixingratio_4d[:, :, :, i] = mixingratio_3d + +# Generate vapor pressure +vapor_pressure_4d = mpcalc.vapor_pressure(p, mixingratio_4d) + +# Generate dewpoint +td_sfc = (np.linspace(10, 0, 50) * np.ones((50, 50))) +td_3d = np.zeros((len(pressure), 50, 50)) * units.kelvin +for i, p in enumerate(pressure): + # Scale dewpoint colder and drier with height + scale = (p / 1000.0).magnitude ** 0.8 + td_3d[i, :, :] = (td_sfc * scale) * units.K + + +td_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + td_4d[:, :, :, i] = t_3d + seasonal_variation[i] + rng.uniform(-6, 3, size=(50, 50, 50)) + +td_4d = td_4d * units.K +td_4d = np.minimum(td_4d, t_4d) + +# Generate relative humidity from dewpoint +rh = mpcalc.relative_humidity_from_dewpoint(t_4d, td_4d) + +# Generate sigma values +sigma_3d = (p_3d - (250 * units.hPa)) / ((1000 * units.hPa) - (250 * units.hPa)) + +sigma_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + sigma_4d[:, :, :, i] = sigma_3d[:, :, :] + +# Generate geopotential values +geopotential_3d = mpcalc.height_to_geopotential(height) + +geopotential_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + geopotential_4d[:, :, :, i] = geopotential_3d[:, :, :] + +# generate specific humidity values +q = mpcalc.specific_humidity_from_mixing_ratio(mixingratio_4d) + +# generate wet bulb temperature +wet_bulb_4d = np.zeros((50, 50, 50, len(times))) +for i, _tm in enumerate(times): + wet_bulb_4d[:, :, :, i] = mpcalc.wet_bulb_temperature(p_3d[:, :, :], t_4d[:, :, :, i], + td_4d[:, :, :, i]) + +# generate omega +omega_4d = mpcalc.vertical_velocity_pressure(w_4d * units('m/s'), p_3d, t_4d, mixingratio_4d) + +# place data into an xarray dataset object +lat_da = xr.DataArray(lats, dims='lat', attrs={'standard_name': 'latitude', + 'units': 'degrees_north'}) +lon_da = xr.DataArray(lons, dims='lon', attrs={'standard_name': 'longitude', + 'units': 'degrees_east'}) +pressure_level = xr.DataArray(pressure.magnitude, dims='pressure', + attrs={'standard_name': 'pressure', 'units': 'hPa'}) +time_da = xr.DataArray(times, dims='time', attrs={'standard name': 'time'}) + +coords = {'lat': lat_da, 'lon': lon_da, 'pressure': pressure_level, 'time': time_da} + + +uwind = xr.DataArray(u_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'u-component_of_wind', 'units': 'm s-1'}) +vwind = xr.DataArray(v_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'v-component_of_wind', 'units': 'm s-1'}) +wwind = xr.DataArray(w_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'w-component_of_wind', 'units': 'm s-1'}) +temperature = xr.DataArray(t_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'temperature', 'units': 'K'}) +height = xr.DataArray(height, dims=['pressure', 'lat', 'lon'], + attrs={'standard_name': 'z dimension', 'units': 'km'}) +theta = xr.DataArray(theta_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'Potential temperature', 'units': 'K'}) +mixing_ratio = xr.DataArray(mixingratio_4d, coords=coords, + dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'Mixing Ratio', 'units': 'dimensionless'}) +vapor_pressure = xr.DataArray(vapor_pressure_4d, coords=coords, + dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard name': 'Vapor pressure', 'units': 'hPa'}) +dewpoint = xr.DataArray(td_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard name': 'dewpoint', 'units': 'K'}) +relative_humidity = xr.DataArray(rh, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard name': 'relative humidity', 'units': '%'}) +windspeed = xr.DataArray(windspeed, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'windspeed', 'units': 'm s-1'}) +winddir = xr.DataArray(winddir, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'wind direction', 'units': 'degrees'}) +sigma = xr.DataArray(sigma_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'sigma', 'units': 'dimensionless'}) +geopotential = xr.DataArray(geopotential_4d, coords=coords, + dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'geopotential', 'units': 'm2 s-2'}) +specific_humidity = xr.DataArray(q, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'specific humidity', + 'units': 'dimensionless'}) +wet_bulb_temperature = xr.DataArray(wet_bulb_4d, coords=coords, + dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'wet_bulb_temperature', + 'units': 'K'}) +omega = xr.DataArray(omega_4d, coords=coords, dims=['pressure', 'lat', 'lon', 'time'], + attrs={'standard_name': 'omega component of wind', 'units': 'pascal/s'}) + +ds = xr.Dataset({'uwind': uwind, + 'vwind': vwind, + 'wwind': wwind, + 'temperature': temperature, + 'height': height, + 'theta': theta, + 'mixing_ratio': mixing_ratio, + 'vapor_pressure': vapor_pressure, + 'dewpoint': dewpoint, + 'relative_humidity': relative_humidity, + 'windspeed': windspeed, + 'winddir': winddir, + 'sigma': sigma, + 'geopotential': geopotential, + 'specific_humidity': specific_humidity, + 'wet_bulb_temperature': wet_bulb_temperature, + 'omega': omega}) + +# Step 1: Initialize encoding dict for data variables +encoding = { + var: {'zlib': True, 'complevel': 9, 'dtype': 'float32'} + for var in ds.data_vars +} + +# Step 2: Add compression settings for coordinates (no dtype conversion) +for coord in ds.coords: + encoding[coord] = {'zlib': True, 'complevel': 9} + + +if os.path.exists('data_array_compressed.nc'): + os.remove('data_array_compressed.nc') + +ds.to_netcdf('data_array_compressed.nc', format='NETCDF4', encoding=encoding) diff --git a/benchmarks/entrypoint.sh b/benchmarks/entrypoint.sh new file mode 100644 index 00000000000..73cdb92f140 --- /dev/null +++ b/benchmarks/entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -e + +# "command" to pass docker run to execute benchmarks +RUN_BENCHMARKS="benchmark" +# "command" to pass docker run to get a shell in the benchmark user environment +BENCHMARK_USER_LOGIN="peek" + +if [[ "$1" == "$RUN_BENCHMARKS" || "$1" == "$BENCHMARK_USER_LOGIN" ]]; then + USER_ID=${DUID:-1000} + GROUP_ID=${DGUI:-1000} + GROUP_NAME="benchmark" + USER_NAME=$GROUP_NAME + + # create group for GROUP_ID if one doesn't already exist + if ! getent group $GROUP_ID &> /dev/null; then + groupadd -r $GROUP_NAME -g $GROUP_ID + fi + + # create user for USER_ID if one doesn't already exist + if ! getent passwd $USER_ID &> /dev/null; then + useradd -u $USER_ID -g $GROUP_ID $USER_NAME + fi + + mkdir /temp-home + chown -R $USER_ID:$GROUP_ID /temp-home + + # modify benchmark user to have /bin/bash as shell + usermod -s /bin/bash $(id -u -n $USER_ID) -d /temp-home + + sync + + + if [[ "$1" == "$RUN_BENCHMARKS" ]]; then + # step-down from root and run benchmarks as benchmark user + exec pysu $(id -u -n $USER_ID) container-benchmarks/benchmarks/runner.sh + else + # step-down from root and run bash (for exploration in interactive mode) + exec pysu $(id -u -n $USER_ID) /bin/bash + fi +fi + +exec "$@" diff --git a/benchmarks/generate_hashes.sh b/benchmarks/generate_hashes.sh new file mode 100644 index 00000000000..5e793007cb0 --- /dev/null +++ b/benchmarks/generate_hashes.sh @@ -0,0 +1,25 @@ +# Set repo info +REPO_URL="https://github.com/Unidata/MetPy.git" #metpy repo to clone from +CLONE_DIR_GEN_HASH="temp_repo_generate_hashes" #temporary repo to clone to - deleted at the end of the script + +# clone metpy and fetch tags +git clone --depth=100 --no-tags "$REPO_URL" "$CLONE_DIR_GEN_HASH" #shallow clone metpy repo +cd "$CLONE_DIR_GEN_HASH" || exit 1 #change directories to temporary repo, if this fails exit +git fetch --tags #fetch metpy tags + +# Set the range: from last v1.6.x to present (all 1.7.x merge commits) - no commits authored by or mentioning dependabot or github-actions +git log --merges v1.6.3.. --pretty=format:"%H %s" | \ +grep -v -i "dependabot" | \ +grep -v -i "github-actions" | \ +awk '{print $1}' > ../no_bot_merge_commits.txt #print output to this file in the benchmarks dir + + +#Get the commit hashes for each minor version after 1. +git for-each-ref --sort=version:refname \ + --format='%(refname:short) %(objectname)' refs/tags | \ + grep -E '^v[1-9].[4-9]*\..*' | + awk '{print $2}' >> ../no_bot_merge_commits.txt #append these results to same file + +cd .. #leave temp_repo + +rm -rf "$CLONE_DIR_GEN_HASH" #clean up by removing temporary repo \ No newline at end of file diff --git a/benchmarks/runner.sh b/benchmarks/runner.sh new file mode 100644 index 00000000000..daa63c08eda --- /dev/null +++ b/benchmarks/runner.sh @@ -0,0 +1,5 @@ +#!/bin/bash +git config --global --add safe.directory /container-benchmarks +git config --global --add safe.directory /container-benchmarks/.git +cd /container-benchmarks/benchmarks +./asv_run_script.sh diff --git a/docs/devel/benchmarking.rst b/docs/devel/benchmarking.rst new file mode 100644 index 00000000000..7eb9cd751a9 --- /dev/null +++ b/docs/devel/benchmarking.rst @@ -0,0 +1,96 @@ +======================== +Performance Benchmarking +======================== + +This guide provides information on the implementation and management of benchmarking in MetPy. + +----------------- +Airspeed Velocity +----------------- + +MetPy's source code is benchmarked using `Airspeed Velocity `_. +ASV is an open source software which builds environments based on historical and current +iterations of software and runs benchmark functions before compiling the results into +digestable html pages. MetPy's developers have used GitHub Actions and a Unidata Jenkins +instance in order to automatically perform benchmarking as part of the continuous +integration/continuous development workflow. These benchmarks allow us to identify bottlenecks +in the code, determine performance changes from pull requests, and view a history of MetPy's +time efficiency. + +---------------------- +Historical Performance +---------------------- + +Results of Metpy's performance throughout versions is available at `this page `_. +Currently, the history is benchmarked starting with MetPy version ``1.4.0`` and benchmarks the +first commit hash associated with each minor version until present. Additionally, starting with +the most recent minor release, every merged pull request made by a human contributor is +benchmarked and added to the results. Note that these benchmarks run weekly, so it may take a +few days for your merged commit to be updated into the results. + +This performance history is run using the Unidata Jenkins instance. Upon run, the +``benchmarks/Jenkinsfile`` instructs the Jenkins instance to create a custom +``Docker container`` using the ``benchmarks/Dockerfile`` and runs the benchmark +functions within it. Jenkins uses the same Unidata machine for each run in order to ensure +consistent benchmarking results. ASV is installed in this container and runs the benchmark +functions for the historical commits of interest. In the event that successful results already +exist for the requested commit hash, ASV will skip it and maintain the previous results. +Finally, Jenkins pushes the results to a separate `results repository `_ +where a GitHub Action uses an ASV command to generate and deploy the html. + +------------------- +Benchmark Functions +------------------- + +Located within the ``benchmarks/benchmarks`` directory are ``.py`` files each containing a +class ``TimeSuite``, ``setup`` and ``setup_cache`` functions, and functions with the name +scheme ``time_example_metpy_function``. This is ASV's required `syntax `_ +for writing benchmarks. The ``setup_cache`` function loads the artificial benchmarking dataset +``data_array_compressed.nc`` and prepares the dataset for use by the benchmarks. The ``setup`` +function "slices" the 4D dataset into the appropriate dimensions to create variables that can +be passed to and used by the benchmark functions. Each benchmarking function then receives one +of these slices (or the entire dataset) and runs the code inside the function as a benchmark, +timing the performance and saving the results. + +------------------ +Local Benchmarking +------------------ + +If you would like to run the benchmarking suite on your own development branch, +follow these steps: + +1. Install asv in your ``devel`` environment using ``conda install asv`` +2. Ensure that you have the ``benchmarks`` directory at the root of your MetPy repository +3. Navigate to the ``benchmarks`` directory: ``cd benchmarks`` +4. Generate the benchmarking data array by running the ``data_array_generate.py`` file +5. Now it depends on exactly which benchmarks you want to run: + + a. To benchmark your code as is currently is, + use ``python -m asv run`` + + b. To compare a working branch with *your version* of MetPy's main branch, use + ``python -m asv continuous main `` where ```` is the name of your + branch. You can also simply use two commit hashes in the place of the branch names. To view + a text-based table for the functions and comparisons, use ``python -m asv compare main + `` + + c. To run the history of MetPy as mentioned above, you can use + ``python -m asv run HASHFILE:no_bot_merge_commits.txt`` after running the + ``generate_hashes.sh`` script + **Note that this is computationally taxing and often takes several hours, + depending on the specs of your machine** + + d. If you have a running ``Docker Engine``, you can build the Docker image from the + Dockerfile in the benchmarks directory using the command + ``docker build -t metpy-benchmarks:latest .`` + + After this is built, you can run the + benchmarks from the root of the repository in the container using + ``docker run --rm -v .:/container-benchmarks --hostname Docker_Container + metpy-benchmarks:latest benchmark`` on Windows or + ``docker run --rm -v .:/container-benchmarks --hostname Docker_Container -e DUID=$(id -u) + -e DGID=$(id -g) metpy-benchmarks:latest benchmark`` on Mac/Linux **Note that as above, + this runs all the benchmarks in no_bot_merge_commits.txt and can be taxing** + + If you want to just enter the container, you can use the same command as above, but + replace the end ``benchmark`` with ``peek`` diff --git a/docs/devel/index.rst b/docs/devel/index.rst index 331c37b0a3c..451acd96df0 100644 --- a/docs/devel/index.rst +++ b/docs/devel/index.rst @@ -9,6 +9,7 @@ Developer's Guide CONTRIBUTING roadmap infrastructureguide + benchmarking This discusses information relevant to developing MetPy.