Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5fd8001
Update to build using scikit-build-core
dopplershift Sep 6, 2024
19399ff
WIP: Add stub module that uses pybind11
dopplershift Sep 6, 2024
e76004c
add virtual_temperature.cpp and link it using pybind11
May 26, 2025
358161a
debug 1.input data format compatibility 2.set default epsilon 0.622
May 27, 2025
f18d93b
remove setting c++14 in CMakeLists.txt
May 28, 2025
d12c77d
Pybind11_vectorize (#1)
gitlinffff Jun 3, 2025
31dbf18
wip
Jun 4, 2025
8e1eccc
create constants.cpp and .hpp to fetch all constants from python; con…
Jun 4, 2025
6b81f0e
c++ fetch nounit constants from nounit.py
Jun 5, 2025
0cb07ba
finish converting saturation vapor pressure
Jun 6, 2025
5514401
calc.thermo calls c++ function WIP
Jun 6, 2025
697afd6
apply c++ functions in thermo.py
Jun 6, 2025
5072b95
wip
Jun 6, 2025
40c9419
wip
Jun 9, 2025
f913ea7
wip
Jun 9, 2025
e7d870e
add c++ functions: mixing_ratio, saturation_mixing_ratio, specific_hu…
Jun 11, 2025
ad18ce3
add c++ functions: moist_air_gas_constant, moist_air_specific_heat_pr…
Jun 11, 2025
89a17ab
wip
Jun 11, 2025
0af355b
add function lambert_wm1: Lambert W function -1 section; LCL c++ WIP
Jun 12, 2025
94803bd
achieved manually vectorization of function LCL, support np.array of …
Jun 15, 2025
8f50f44
add reference to Lambert W function calculation
Jun 16, 2025
76ce4c2
set warning messages and return NAN value
Jun 16, 2025
097844a
add c++ dry_lapse
Jun 18, 2025
94fde62
add DryLapseProfile for c++ internal vectorization of DryLapse; _Parc…
Jun 19, 2025
e9feef3
add moist lapse, use rk4 to integrate dlnT_dlnP
Jun 21, 2025
569c92f
use Bakhshaii2013 for c++ moist_lapse
Jun 23, 2025
ca222aa
import c++ lcl into metpy thermo.py
Jun 23, 2025
7986a8a
change file name 'virtual_temperature.cpp' -> 'thermo.cpp'
Jun 23, 2025
1f12749
parcel profile wip
Jun 23, 2025
8eb21f0
add a return struct for _ParcelProfileHelper wip
Jun 24, 2025
e33ff90
c++ ParcelProfile finished, assembled dry and moist part of the profile
Jun 24, 2025
44f1ddd
c++ version moist_lapse can handle 2D multiple starting temperature now
Jun 26, 2025
e0d1f1e
put MoistLapseVectorized in thermo.cpp, no need to vectorize in pybin…
Jun 26, 2025
853a213
add DryLapseVectorized in thermo.cpp, no need to vectorize in pybind1…
Jun 26, 2025
e767960
modify conditions for LCL to execute
Jun 27, 2025
0afe0cc
add c++ virtual_temperature_from_dewpoint
Jun 30, 2025
47845b5
change ssize_t to size_t, ssize_t is not supported on windows
Jul 7, 2025
87b31ea
move lcl python binding from calcmod.cpp to thermo.cpp; debug syntax …
Jul 7, 2025
c93e6aa
add DryLapseVectorized_3D
Jul 11, 2025
57995eb
debug: number of dimension inconsistency when lcl has inputs of a sca…
Jul 16, 2025
9dbc348
code convention check
Jul 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[build-system]
requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=3.4"]
build-backend = "setuptools.build_meta"
requires = ["setuptools>=61", "setuptools-scm[toml]>=3.4", "scikit-build-core", "pybind11"]
build-backend = "scikit_build_core.build"
Comment on lines +2 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're using that build backend, you can drop setuptools from the build-requires list too.


[project]
name = "MetPy"
name = "metpy"
description = "Collection of tools for reading, visualizing and performing calculations with weather data."
readme = "README.md"
dynamic = ["version"]
Expand Down Expand Up @@ -174,5 +174,9 @@ max-complexity = 61
[tool.ruff.lint.pydocstyle]
convention = "numpy"

[tool.scikit-build]
metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
cmake.source-dir = "src"

[tool.setuptools_scm]
version_scheme = "post-release"
24 changes: 24 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.15...3.26)
project(
"${SKBUILD_PROJECT_NAME}"
LANGUAGES CXX
VERSION "${SKBUILD_PROJECT_VERSION}")

# Enforce C++14 or higher
#set(CMAKE_CXX_STANDARD 14)
#set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason you need the interpreter? It looks like the module development component would be enough so far.

find_package(pybind11 CONFIG REQUIRED)

pybind11_add_module(_calc_mod MODULE
calcmod.cpp
constants.cpp
math.cpp
thermo.cpp
)

target_compile_definitions(_calc_mod
PRIVATE VERSION_INFO=${PROJECT_VERSION})

install(TARGETS _calc_mod DESTINATION metpy)
111 changes: 111 additions & 0 deletions src/calcmod.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#include <pybind11/pybind11.h>

Check warning on line 1 in src/calcmod.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/calcmod.cpp#L1

Include file: <pybind11/pybind11.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <pybind11/numpy.h>

Check warning on line 2 in src/calcmod.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/calcmod.cpp#L2

Include file: <pybind11/numpy.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <pybind11/stl.h>

Check warning on line 3 in src/calcmod.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/calcmod.cpp#L3

Include file: <pybind11/stl.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These need to be the first includes in the file.

#include "constants.hpp"
#include "thermo.hpp"
#include <utility> // For std::pair

Check warning on line 6 in src/calcmod.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/calcmod.cpp#L6

Include file: <utility> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <tuple> // For std::make_tuple
#include <iostream>

Check warning on line 8 in src/calcmod.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/calcmod.cpp#L8

Include file: <iostream> not found. Please note: Cppcheck does not need standard library headers to get proper results.

namespace py = pybind11;

int add(int i, int j) {
return i + j;
}

PYBIND11_MODULE(_calc_mod, m) {
m.doc() = "accelerator module docstring";

metpy_constants::load_constants_from_python();

m.def("add", &add, "Add two numbers");

m.def("moist_air_gas_constant", py::vectorize(MoistAirGasConstant),
"Calculate R_m, the gas constant for moist air.",
py::arg("specific_humidity"));

m.def("moist_air_specific_heat_pressure", py::vectorize(MoistAirSpecificHeatPressure),
"Calculate C_pm, the specific heat of moist air at constant pressure.",
py::arg("specific_humidity"));

m.def("water_latent_heat_vaporization", py::vectorize(WaterLatentHeatVaporization),
"Calculate water latent heat vaporization from temperature.",
py::arg("temperature"));

m.def("water_latent_heat_sublimation", py::vectorize(WaterLatentHeatSublimation),
"Calculate water latent heat sublimation from temperature.",
py::arg("temperature"));

m.def("relative_humidity_from_dewpoint", py::vectorize(RelativeHumidityFromDewPoint),
"Calculate relative humidity from temperature and dewpoint.",
py::arg("temperature"), py::arg("dewpoint"), py::arg("phase"));

m.def("dry_lapse", &DryLapseVectorized,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason this doesn't follow the pattern of py::vectorize(DryLapse)? That seems to be what the current implementation does.

"Calculate the temperature along a pressure profile assuming dry adiabatic process.",
py::arg("pressure"), py::arg("ref_temperature"), py::arg("ref_pressure"));

m.def("dry_lapse_3d", &DryLapseVectorized_3D,
"Calculate the temperature along multiple pressure profiles assuming dry adiabatic process.",
py::arg("pressure"), py::arg("ref_temperature"), py::arg("ref_pressure"));

m.def("moist_lapse", &MoistLapseVectorized,
"Calculate the temperature along a pressure profile assuming saturated adiabatic process.",
py::arg("pressure"), py::arg("ref_temperature"), py::arg("ref_pressure"), py::arg("rk_nstep"));


m.def("lcl", &LCLVectorized,
"Calculate the lifting condensation level (LCL) from pressure, temperature and dewpoint.",
py::arg("pressure"), py::arg("temperature"), py::arg("dewpoint"));

m.def("parcel_profile",
[](py::array_t<double> pressure, double temperature, double dewpoint) {
// pressure.data() gives the beginning pointer of the data buffer
std::vector<double> pressure_vec(pressure.data(), pressure.data() + pressure.size());
std::vector<double> temp_prof = ParcelProfile(pressure_vec, temperature, dewpoint);
return py::array_t<double>(temp_prof.size(), temp_prof.data());
},
"Compute the parcel temperature profile as it rises from a given pressure and temperature.",
py::arg("pressure"), py::arg("temperature"), py::arg("dewpoint"));


m.def("saturation_vapor_pressure", py::vectorize(SaturationVaporPressure),
"Calculate saturation vapor pressure from temperature.",
py::arg("temperature"), py::arg("phase") = "liquid");

m.def("_saturation_vapor_pressure_liquid", py::vectorize(_SaturationVaporPressureLiquid),
"Calculate saturation vapor pressure from temperature.",
py::arg("temperature"));

m.def("_saturation_vapor_pressure_solid", py::vectorize(_SaturationVaporPressureSolid),
"Calculate saturation vapor pressure from temperature.",
py::arg("temperature"));

m.def("dewpoint", py::vectorize(DewPoint),
"Calculate dewpoint from water vapor partial pressure.",
py::arg("vapor_pressure"));

m.def("mixing_ratio", py::vectorize(MixingRatio),
"Calculate the mixing ratio of a gas.",
py::arg("partial_press"), py::arg("total_press"), py::arg("epsilon"));

m.def("saturation_mixing_ratio", py::vectorize(SaturationMixingRatio),
"Calculate the saturation mixing ratio of water vapor given total atmospheric pressure and temperature.",
py::arg("total_press"), py::arg("temperature"), py::arg("phase"));

m.def("specific_humidity_from_mixing_ratio", py::vectorize(SpecificHumidityFromMixingRatio),
"Calculate the specific humidity from the mixing ratio.",
py::arg("mixing_ratio"));

m.def("specific_humidity_from_dewpoint", py::vectorize(SpecificHumidityFromDewPoint),
"Calculate the specific humidity from the dewpoint temperature and pressure.",
py::arg("pressure"), py::arg("dewpoint"), py::arg("phase"));

m.def("virtual_temperature", py::vectorize(VirtualTemperature),
"Calculate virtual temperature from temperature and mixing ratio.",
py::arg("temperature"), py::arg("mixing_ratio"), py::arg("epsilon"));

m.def("virtual_temperature_from_dewpoint", py::vectorize(VirtualTemperatureFromDewpoint),
"Calculate virtual temperature from dewpoint.",
py::arg("pressure"), py::arg("temperature"), py::arg("dewpoint"), py::arg("epsilon"), py::arg("phase"));

}
50 changes: 50 additions & 0 deletions src/constants.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// constants.cpp
#include "constants.hpp"
#include <pybind11/pybind11.h>

Check warning on line 3 in src/constants.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/constants.cpp#L3

Include file: <pybind11/pybind11.h> not found. Please note: Cppcheck does not need standard library headers to get proper results.

namespace py = pybind11;

namespace metpy_constants {
double Mw;
double Rd;
double Rv;
double Cp_d;
double Cp_v;
double Cp_l;
double Lv;
double sat_pressure_0c;
double T0;
double Ls;
double Cp_i;
double zero_degc;
double epsilon;
double kappa;

void load_constants_from_python() {
py::object mod = py::module_::import("metpy.constants.nounit");

// Mw = mod.attr("Mw").attr("magnitude").cast<double>();
// Rv = mod.attr("Rv").attr("magnitude").cast<double>();
// Cp_v = mod.attr("Cp_v").attr("magnitude").cast<double>();
// Cp_l = mod.attr("Cp_l").attr("magnitude").cast<double>();
// Lv = mod.attr("Lv").attr("magnitude").cast<double>();
// sat_pressure_0c = mod.attr("sat_pressure_0c").cast<double>();
// T0 = mod.attr("T0").attr("magnitude").cast<double>();


Mw = mod.attr("Mw").cast<double>();
Rd = mod.attr("Rd").cast<double>();
Rv = mod.attr("Rv").cast<double>();
Cp_d = mod.attr("Cp_d").cast<double>();
Cp_v = mod.attr("Cp_v").cast<double>();
Cp_l = mod.attr("Cp_l").cast<double>();
Lv = mod.attr("Lv").cast<double>();
sat_pressure_0c = mod.attr("sat_pressure_0c").cast<double>();
T0 = mod.attr("T0").cast<double>();
Ls = mod.attr("Ls").cast<double>();
Cp_i = mod.attr("Cp_i").cast<double>();
zero_degc = mod.attr("zero_degc").cast<double>();
epsilon = mod.attr("epsilon").cast<double>();
kappa = mod.attr("kappa").cast<double>();
}
}
49 changes: 49 additions & 0 deletions src/constants.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#ifndef CONSTANTS_HPP
#define CONSTANTS_HPP

namespace metpy_constants {

// Gas constants (J / kg / K)
// extern double R;

// Dry air
// extern double Md;
// extern double Rd;
// extern double dry_air_spec_heat_ratio;
// extern double Cp_d = Rd * dry_air_spec_heat_ratio / (dry_air_spec_heat_ratio - 1.0); // (J / kg / K)
// extern double Cv_d = Cp_d / dry_air_spec_heat_ratio; // (J / kg / K)

// Water
extern double Mw;
extern double Rd;
extern double Rv;
// extern double wv_spec_heat_ratio = 1.33;
extern double Cp_d;
extern double Cp_v;
// extern double Cv_v = Cp_v / wv_spec_heat_ratio; // (J / kg / K)
extern double Cp_l;
extern double Lv;
extern double sat_pressure_0c;
extern double T0;
extern double Ls;
extern double Cp_i;
extern double zero_degc;
extern double epsilon;
extern double kappa;

// General Meteorological constants
// extern double epsilon = Mw / Md; // ≈ 0.622


// Standard gravity
// extern double g = 9.80665; // m / s^2

// Reference pressure
// extern double P0 = 100000.0; // Pa (hPa = 1000)


void load_constants_from_python(); // call once in your PYBIND11_MODULE
}

#endif

32 changes: 32 additions & 0 deletions src/math.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include <cmath>

Check warning on line 1 in src/math.cpp

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/math.cpp#L1

Include file: <cmath> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <iostream> // for std::cerr
#include "math.hpp"

double lambert_wm1(double x, double tol, int max_iter) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to use the SciPy LambertW (or the xsf function it wraps (C++ in a separate repo for people who don't want all of SciPy), or the Cython wrapper it might generate (it looks like it would be scipy.special._ufuncs.lambertw)? If we are very lucky, PyCapsule_Import() would get the correct function with little effort.

// Corless, R.M., Gonnet, G.H., Hare, D.E.G. et al. On the LambertW function.
// Adv Comput Math 5, 329–359 (1996). https://doi.org/10.1007/BF02124750

if (x >= 0 || x < -1.0 / std::exp(1.0)) {
std::cerr << "Warning in function '" << __func__
<< "': lambert_wm1 is only defined for -1/e < x < 0\n";
return std::numeric_limits<double>::quiet_NaN();
}

double L1 = std::log(-x);
double L2 = std::log(-L1);
double w = L1 - L2 + (L2 / L1);

for (int i = 0; i < max_iter; ++i) {
double ew = std::exp(w);
double w_ew = w * ew;
double diff = (w_ew - x) / (ew * (w + 1) - ((w + 2) * (w_ew - x)) / (2 * w + 2));
w -= diff;
if (std::abs(diff) < tol) {
return w;
}
}

std::cerr << "Warning in function '" << __func__
<< "': lambert_wm1 did not converge.\n";
return std::numeric_limits<double>::quiet_NaN();
}
7 changes: 7 additions & 0 deletions src/math.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#ifndef MATH_HPP
#define MATH_HPP

double lambert_wm1(double x, double tol = 1e-10, int max_iter = 100);

#endif // MATH_HPP

Loading
Loading