From af03ead26820fe835ce5f398de5fe2e75558cdd5 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Feb 2026 11:46:51 -0600 Subject: [PATCH 01/55] bump dev version --- src/nanobind/nwsspc/sharp/calc/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nanobind/nwsspc/sharp/calc/__init__.pyi b/src/nanobind/nwsspc/sharp/calc/__init__.pyi index bafd5ffc..d9862efe 100644 --- a/src/nanobind/nwsspc/sharp/calc/__init__.pyi +++ b/src/nanobind/nwsspc/sharp/calc/__init__.pyi @@ -9,4 +9,4 @@ from . import ( ) -__version_tuple__: tuple = (1, 3, 0) +__version_tuple__: tuple = (1, 3, 1) From d06609b039ac6cb2818bfe83c670094d054b2fee Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Feb 2026 11:59:43 -0600 Subject: [PATCH 02/55] v1.4.0.dev --- src/nanobind/nwsspc/sharp/calc/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nanobind/nwsspc/sharp/calc/__init__.pyi b/src/nanobind/nwsspc/sharp/calc/__init__.pyi index d9862efe..975dd96c 100644 --- a/src/nanobind/nwsspc/sharp/calc/__init__.pyi +++ b/src/nanobind/nwsspc/sharp/calc/__init__.pyi @@ -9,4 +9,4 @@ from . import ( ) -__version_tuple__: tuple = (1, 3, 1) +__version_tuple__: tuple = (1, 4, 0) From 8ee92d8185ba2fbcc5038d3833d6d802ed456f37 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Feb 2026 14:25:03 -0600 Subject: [PATCH 03/55] trying a PFT algorithm that is binary search based --- include/SHARPlib/params/fire.h | 54 ++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/include/SHARPlib/params/fire.h b/include/SHARPlib/params/fire.h index a67b2fce..f879f5fd 100644 --- a/include/SHARPlib/params/fire.h +++ b/include/SHARPlib/params/fire.h @@ -160,25 +160,32 @@ template WindComponents mean_uv = mean_wind(mix_layer, pressure, uwin, vwin, N, false); float mean_wspd = sharp::vector_magnitude(mean_uv.u, mean_uv.v); + float pres_sfc = pressure[0]; - float z_fc = sharp::MISSING; - float delta_theta = 0.0; + float beta_max = 0.1f; + int max_steps = static_cast(beta_max / beta_incr); + int low_step = 0; + int high_step = max_steps; - float beta_max = 0.1; - float beta_val = 0.0; - Parcel fire_pcl; bool found = false; - while (beta_val <= beta_max) { + Parcel best_fire_pcl; + float best_z_fc = sharp::MISSING; + float best_delta_theta = 0.0f; + + while (low_step <= high_step) { + int mid_step = low_step + (high_step - low_step) / 2; + float beta_val = mid_step * beta_incr; + float eql_tmpk = 999.0; float plume_theta = pft_plume_potential_temperature(mean_theta, beta_val); float plume_mixr = pft_plume_mixratio(mean_theta, mean_mixr, beta_val, phi); - float plume_tmpk = theta(THETA_REF_PRESSURE, plume_theta, pressure[0]); - float plume_dwpk = temperature_at_mixratio(plume_mixr, pressure[0]); + float plume_tmpk = theta(THETA_REF_PRESSURE, plume_theta, pres_sfc); + float plume_dwpk = temperature_at_mixratio(plume_mixr, pres_sfc); - fire_pcl = Parcel(pressure[0], plume_tmpk, plume_dwpk, LPL::USR); + Parcel fire_pcl = Parcel(pres_sfc, plume_tmpk, plume_dwpk, LPL::USR); fire_pcl.lift_parcel(lifter, pressure, pcl_vtmpk_arr, N); buoyancy(pcl_vtmpk_arr, virtemp, pcl_buoy_arr, N); fire_pcl.cape_cinh(pressure, height, pcl_buoy_arr, N); @@ -190,25 +197,29 @@ template if ((fire_pcl.cinh >= -sharp::TOL) && (fire_pcl.cape > 0) && (eql_tmpk <= 253.15)) { - z_fc = interp_pressure(fire_pcl.lfc_pressure, pressure, height, N); + found = true; + + best_fire_pcl = fire_pcl; + best_z_fc = + interp_pressure(fire_pcl.lfc_pressure, pressure, height, N); float theta_fc = interp_pressure(fire_pcl.lfc_pressure, pressure, potential_temperature, N); - delta_theta = theta_fc - mean_theta; - found = true; - break; - } + best_delta_theta = theta_fc - mean_theta; - beta_val += beta_incr; + high_step = mid_step - 1; + } else { + low_step = mid_step + 1; + } } - if ((z_fc == MISSING) || !found) { + if (!found) { std::fill_n(&pcl_vtmpk_arr[0], N, sharp::MISSING); std::fill_n(&pcl_buoy_arr[0], N, sharp::MISSING); if (pcl) *pcl = Parcel(); return MISSING; - }; + } - z_fc = z_fc - height[0]; + float z_fc = best_z_fc - height[0]; constexpr float beta_prime = 0.4; constexpr float alpha_prime = 0.32; @@ -216,7 +227,7 @@ template (PI * CP_DRYAIR) * std::pow((beta_prime / (1 + (alpha_prime * beta_prime))), 2.0f); - float pres_pl_c = ((fire_pcl.lfc_pressure - fire_pcl.pres) / + float pres_pl_c = ((best_fire_pcl.lfc_pressure - best_fire_pcl.pres) / (1 + (alpha_prime * beta_prime))) + pressure[0]; float theta_pl_c = @@ -225,10 +236,9 @@ template float rho = (pres_pl_c / (sharp::RDGAS * theta_pl_c)) * std::pow(sharp::THETA_REF_PRESSURE / pres_pl_c, sharp::ROCP); - float PFT = - big_const * rho * std::pow(z_fc, 2.0f) * mean_wspd * delta_theta; + float PFT = big_const * rho * (z_fc * z_fc) * mean_wspd * best_delta_theta; - if (pcl) *pcl = fire_pcl; + if (pcl) *pcl = best_fire_pcl; return PFT; } From ee9d086e61e5c909e1e20919c206f59e8765d13e Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Feb 2026 14:29:50 -0600 Subject: [PATCH 04/55] updated PFT to use a binary search for criteria --- tests/python/test_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/test_params.py b/tests/python/test_params.py index 05b4cac5..fcb8c7fb 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -440,4 +440,4 @@ def test_pft(): snd_data["vwin"], snd_data["theta"] ) - assert (pft == pytest.approx(153856147456.0, abs=1e6)) + assert (pft == pytest.approx(153856081920.0, abs=1e6)) From 57d843a92f204343ac674c853d98d22407fb39e3 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Feb 2026 12:38:12 -0600 Subject: [PATCH 05/55] remove cpp files from FILE_PATTERNS --- docs/Doxyfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index 54118526..cdff0dd5 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -965,8 +965,7 @@ INPUT_FILE_ENCODING = # be provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. -FILE_PATTERNS = *.h \ - *.cpp +FILE_PATTERNS = *.h # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. From 8f2d8f4a1ff9ed0e07d13350c929a7e3c7a22e7f Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Feb 2026 12:44:25 -0600 Subject: [PATCH 06/55] added back CPP fow now. Will need to do a broader restructuring for template instantiation --- docs/Doxyfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index cdff0dd5..54118526 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -965,7 +965,8 @@ INPUT_FILE_ENCODING = # be provided as Doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, # *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. -FILE_PATTERNS = *.h +FILE_PATTERNS = *.h \ + *.cpp # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. From a4b2db966766b76dbe417b46010f986b878f42f7 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Mon, 23 Mar 2026 16:15:44 -0500 Subject: [PATCH 07/55] prototyping #101 --- include/SHARPlib/parcel.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index e483dfa3..0dfd8ffb 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -188,6 +188,26 @@ struct lifter_cm1 { } }; +template +struct lifter_tbl { + Lft m_lifter; + float p_max = 110000.0f; + float p_min = 100.0f; + float thetae_min = 250.0f; + float thetae_max = 400.0f; + + std::vector m_logp_coord; + std::vector m_thetae_coord; + std::vector m_LUT_pcl_tmpk; + std::vector m_LUT_pcl_qv; + std::vector m_LUT_pcl_qi; + std::vector m_LUT_pcl_ql; + + inline void setup() { + // fill the coordinates and tables + } +}; + // //////////// END FUNCTORS /////////// From 4d5aa140f92d1ac7976e377a0b3f0e9a23bba2b2 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 12:54:41 -0500 Subject: [PATCH 08/55] Untested implementation of table-based parcel lifting (#101) --- include/SHARPlib/parcel.h | 171 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 6 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 0dfd8ffb..3775a9e7 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -17,8 +17,12 @@ #include #include #include +#include +#include +#include #include +#include namespace sharp { @@ -45,6 +49,7 @@ namespace sharp { */ struct lifter_wobus { static constexpr bool lift_from_lcl = true; + static constexpr bool tracks_moisture = false; /** * \brief The iterative convergence criteria (K) @@ -108,6 +113,7 @@ struct lifter_wobus { */ struct lifter_cm1 { static constexpr bool lift_from_lcl = false; + static constexpr bool tracks_moisture = true; /** * \brief The type of moist adiabat to use, as defined by sharp::adiabat @@ -191,20 +197,173 @@ struct lifter_cm1 { template struct lifter_tbl { Lft m_lifter; + static constexpr bool lift_from_lcl = Lft::lift_from_lcl; + static constexpr bool tracks_moisture = Lft::tracks_moisture; + float p_max = 110000.0f; float p_min = 100.0f; float thetae_min = 250.0f; - float thetae_max = 400.0f; + float thetae_max = 500.0f; + float m_delta_thetae_inv = 0.0f; + float m_delta_logp_inv = 0.0f; + float m_rv = sharp::MISSING; + float m_rl = sharp::MISSING; + float m_ri = sharp::MISSING; + + std::size_t num_logp = 200; + std::size_t num_thetae = 200; std::vector m_logp_coord; std::vector m_thetae_coord; std::vector m_LUT_pcl_tmpk; - std::vector m_LUT_pcl_qv; - std::vector m_LUT_pcl_qi; - std::vector m_LUT_pcl_ql; + std::vector m_LUT_pcl_rv; + std::vector m_LUT_pcl_rl; + std::vector m_LUT_pcl_ri; + + lifter_tbl(Lft lifter) : m_lifter(std::move(lifter)) {} + + inline void setup([[maybe_unused]] const float lcl_pres, + [[maybe_unused]] const float lcl_tmpk) { + if constexpr (Lft::tracks_moisture) { + this->rv = sharp::MISSING; + this->rl = sharp::MISSING; + this->ri = sharp::MISSING; + } + } + + inline void generate_table() { + m_logp_coord.resize(num_logp); + m_thetae_coord.resize(num_thetae); + + const float delta_thetae = (thetae_max - thetae_min) / (num_thetae - 1); + m_delta_thetae_inv = 1.0f / delta_thetae; + for (std::size_t i = 0; i < num_thetae; ++i) { + m_thetae_coord[i] = thetae_min + i * delta_thetae; + } + + const float logp_max = std::log(p_max); + const float logp_min = std::log(p_min); + const float delta_logp = (logp_min - logp_max) / (num_logp - 1); + m_delta_logp_inv = 1.0f / delta_logp; + for (std::size_t i = 0; i < num_logp; ++i) { + m_logp_coord[i] = logp_max + i * delta_logp; + } + + std::size_t size_2D = num_logp * num_thetae; + m_LUT_pcl_tmpk.resize(size_2D); + m_LUT_pcl_rv.resize(size_2D); + m_LUT_pcl_rl.resize(size_2D); + m_LUT_pcl_ri.resize(size_2D); + + for (std::size_t i = 0; i < num_thetae; ++i) { + const float thetae_target = m_thetae_coord[i]; + const float tmpk = solve_tmpk_for_thetae(p_max, thetae_target); + + m_lifter.setup(p_max, tmpk); + float pres_curr = p_max; + float tmpk_curr = tmpk; + + for (std::size_t k = 0; k < num_logp; ++k) { + float pres = std::exp(m_logp_coord[k]); + if (k > 0) { + tmpk_curr = m_lifter(pres_curr, tmpk_curr, pres); + } + + const std::size_t idx_2D = i * num_logp + k; + m_LUT_pcl_tmpk[idx_2D] = tmpk_curr; + + if constexpr (Lft::tracks_moisture) { + m_LUT_pcl_rv[idx_2D] = m_lifter.rv; + m_LUT_pcl_rl[idx_2D] = m_lifter.rl; + m_LUT_pcl_ri[idx_2D] = m_lifter.ri; + } else { + m_LUT_pcl_rv[idx_2D] = mixratio(pres, tmpk_curr); + m_LUT_pcl_rl[idx_2D] = 0.0f; + m_LUT_pcl_ri[idx_2D] = 0.0f; + } + + pres_curr = pres; + } + } + } + + [[nodiscard]] float solve_tmpk_for_thetae(float pressure, + float thetae_target) const { + float tmpk_lo = 200.0f; + float tmpk_hi = 400.0f; + float tmpk_mid = sharp::ZEROCNK; - inline void setup() { - // fill the coordinates and tables + const float tol = 0.001f; + const std::size_t max_iters = 50; + + for (std::size_t i = 0; i < max_iters; ++i) { + tmpk_mid = 0.5f * (tmpk_lo + tmpk_hi); + const float curr_thetae = thetae(pressure, tmpk_mid, tmpk_mid); + + if (std::abs(curr_thetae - thetae_target) < tol) { + return tmpk_mid; + } + + if (curr_thetae < thetae_target) { + tmpk_lo = tmpk_mid; + } else { + tmpk_hi = tmpk_mid; + } + } + fmt::println("Failed to converge on a temperature for thetae."); + return tmpk_mid; + } + + [[nodiscard]] inline float operator()(const float pres, const float tmpk, + const float new_pres) { + const float target_thetae = thetae(pres, tmpk, tmpk); + + float fi = (target_thetae - thetae_min) * m_delta_thetae_inv; + fi = std::clamp(fi, 0.0f, static_cast(num_thetae - 1.001f)); + + const std::size_t i0 = static_cast(fi); + const std::size_t i1 = i0 + 1; + const float wi = fi - static_cast(i0); + + const float target_logp = std::log(new_pres); + const float logp_max = std::log(p_max); + + float fk = (target_logp - logp_max) * m_delta_logp_inv; + fk = std::clamp(fk, 0.0f, static_cast(num_logp - 1.001f)); + + const std::size_t k0 = static_cast(fk); + const std::size_t k1 = k0 + 1; + const float wk = fk - static_cast(k0); + + auto bilinear_interp = [&](const std::vector& lut) -> float { + const float val00 = lut[i0 * num_logp + k0]; + const float val01 = lut[i0 * num_logp + k1]; + const float val10 = lut[i1 * num_logp + k0]; + const float val11 = lut[i1 * num_logp + k1]; + + const float val0 = sharp::lerp(val00, val01, wk); + const float val1 = sharp::lerp(val10, val11, wk); + return sharp::lerp(val0, val1, wi); + }; + + if constexpr (Lft::tracks_moisture) { + this->rv = bilinear_interp(m_LUT_pcl_rv); + this->rl = bilinear_interp(m_LUT_pcl_rl); + this->ri = bilinear_interp(m_LUT_pcl_ri); + } + + return bilinear_interp(m_LUT_pcl_tmpk); + } + + [[nodiscard]] inline float parcel_virtual_temperature( + const float pres, const float tmpk) const { + if constexpr (Lft::tracks_moisture) { + return sharp::virtual_temperature(tmpk, this->rv, this->rl, + this->ri); + } else { + return sharp::virtual_temperature(tmpk, + sharp::mixratio(pres, tmpk)); + } } }; From 079dc83494492c5079629304618317dcb965588a Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 13:16:48 -0500 Subject: [PATCH 09/55] fixed variable name issue --- include/SHARPlib/parcel.h | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 3775a9e7..e948683d 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -225,9 +225,9 @@ struct lifter_tbl { inline void setup([[maybe_unused]] const float lcl_pres, [[maybe_unused]] const float lcl_tmpk) { if constexpr (Lft::tracks_moisture) { - this->rv = sharp::MISSING; - this->rl = sharp::MISSING; - this->ri = sharp::MISSING; + m_rv = sharp::MISSING; + m_rl = sharp::MISSING; + m_ri = sharp::MISSING; } } @@ -347,9 +347,9 @@ struct lifter_tbl { }; if constexpr (Lft::tracks_moisture) { - this->rv = bilinear_interp(m_LUT_pcl_rv); - this->rl = bilinear_interp(m_LUT_pcl_rl); - this->ri = bilinear_interp(m_LUT_pcl_ri); + m_rv = bilinear_interp(m_LUT_pcl_rv); + m_rl = bilinear_interp(m_LUT_pcl_rl); + m_ri = bilinear_interp(m_LUT_pcl_ri); } return bilinear_interp(m_LUT_pcl_tmpk); @@ -358,8 +358,7 @@ struct lifter_tbl { [[nodiscard]] inline float parcel_virtual_temperature( const float pres, const float tmpk) const { if constexpr (Lft::tracks_moisture) { - return sharp::virtual_temperature(tmpk, this->rv, this->rl, - this->ri); + return sharp::virtual_temperature(tmpk, m_rv, m_rl, m_ri); } else { return sharp::virtual_temperature(tmpk, sharp::mixratio(pres, tmpk)); From c9282ee5600b318a8657dab4fcb2c622b90b21d0 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 13:19:06 -0500 Subject: [PATCH 10/55] updated table dimensions --- include/SHARPlib/parcel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index e948683d..fe3bd1ba 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -210,8 +210,8 @@ struct lifter_tbl { float m_rl = sharp::MISSING; float m_ri = sharp::MISSING; - std::size_t num_logp = 200; - std::size_t num_thetae = 200; + std::size_t num_logp = 251; + std::size_t num_thetae = 251; std::vector m_logp_coord; std::vector m_thetae_coord; From b533ac28ff63ea6d2e95d8e60f39c35a6770b2c8 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 13:44:02 -0500 Subject: [PATCH 11/55] added the explicit template instantiations for lifter_tbl (#101) --- include/SHARPlib/params/convective.h | 26 ++++++++++++++++ include/SHARPlib/params/fire.h | 18 ++++++++++++ include/SHARPlib/parcel.h | 44 ++++++++++++++++++++++++++++ include/SHARPlib/thermo.h | 36 ++++++++++++++++++----- src/SHARPlib/params/convective.cpp | 26 ++++++++++++++++ src/SHARPlib/params/fire.cpp | 16 ++++++++++ src/SHARPlib/parcel.cpp | 44 ++++++++++++++++++++++++++++ src/SHARPlib/thermo.cpp | 25 +++++++++++++--- 8 files changed, 223 insertions(+), 12 deletions(-) diff --git a/include/SHARPlib/params/convective.h b/include/SHARPlib/params/convective.h index dfe6cbcb..a8674f53 100644 --- a/include/SHARPlib/params/convective.h +++ b/include/SHARPlib/params/convective.h @@ -597,6 +597,20 @@ extern template PressureLayer effective_inflow_layer( const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, Parcel* mupcl); +extern template PressureLayer effective_inflow_layer>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float dewpoint[], + const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], + const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, + Parcel* mupcl); + +extern template PressureLayer effective_inflow_layer>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float dewpoint[], + const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], + const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, + Parcel* mupcl); + extern template float convective_temperature( lifter_wobus& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float mixratio[], @@ -609,6 +623,18 @@ extern template float convective_temperature( float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); +extern template float convective_temperature>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float virtemp[], + const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], + const std::ptrdiff_t N, float cinh_thresh); + +extern template float convective_temperature>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float virtemp[], + const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], + const std::ptrdiff_t N, float cinh_thresh); + /// @endcond } // end namespace sharp diff --git a/include/SHARPlib/params/fire.h b/include/SHARPlib/params/fire.h index 40a51c14..7a0895af 100644 --- a/include/SHARPlib/params/fire.h +++ b/include/SHARPlib/params/fire.h @@ -257,6 +257,24 @@ extern template float pyrocumulonimbus_firepower_threshold( float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, float phi, float beta_incr); +extern template float +pyrocumulonimbus_firepower_threshold>( + lifter_tbl& lifter, PressureLayer mix_layer, + const float pressure[], const float height[], const float temperature[], + const float mixratio[], const float virtemp[], const float uwin[], + const float vwin[], const float potential_temperature[], + float pcl_vtmpk_arr[], float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, + float phi, float beta_incr); + +extern template float +pyrocumulonimbus_firepower_threshold>( + lifter_tbl& lifter, PressureLayer mix_layer, + const float pressure[], const float height[], const float temperature[], + const float mixratio[], const float virtemp[], const float uwin[], + const float vwin[], const float potential_temperature[], + float pcl_vtmpk_arr[], float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, + float phi, float beta_incr); + /// @endcond } // namespace sharp diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index fe3bd1ba..a3b20db4 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -973,6 +973,34 @@ extern template Parcel Parcel::most_unstable_parcel( const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); +extern template Parcel +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + +extern template Parcel +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + +extern template Parcel +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + +extern template Parcel +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + extern template Parcel Parcel::mixed_layer_parcel( PressureLayer& mix_layer, const float pressure[], const float height[], const float pot_temperature[], const float wv_mixratio[], @@ -992,6 +1020,14 @@ extern template void Parcel::lift_parcel(lifter_cm1& liftpcl, float pcl_vtmpk_arr[], const std::ptrdiff_t N); +extern template void Parcel::lift_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_vtmpk_arr[], const std::ptrdiff_t N); + +extern template void Parcel::lift_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_vtmpk_arr[], const std::ptrdiff_t N); + extern template void DowndraftParcel::lower_parcel( lifter_wobus& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); @@ -1000,6 +1036,14 @@ extern template void DowndraftParcel::lower_parcel( lifter_cm1& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); +extern template void DowndraftParcel::lower_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_tmpk_arr[], const std::ptrdiff_t N); + +extern template void DowndraftParcel::lower_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_tmpk_arr[], const std::ptrdiff_t N); + /// @endcond } // end namespace sharp diff --git a/include/SHARPlib/thermo.h b/include/SHARPlib/thermo.h index cebf05b7..c03955a7 100644 --- a/include/SHARPlib/thermo.h +++ b/include/SHARPlib/thermo.h @@ -456,7 +456,7 @@ void drylift(float pressure, float temperature, float dewpoint, * \return wetbulb_temperature (K) */ template -[[nodiscard]] float wetbulb(Lft lifter, float pressure, float temperature, +[[nodiscard]] float wetbulb(Lft& lifter, float pressure, float temperature, float dewpoint) { #ifndef NO_QC if ((pressure == MISSING) || (temperature == MISSING) || @@ -503,8 +503,8 @@ template * \return wetbulb_potential_temperature (K) */ template -[[nodiscard]] float theta_wetbulb(Lft lifter, float pressure, float temperature, - float dewpoint) { +[[nodiscard]] float theta_wetbulb(Lft& lifter, float pressure, + float temperature, float dewpoint) { #ifndef NO_QC if ((pressure == MISSING) || (temperature == MISSING) || (dewpoint == MISSING)) { @@ -726,22 +726,42 @@ PressureLayer temperature_layer(const float pressure[], struct lifter_wobus; struct lifter_cm1; -extern template float wetbulb(lifter_wobus lifter, float pressure, - float temperature, float dewpoint); +template +struct lifter_tbl; + +extern template float wetbulb(lifter_wobus& lifter, + float pressure, float temperature, + float dewpoint); -extern template float wetbulb(lifter_cm1 lifter, float pressure, +extern template float wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); -extern template float theta_wetbulb(lifter_cm1 lifter, +extern template float wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + +extern template float wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + +extern template float theta_wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); -extern template float theta_wetbulb(lifter_wobus lifter, +extern template float theta_wetbulb(lifter_wobus& lifter, float pressure, float temperature, float dewpoint); +extern template float theta_wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + +extern template float theta_wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + /// @endcond } // end namespace sharp diff --git a/src/SHARPlib/params/convective.cpp b/src/SHARPlib/params/convective.cpp index 919a4370..b77e4886 100644 --- a/src/SHARPlib/params/convective.cpp +++ b/src/SHARPlib/params/convective.cpp @@ -468,6 +468,20 @@ template PressureLayer effective_inflow_layer( const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, Parcel* mupcl); +template PressureLayer effective_inflow_layer>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float dewpoint[], + const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], + const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, + Parcel* mupcl); + +template PressureLayer effective_inflow_layer>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float dewpoint[], + const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], + const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, + Parcel* mupcl); + template float convective_temperature( lifter_wobus& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float mixratio[], @@ -480,6 +494,18 @@ template float convective_temperature( float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); +template float convective_temperature>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float virtemp[], + const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], + const std::ptrdiff_t N, float cinh_thresh); + +template float convective_temperature>( + lifter_tbl& lifter, const float pressure[], + const float height[], const float temperature[], const float virtemp[], + const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], + const std::ptrdiff_t N, float cinh_thresh); + /// @endcond } // end namespace sharp diff --git a/src/SHARPlib/params/fire.cpp b/src/SHARPlib/params/fire.cpp index 1e0aba04..5ed75abe 100644 --- a/src/SHARPlib/params/fire.cpp +++ b/src/SHARPlib/params/fire.cpp @@ -114,6 +114,22 @@ template float pyrocumulonimbus_firepower_threshold( float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, float phi, float beta_incr); +template float pyrocumulonimbus_firepower_threshold>( + lifter_tbl& lifter, PressureLayer mix_layer, + const float pressure[], const float height[], const float temperature[], + const float mixratio[], const float virtemp[], const float uwin[], + const float vwin[], const float potential_temperature[], + float pcl_vtmpk_arr[], float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, + float phi, float beta_incr); + +template float pyrocumulonimbus_firepower_threshold>( + lifter_tbl& lifter, PressureLayer mix_layer, + const float pressure[], const float height[], const float temperature[], + const float mixratio[], const float virtemp[], const float uwin[], + const float vwin[], const float potential_temperature[], + float pcl_vtmpk_arr[], float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, + float phi, float beta_incr); + /// @endcond } // namespace sharp diff --git a/src/SHARPlib/parcel.cpp b/src/SHARPlib/parcel.cpp index a08756f6..5b3e852d 100644 --- a/src/SHARPlib/parcel.cpp +++ b/src/SHARPlib/parcel.cpp @@ -271,6 +271,34 @@ template Parcel Parcel::most_unstable_parcel( const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); +template Parcel +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + +template Parcel +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + +template Parcel +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + +template Parcel +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_tbl& lifter, + const float pressure[], const float height[], const float temperature[], + const float virtemp[], const float dewpoint[], float pcl_virtemp[], + float buoy_arr[], const std::ptrdiff_t N); + template Parcel Parcel::mixed_layer_parcel( PressureLayer& mix_layer, const float pressure[], const float height[], const float pot_temperature[], const float wv_mixratio[], @@ -291,6 +319,14 @@ template void Parcel::lift_parcel(lifter_cm1& liftpcl, float pcl_vtmpk_arr[], const std::ptrdiff_t N); +template void Parcel::lift_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_vtmpk_arr[], const std::ptrdiff_t N); + +template void Parcel::lift_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_vtmpk_arr[], const std::ptrdiff_t N); + template void DowndraftParcel::lower_parcel( lifter_wobus& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); @@ -299,6 +335,14 @@ template void DowndraftParcel::lower_parcel( lifter_cm1& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); +template void DowndraftParcel::lower_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_tmpk_arr[], const std::ptrdiff_t N); + +template void DowndraftParcel::lower_parcel>( + lifter_tbl& liftpcl, const float pressure_arr[], + float pcl_tmpk_arr[], const std::ptrdiff_t N); + /// @endcond } // end namespace sharp diff --git a/src/SHARPlib/thermo.cpp b/src/SHARPlib/thermo.cpp index bf530c98..7531df7b 100644 --- a/src/SHARPlib/thermo.cpp +++ b/src/SHARPlib/thermo.cpp @@ -641,18 +641,35 @@ PressureLayer temperature_layer(const float pressure[], /// @cond DOXYGEN_IGNORE -template float wetbulb(lifter_wobus lifter, float pressure, +template float wetbulb(lifter_wobus& lifter, float pressure, float temperature, float dewpoint); -template float wetbulb(lifter_cm1 lifter, float pressure, +template float wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); -template float theta_wetbulb(lifter_wobus lifter, float pressure, +template float wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + +template float wetbulb>(lifter_tbl& lifter, + float pressure, + float temperature, + float dewpoint); + +template float theta_wetbulb(lifter_wobus& lifter, float pressure, float temperature, float dewpoint); -template float theta_wetbulb(lifter_cm1 lifter, float pressure, +template float theta_wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); +template float theta_wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + +template float theta_wetbulb>( + lifter_tbl& lifter, float pressure, float temperature, + float dewpoint); + /// @endcond } // end namespace sharp From 274d6a5b4a62bc23dcd36ecd8a324490b8f091c1 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 13:58:16 -0500 Subject: [PATCH 12/55] separated table data from the lifter to better support OpenMP parallelism (#101) --- include/SHARPlib/parcel.h | 71 ++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index a3b20db4..ce872d03 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -195,10 +195,8 @@ struct lifter_cm1 { }; template -struct lifter_tbl { +struct tbl_data { Lft m_lifter; - static constexpr bool lift_from_lcl = Lft::lift_from_lcl; - static constexpr bool tracks_moisture = Lft::tracks_moisture; float p_max = 110000.0f; float p_min = 100.0f; @@ -206,9 +204,6 @@ struct lifter_tbl { float thetae_max = 500.0f; float m_delta_thetae_inv = 0.0f; float m_delta_logp_inv = 0.0f; - float m_rv = sharp::MISSING; - float m_rl = sharp::MISSING; - float m_ri = sharp::MISSING; std::size_t num_logp = 251; std::size_t num_thetae = 251; @@ -220,16 +215,7 @@ struct lifter_tbl { std::vector m_LUT_pcl_rl; std::vector m_LUT_pcl_ri; - lifter_tbl(Lft lifter) : m_lifter(std::move(lifter)) {} - - inline void setup([[maybe_unused]] const float lcl_pres, - [[maybe_unused]] const float lcl_tmpk) { - if constexpr (Lft::tracks_moisture) { - m_rv = sharp::MISSING; - m_rl = sharp::MISSING; - m_ri = sharp::MISSING; - } - } + tbl_data(Lft lifter) : m_lifter(std::move(lifter)) {} inline void generate_table() { m_logp_coord.resize(num_logp); @@ -313,33 +299,58 @@ struct lifter_tbl { fmt::println("Failed to converge on a temperature for thetae."); return tmpk_mid; } +}; + +template +struct lifter_tbl { + const tbl_data& m_data; + static constexpr bool lift_from_lcl = Lft::lift_from_lcl; + static constexpr bool tracks_moisture = Lft::tracks_moisture; + + float m_rv = sharp::MISSING; + float m_rl = sharp::MISSING; + float m_ri = sharp::MISSING; + + lifter_tbl(const tbl_data& data) : m_data(data) {} + + inline void setup([[maybe_unused]] const float lcl_pres, + [[maybe_unused]] const float lcl_tmpk) { + if constexpr (Lft::tracks_moisture) { + m_rv = sharp::MISSING; + m_rl = sharp::MISSING; + m_ri = sharp::MISSING; + } + } [[nodiscard]] inline float operator()(const float pres, const float tmpk, const float new_pres) { - const float target_thetae = thetae(pres, tmpk, tmpk); + const float target_thetae = sharp::thetae(pres, tmpk, tmpk); - float fi = (target_thetae - thetae_min) * m_delta_thetae_inv; - fi = std::clamp(fi, 0.0f, static_cast(num_thetae - 1.001f)); + // Access all the bounding limits and spacing from m_data + float fi = + (target_thetae - m_data.thetae_min) * m_data.m_delta_thetae_inv; + fi = std::clamp(fi, 0.0f, + static_cast(m_data.num_thetae - 1.001f)); const std::size_t i0 = static_cast(fi); const std::size_t i1 = i0 + 1; const float wi = fi - static_cast(i0); const float target_logp = std::log(new_pres); - const float logp_max = std::log(p_max); + const float logp_max = std::log(m_data.p_max); - float fk = (target_logp - logp_max) * m_delta_logp_inv; - fk = std::clamp(fk, 0.0f, static_cast(num_logp - 1.001f)); + float fk = (target_logp - logp_max) * m_data.m_delta_logp_inv; + fk = std::clamp(fk, 0.0f, static_cast(m_data.num_logp - 1.001f)); const std::size_t k0 = static_cast(fk); const std::size_t k1 = k0 + 1; const float wk = fk - static_cast(k0); auto bilinear_interp = [&](const std::vector& lut) -> float { - const float val00 = lut[i0 * num_logp + k0]; - const float val01 = lut[i0 * num_logp + k1]; - const float val10 = lut[i1 * num_logp + k0]; - const float val11 = lut[i1 * num_logp + k1]; + const float val00 = lut[i0 * m_data.num_logp + k0]; + const float val01 = lut[i0 * m_data.num_logp + k1]; + const float val10 = lut[i1 * m_data.num_logp + k0]; + const float val11 = lut[i1 * m_data.num_logp + k1]; const float val0 = sharp::lerp(val00, val01, wk); const float val1 = sharp::lerp(val10, val11, wk); @@ -347,12 +358,12 @@ struct lifter_tbl { }; if constexpr (Lft::tracks_moisture) { - m_rv = bilinear_interp(m_LUT_pcl_rv); - m_rl = bilinear_interp(m_LUT_pcl_rl); - m_ri = bilinear_interp(m_LUT_pcl_ri); + m_rv = bilinear_interp(m_data.m_LUT_pcl_rv); + m_rl = bilinear_interp(m_data.m_LUT_pcl_rl); + m_ri = bilinear_interp(m_data.m_LUT_pcl_ri); } - return bilinear_interp(m_LUT_pcl_tmpk); + return bilinear_interp(m_data.m_LUT_pcl_tmpk); } [[nodiscard]] inline float parcel_virtual_temperature( From d138e43b60b3a15e5d29bb81033113fcc7b56804 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 14:11:33 -0500 Subject: [PATCH 13/55] add maybe_unused --- include/SHARPlib/parcel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index ce872d03..016d714d 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -367,7 +367,7 @@ struct lifter_tbl { } [[nodiscard]] inline float parcel_virtual_temperature( - const float pres, const float tmpk) const { + [[maybe_unused]] const float pres, const float tmpk) const { if constexpr (Lft::tracks_moisture) { return sharp::virtual_temperature(tmpk, m_rv, m_rl, m_ri); } else { From 5c2302afccdb9d4ee5d4698b192aede702f605a0 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 15:01:39 -0500 Subject: [PATCH 14/55] refining the ranges of values --- include/SHARPlib/parcel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 016d714d..0263d323 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -201,7 +201,7 @@ struct tbl_data { float p_max = 110000.0f; float p_min = 100.0f; float thetae_min = 250.0f; - float thetae_max = 500.0f; + float thetae_max = 450.0f; float m_delta_thetae_inv = 0.0f; float m_delta_logp_inv = 0.0f; @@ -276,7 +276,7 @@ struct tbl_data { [[nodiscard]] float solve_tmpk_for_thetae(float pressure, float thetae_target) const { float tmpk_lo = 200.0f; - float tmpk_hi = 400.0f; + float tmpk_hi = 330.0f; float tmpk_mid = sharp::ZEROCNK; const float tol = 0.001f; From 6c82d3528f73d2204964386cf9fb9f965b4e25ad Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 15:25:47 -0500 Subject: [PATCH 15/55] brought the ceiling to 10 hPa --- include/SHARPlib/parcel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 0263d323..7988a19e 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -199,7 +199,7 @@ struct tbl_data { Lft m_lifter; float p_max = 110000.0f; - float p_min = 100.0f; + float p_min = 1000.0f; float thetae_min = 250.0f; float thetae_max = 450.0f; float m_delta_thetae_inv = 0.0f; From 6efa0a8ac4a9d9fd2b5f837f6031cb258b11702a Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 15:28:57 -0500 Subject: [PATCH 16/55] trying to work with the relaxation term --- src/SHARPlib/thermo.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SHARPlib/thermo.cpp b/src/SHARPlib/thermo.cpp index 7531df7b..db04851b 100644 --- a/src/SHARPlib/thermo.cpp +++ b/src/SHARPlib/thermo.cpp @@ -294,9 +294,10 @@ float _solve_cm1(float& pcl_pres_next, float& pcl_pi_next, float& pcl_t_next, pcl_theta_next = pcl_theta_prev * std::exp(term); + float relax = (iter_count > 20) ? 0.05f : 0.3f; if (std::abs(pcl_theta_next - pcl_theta_last) > converge) { pcl_theta_last = - pcl_theta_last + 0.3 * (pcl_theta_next - pcl_theta_last); + pcl_theta_last + relax * (pcl_theta_next - pcl_theta_last); } else { not_converged = false; } From 49ba0f9c1ec71bf28cf16efa952dc66c24fefad8 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 15:30:53 -0500 Subject: [PATCH 17/55] expanding the temperature range --- include/SHARPlib/parcel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 7988a19e..0902da82 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -200,8 +200,8 @@ struct tbl_data { float p_max = 110000.0f; float p_min = 1000.0f; - float thetae_min = 250.0f; - float thetae_max = 450.0f; + float thetae_min = 200.0f; + float thetae_max = 500.0f; float m_delta_thetae_inv = 0.0f; float m_delta_logp_inv = 0.0f; From b4112c7644cb174149714795f1aa57ce8c067fd7 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 15:34:35 -0500 Subject: [PATCH 18/55] updated grid axes --- include/SHARPlib/parcel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 0902da82..97c80bcf 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -205,8 +205,8 @@ struct tbl_data { float m_delta_thetae_inv = 0.0f; float m_delta_logp_inv = 0.0f; - std::size_t num_logp = 251; - std::size_t num_thetae = 251; + std::size_t num_logp = 301; + std::size_t num_thetae = 301; std::vector m_logp_coord; std::vector m_thetae_coord; From 0519648cb633ede75e68d489c844e9585d262dcc Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 15:51:03 -0500 Subject: [PATCH 19/55] trying to fix lifter_tbl --- include/SHARPlib/parcel.h | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 97c80bcf..5a611004 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -307,14 +307,20 @@ struct lifter_tbl { static constexpr bool lift_from_lcl = Lft::lift_from_lcl; static constexpr bool tracks_moisture = Lft::tracks_moisture; + float m_fi = 0.0f; float m_rv = sharp::MISSING; float m_rl = sharp::MISSING; float m_ri = sharp::MISSING; lifter_tbl(const tbl_data& data) : m_data(data) {} - inline void setup([[maybe_unused]] const float lcl_pres, - [[maybe_unused]] const float lcl_tmpk) { + inline void setup(const float lcl_pres, const float lcl_tmpk) { + const float target_thetae = sharp::thetae(lcl_pres, lcl_tmpk, lcl_tmpk); + + m_fi = (target_thetae - m_data.thetae_min) * m_data.m_delta_thetae_inv; + m_fi = std::clamp(m_fi, 0.0f, + static_cast(m_data.num_thetae - 1.001f)); + if constexpr (Lft::tracks_moisture) { m_rv = sharp::MISSING; m_rl = sharp::MISSING; @@ -322,19 +328,12 @@ struct lifter_tbl { } } - [[nodiscard]] inline float operator()(const float pres, const float tmpk, + [[nodiscard]] inline float operator()([[maybe_unused]] const float pres, + [[maybe_unused]] const float tmpk, const float new_pres) { - const float target_thetae = sharp::thetae(pres, tmpk, tmpk); - - // Access all the bounding limits and spacing from m_data - float fi = - (target_thetae - m_data.thetae_min) * m_data.m_delta_thetae_inv; - fi = std::clamp(fi, 0.0f, - static_cast(m_data.num_thetae - 1.001f)); - - const std::size_t i0 = static_cast(fi); + const std::size_t i0 = static_cast(m_fi); const std::size_t i1 = i0 + 1; - const float wi = fi - static_cast(i0); + const float wi = m_fi - static_cast(i0); const float target_logp = std::log(new_pres); const float logp_max = std::log(m_data.p_max); From dac07bf9853cd256e2aca5f7266e9d6370b70ed4 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 16:04:02 -0500 Subject: [PATCH 20/55] trying to fix lookup --- include/SHARPlib/parcel.h | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 5a611004..d69a4c4e 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -315,11 +315,40 @@ struct lifter_tbl { lifter_tbl(const tbl_data& data) : m_data(data) {} inline void setup(const float lcl_pres, const float lcl_tmpk) { - const float target_thetae = sharp::thetae(lcl_pres, lcl_tmpk, lcl_tmpk); + const float target_logp = std::log(lcl_pres); + const float logp_max = std::log(m_data.p_max); + + float fk = (target_logp - logp_max) * m_data.m_delta_logp_inv; + fk = std::clamp(fk, 0.0f, static_cast(m_data.num_logp - 1.001f)); + + const std::size_t k0 = static_cast(fk); + const std::size_t k1 = k0 + 1; + const float wk = fk - static_cast(k0); - m_fi = (target_thetae - m_data.thetae_min) * m_data.m_delta_thetae_inv; - m_fi = std::clamp(m_fi, 0.0f, - static_cast(m_data.num_thetae - 1.001f)); + float fi_lo = 0.0f; + float fi_hi = static_cast(m_data.num_thetae - 1.001f); + + for (int step = 0; step < 20; ++step) { + m_fi = 0.5f * (fi_lo + fi_hi); + + std::size_t i0 = static_cast(m_fi); + std::size_t i1 = i0 + 1; + float wi = m_fi - static_cast(i0); + + float t0 = sharp::lerp( + m_data.m_LUT_pcl_tmpk[i0 * m_data.num_logp + k0], + m_data.m_LUT_pcl_tmpk[i0 * m_data.num_logp + k1], wk); + float t1 = sharp::lerp( + m_data.m_LUT_pcl_tmpk[i1 * m_data.num_logp + k0], + m_data.m_LUT_pcl_tmpk[i1 * m_data.num_logp + k1], wk); + float t_interp = sharp::lerp(t0, t1, wi); + + if (t_interp < lcl_tmpk) { + fi_lo = m_fi; + } else { + fi_hi = m_fi; + } + } if constexpr (Lft::tracks_moisture) { m_rv = sharp::MISSING; From f781600c58ab25e9ff3979aa783c0b8f81892f02 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 16:19:10 -0500 Subject: [PATCH 21/55] semi-working fix (at least at the LCL) --- include/SHARPlib/parcel.h | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index d69a4c4e..293e686e 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -312,6 +312,9 @@ struct lifter_tbl { float m_rl = sharp::MISSING; float m_ri = sharp::MISSING; + float m_rl_base = 0.0f; + float m_ri_base = 0.0f; + lifter_tbl(const tbl_data& data) : m_data(data) {} inline void setup(const float lcl_pres, const float lcl_tmpk) { @@ -330,7 +333,6 @@ struct lifter_tbl { for (int step = 0; step < 20; ++step) { m_fi = 0.5f * (fi_lo + fi_hi); - std::size_t i0 = static_cast(m_fi); std::size_t i1 = i0 + 1; float wi = m_fi - static_cast(i0); @@ -343,14 +345,28 @@ struct lifter_tbl { m_data.m_LUT_pcl_tmpk[i1 * m_data.num_logp + k1], wk); float t_interp = sharp::lerp(t0, t1, wi); - if (t_interp < lcl_tmpk) { + if (t_interp < lcl_tmpk) fi_lo = m_fi; - } else { + else fi_hi = m_fi; - } } if constexpr (Lft::tracks_moisture) { + std::size_t i0 = static_cast(m_fi); + std::size_t i1 = i0 + 1; + float wi = m_fi - static_cast(i0); + + auto get_base = [&](const std::vector& lut) -> float { + float v0 = sharp::lerp(lut[i0 * m_data.num_logp + k0], + lut[i0 * m_data.num_logp + k1], wk); + float v1 = sharp::lerp(lut[i1 * m_data.num_logp + k0], + lut[i1 * m_data.num_logp + k1], wk); + return sharp::lerp(v0, v1, wi); + }; + + m_rl_base = get_base(m_data.m_LUT_pcl_rl); + m_ri_base = get_base(m_data.m_LUT_pcl_ri); + m_rv = sharp::MISSING; m_rl = sharp::MISSING; m_ri = sharp::MISSING; @@ -387,8 +403,12 @@ struct lifter_tbl { if constexpr (Lft::tracks_moisture) { m_rv = bilinear_interp(m_data.m_LUT_pcl_rv); - m_rl = bilinear_interp(m_data.m_LUT_pcl_rl); - m_ri = bilinear_interp(m_data.m_LUT_pcl_ri); + + // OFFSET the water loading so it starts at 0 at the LCL! + m_rl = std::max(0.0f, + bilinear_interp(m_data.m_LUT_pcl_rl) - m_rl_base); + m_ri = std::max(0.0f, + bilinear_interp(m_data.m_LUT_pcl_ri) - m_ri_base); } return bilinear_interp(m_data.m_LUT_pcl_tmpk); From 5e27b26383df4f099d1fca0f7dc933999be833fc Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 24 Mar 2026 16:28:07 -0500 Subject: [PATCH 22/55] fail on adiabatic parcels --- include/SHARPlib/parcel.h | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 293e686e..33250536 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -215,7 +215,17 @@ struct tbl_data { std::vector m_LUT_pcl_rl; std::vector m_LUT_pcl_ri; - tbl_data(Lft lifter) : m_lifter(std::move(lifter)) {} + tbl_data(Lft lifter) : m_lifter(std::move(lifter)) { + if (m_lifter.ma_type == sharp::adiabat::adiab_liq || + m_lifter.ma_type == sharp::adiabat::adiab_ice) { + throw std::invalid_argument( + "CRITICAL ERROR: sharp::tbl_data does not support reversible " + "adiabatic ascent (adiab_liq or adiab_ice). The 2D lookup " + "table " + "cannot track conserved total water mass. Please configure the " + "lifter to use pseudo_liq or pseudo_ice instead."); + } + } inline void generate_table() { m_logp_coord.resize(num_logp); From da213ee08a36a1662fbfff8a962c8a11fa41391a Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 12:10:50 -0500 Subject: [PATCH 23/55] update table bounds --- include/SHARPlib/parcel.h | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 33250536..fb7b4b28 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -200,13 +200,13 @@ struct tbl_data { float p_max = 110000.0f; float p_min = 1000.0f; - float thetae_min = 200.0f; - float thetae_max = 500.0f; + float thetae_min = 210.0f; + float thetae_max = 430.0f; float m_delta_thetae_inv = 0.0f; float m_delta_logp_inv = 0.0f; - std::size_t num_logp = 301; - std::size_t num_thetae = 301; + std::size_t num_logp = 251; + std::size_t num_thetae = 221; std::vector m_logp_coord; std::vector m_thetae_coord; @@ -221,9 +221,9 @@ struct tbl_data { throw std::invalid_argument( "CRITICAL ERROR: sharp::tbl_data does not support reversible " "adiabatic ascent (adiab_liq or adiab_ice). The 2D lookup " - "table " - "cannot track conserved total water mass. Please configure the " - "lifter to use pseudo_liq or pseudo_ice instead."); + "table cannot track conserved total water mass. Please " + "configure the lifter to use pseudo_liq or pseudo_ice " + "instead."); } } From 6aa6f1c1291b1818964f7ab7095fabcfe2a3b84d Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 12:25:45 -0500 Subject: [PATCH 24/55] added ability to fall back to direct solver if needed --- include/SHARPlib/parcel.h | 68 ++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index fb7b4b28..af0e99cc 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -317,15 +317,16 @@ struct lifter_tbl { static constexpr bool lift_from_lcl = Lft::lift_from_lcl; static constexpr bool tracks_moisture = Lft::tracks_moisture; + bool m_use_lifter = false; + Lft m_lifter; + float m_fi = 0.0f; float m_rv = sharp::MISSING; float m_rl = sharp::MISSING; float m_ri = sharp::MISSING; - float m_rl_base = 0.0f; - float m_ri_base = 0.0f; - - lifter_tbl(const tbl_data& data) : m_data(data) {} + lifter_tbl(const tbl_data& data) + : m_data(data), m_lifter(data.m_lifter) {} inline void setup(const float lcl_pres, const float lcl_tmpk) { const float target_logp = std::log(lcl_pres); @@ -338,6 +339,24 @@ struct lifter_tbl { const std::size_t k1 = k0 + 1; const float wk = fk - static_cast(k0); + std::size_t i_max = m_data.num_thetae - 1; + + float t_min_bound = + sharp::lerp(m_data.m_LUT_pcl_tmpk[0 * m_data.num_logp + k0], + m_data.m_LUT_pcl_tmpk[0 * m_data.num_logp + k1], wk); + + float t_max_bound = sharp::lerp( + m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k0], + m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k1], wk); + + if (lcl_tmpk < t_min_bound || lcl_tmpk > t_max_bound) { + m_use_lifter = true; + m_lifter.setup(lcl_pres, lcl_tmpk); + return; + } + + m_use_lifter = false; + float fi_lo = 0.0f; float fi_hi = static_cast(m_data.num_thetae - 1.001f); @@ -355,37 +374,26 @@ struct lifter_tbl { m_data.m_LUT_pcl_tmpk[i1 * m_data.num_logp + k1], wk); float t_interp = sharp::lerp(t0, t1, wi); - if (t_interp < lcl_tmpk) + if (t_interp < lcl_tmpk) { fi_lo = m_fi; - else + } else { fi_hi = m_fi; + } } if constexpr (Lft::tracks_moisture) { - std::size_t i0 = static_cast(m_fi); - std::size_t i1 = i0 + 1; - float wi = m_fi - static_cast(i0); - - auto get_base = [&](const std::vector& lut) -> float { - float v0 = sharp::lerp(lut[i0 * m_data.num_logp + k0], - lut[i0 * m_data.num_logp + k1], wk); - float v1 = sharp::lerp(lut[i1 * m_data.num_logp + k0], - lut[i1 * m_data.num_logp + k1], wk); - return sharp::lerp(v0, v1, wi); - }; - - m_rl_base = get_base(m_data.m_LUT_pcl_rl); - m_ri_base = get_base(m_data.m_LUT_pcl_ri); - m_rv = sharp::MISSING; m_rl = sharp::MISSING; m_ri = sharp::MISSING; } } - [[nodiscard]] inline float operator()([[maybe_unused]] const float pres, - [[maybe_unused]] const float tmpk, + [[nodiscard]] inline float operator()(const float pres, const float tmpk, const float new_pres) { + if (m_use_lifter) { + return m_lifter(pres, tmpk, new_pres); + } + const std::size_t i0 = static_cast(m_fi); const std::size_t i1 = i0 + 1; const float wi = m_fi - static_cast(i0); @@ -413,19 +421,19 @@ struct lifter_tbl { if constexpr (Lft::tracks_moisture) { m_rv = bilinear_interp(m_data.m_LUT_pcl_rv); - - // OFFSET the water loading so it starts at 0 at the LCL! - m_rl = std::max(0.0f, - bilinear_interp(m_data.m_LUT_pcl_rl) - m_rl_base); - m_ri = std::max(0.0f, - bilinear_interp(m_data.m_LUT_pcl_ri) - m_ri_base); + m_rl = bilinear_interp(m_data.m_LUT_pcl_rl); + m_ri = bilinear_interp(m_data.m_LUT_pcl_ri); } return bilinear_interp(m_data.m_LUT_pcl_tmpk); } [[nodiscard]] inline float parcel_virtual_temperature( - [[maybe_unused]] const float pres, const float tmpk) const { + const float pres, const float tmpk) const { + if (m_use_lifter) { + return m_lifter.parcel_virtual_temperature(pres, tmpk); + } + if constexpr (Lft::tracks_moisture) { return sharp::virtual_temperature(tmpk, m_rv, m_rl, m_ri); } else { From 2d2d5e394f7d81ef66ac5ab7e4b2511ec705a15b Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 12:35:35 -0500 Subject: [PATCH 25/55] updated pressure grid --- include/SHARPlib/parcel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index af0e99cc..745f23b7 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -199,13 +199,13 @@ struct tbl_data { Lft m_lifter; float p_max = 110000.0f; - float p_min = 1000.0f; + float p_min = 5000.0f; float thetae_min = 210.0f; float thetae_max = 430.0f; float m_delta_thetae_inv = 0.0f; float m_delta_logp_inv = 0.0f; - std::size_t num_logp = 251; + std::size_t num_logp = 201; std::size_t num_thetae = 221; std::vector m_logp_coord; From cb29b4ad0e8213ddfb8c3fdf051f15dd2e693700 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 13:03:47 -0500 Subject: [PATCH 26/55] return MISSING if outside of pressure range --- include/SHARPlib/parcel.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 745f23b7..cc1a82b9 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -394,6 +394,8 @@ struct lifter_tbl { return m_lifter(pres, tmpk, new_pres); } + if (new_pres < m_data.p_min) return MISSING; + const std::size_t i0 = static_cast(m_fi); const std::size_t i1 = i0 + 1; const float wi = m_fi - static_cast(i0); From a0441f1a85042909551398346d912401b5c824b6 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 13:04:19 -0500 Subject: [PATCH 27/55] return MISSING if outside of pressure range --- include/SHARPlib/parcel.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index cc1a82b9..c5ca97b9 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -394,7 +394,8 @@ struct lifter_tbl { return m_lifter(pres, tmpk, new_pres); } - if (new_pres < m_data.p_min) return MISSING; + if ((new_pres < m_data.p_min) || (new_pres > m_data.p_max)) + return MISSING; const std::size_t i0 = static_cast(m_fi); const std::size_t i1 = i0 + 1; From 73be16258c92acb93239252396d3253891f69e11 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 13:17:12 -0500 Subject: [PATCH 28/55] added pressure check to LCL --- include/SHARPlib/parcel.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index c5ca97b9..146d99ea 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -349,7 +349,8 @@ struct lifter_tbl { m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k0], m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k1], wk); - if (lcl_tmpk < t_min_bound || lcl_tmpk > t_max_bound) { + if (lcl_tmpk < t_min_bound || lcl_tmpk > t_max_bound || + lcl_pres < m_data.p_min || lcl_pres > m_data.p_max) { m_use_lifter = true; m_lifter.setup(lcl_pres, lcl_tmpk); return; From 14efc588c42d2362a406029e5702b6e3d6037b3e Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 14:52:01 -0500 Subject: [PATCH 29/55] removed old ice and liquid trackers --- include/SHARPlib/parcel.h | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 146d99ea..4a7d28e7 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -212,8 +212,6 @@ struct tbl_data { std::vector m_thetae_coord; std::vector m_LUT_pcl_tmpk; std::vector m_LUT_pcl_rv; - std::vector m_LUT_pcl_rl; - std::vector m_LUT_pcl_ri; tbl_data(Lft lifter) : m_lifter(std::move(lifter)) { if (m_lifter.ma_type == sharp::adiabat::adiab_liq || @@ -248,8 +246,6 @@ struct tbl_data { std::size_t size_2D = num_logp * num_thetae; m_LUT_pcl_tmpk.resize(size_2D); m_LUT_pcl_rv.resize(size_2D); - m_LUT_pcl_rl.resize(size_2D); - m_LUT_pcl_ri.resize(size_2D); for (std::size_t i = 0; i < num_thetae; ++i) { const float thetae_target = m_thetae_coord[i]; @@ -270,12 +266,8 @@ struct tbl_data { if constexpr (Lft::tracks_moisture) { m_LUT_pcl_rv[idx_2D] = m_lifter.rv; - m_LUT_pcl_rl[idx_2D] = m_lifter.rl; - m_LUT_pcl_ri[idx_2D] = m_lifter.ri; } else { m_LUT_pcl_rv[idx_2D] = mixratio(pres, tmpk_curr); - m_LUT_pcl_rl[idx_2D] = 0.0f; - m_LUT_pcl_ri[idx_2D] = 0.0f; } pres_curr = pres; @@ -425,8 +417,6 @@ struct lifter_tbl { if constexpr (Lft::tracks_moisture) { m_rv = bilinear_interp(m_data.m_LUT_pcl_rv); - m_rl = bilinear_interp(m_data.m_LUT_pcl_rl); - m_ri = bilinear_interp(m_data.m_LUT_pcl_ri); } return bilinear_interp(m_data.m_LUT_pcl_tmpk); From 1154dbac1100a41a08a3a08d6313d119beec6be5 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 14:54:33 -0500 Subject: [PATCH 30/55] cannot have the rl and ri be MISSING --- include/SHARPlib/parcel.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 4a7d28e7..ecf2e4ca 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -417,6 +417,8 @@ struct lifter_tbl { if constexpr (Lft::tracks_moisture) { m_rv = bilinear_interp(m_data.m_LUT_pcl_rv); + m_rl = 0.0f; + m_ri = 0.0f; } return bilinear_interp(m_data.m_LUT_pcl_tmpk); From 2403e0a5a0813ed82df8927378f304aad68d721c Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 16:15:40 -0500 Subject: [PATCH 31/55] refactored the structure to better reflect a proper separation of concerns --- include/SHARPlib/parcel.h | 149 +++++++++++++++++++++----------------- 1 file changed, 84 insertions(+), 65 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index ecf2e4ca..0ade89a8 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -243,7 +243,7 @@ struct tbl_data { m_logp_coord[i] = logp_max + i * delta_logp; } - std::size_t size_2D = num_logp * num_thetae; + const std::size_t size_2D = num_logp * num_thetae; m_LUT_pcl_tmpk.resize(size_2D); m_LUT_pcl_rv.resize(size_2D); @@ -256,7 +256,7 @@ struct tbl_data { float tmpk_curr = tmpk; for (std::size_t k = 0; k < num_logp; ++k) { - float pres = std::exp(m_logp_coord[k]); + const float pres = std::exp(m_logp_coord[k]); if (k > 0) { tmpk_curr = m_lifter(pres_curr, tmpk_curr, pres); } @@ -267,7 +267,7 @@ struct tbl_data { if constexpr (Lft::tracks_moisture) { m_LUT_pcl_rv[idx_2D] = m_lifter.rv; } else { - m_LUT_pcl_rv[idx_2D] = mixratio(pres, tmpk_curr); + m_LUT_pcl_rv[idx_2D] = sharp::mixratio(pres, tmpk_curr); } pres_curr = pres; @@ -286,7 +286,8 @@ struct tbl_data { for (std::size_t i = 0; i < max_iters; ++i) { tmpk_mid = 0.5f * (tmpk_lo + tmpk_hi); - const float curr_thetae = thetae(pressure, tmpk_mid, tmpk_mid); + const float curr_thetae = + sharp::thetae(pressure, tmpk_mid, tmpk_mid); if (std::abs(curr_thetae - thetae_target) < tol) { return tmpk_mid; @@ -298,9 +299,74 @@ struct tbl_data { tmpk_hi = tmpk_mid; } } - fmt::println("Failed to converge on a temperature for thetae."); + fmt::println("Failed to converge on a LCL temperature for thetae."); return tmpk_mid; } + + [[nodiscard]] inline std::tuple + get_logp_indices(const float pres) const { + const float target_logp = std::log(pres); + const float logp_max = std::log(p_max); + + const float fk = + std::max((target_logp - logp_max) * m_delta_logp_inv, 0.0f); + const std::size_t k0 = + std::min(static_cast(fk), num_logp - 2); + + const std::size_t k1 = k0 + 1; + const float wk = std::clamp(fk - static_cast(k0), 0.0f, 1.0f); + + return {k0, k1, wk}; + } + + [[nodiscard]] inline float find_thetae_index(const float lcl_tmpk, + const std::size_t k0, + const std::size_t k1, + const float wk) const { + float fi_lo = 0.0f; + float fi_hi = static_cast(num_thetae - 1); + float fi_mid = 0.0f; + + for (int step = 0; step < 20; ++step) { + fi_mid = 0.5f * (fi_lo + fi_hi); + + const std::size_t i0 = + std::min(static_cast(fi_mid), num_thetae - 2); + + const std::size_t i1 = i0 + 1; + const float wi = + std::clamp(fi_mid - static_cast(i0), 0.0f, 1.0f); + + const float t0 = + sharp::lerp(m_LUT_pcl_tmpk[i0 * num_logp + k0], + m_LUT_pcl_tmpk[i0 * num_logp + k1], wk); + const float t1 = + sharp::lerp(m_LUT_pcl_tmpk[i1 * num_logp + k0], + m_LUT_pcl_tmpk[i1 * num_logp + k1], wk); + const float t_interp = sharp::lerp(t0, t1, wi); + + if (t_interp < lcl_tmpk) { + fi_lo = fi_mid; + } else { + fi_hi = fi_mid; + } + } + return fi_mid; + } + + [[nodiscard]] inline float bilinear_interp( + const std::vector& lut, const std::size_t i0, + const std::size_t i1, const float wi, const std::size_t k0, + const std::size_t k1, const float wk) const { + const float val00 = lut[i0 * num_logp + k0]; + const float val01 = lut[i0 * num_logp + k1]; + const float val10 = lut[i1 * num_logp + k0]; + const float val11 = lut[i1 * num_logp + k1]; + + const float val0 = sharp::lerp(val00, val01, wk); + const float val1 = sharp::lerp(val10, val11, wk); + return sharp::lerp(val0, val1, wi); + } }; template @@ -321,23 +387,15 @@ struct lifter_tbl { : m_data(data), m_lifter(data.m_lifter) {} inline void setup(const float lcl_pres, const float lcl_tmpk) { - const float target_logp = std::log(lcl_pres); - const float logp_max = std::log(m_data.p_max); - - float fk = (target_logp - logp_max) * m_data.m_delta_logp_inv; - fk = std::clamp(fk, 0.0f, static_cast(m_data.num_logp - 1.001f)); - - const std::size_t k0 = static_cast(fk); - const std::size_t k1 = k0 + 1; - const float wk = fk - static_cast(k0); + const auto [k0, k1, wk] = m_data.get_logp_indices(lcl_pres); - std::size_t i_max = m_data.num_thetae - 1; + const std::size_t i_max = m_data.num_thetae - 1; - float t_min_bound = + const float t_min_bound = sharp::lerp(m_data.m_LUT_pcl_tmpk[0 * m_data.num_logp + k0], m_data.m_LUT_pcl_tmpk[0 * m_data.num_logp + k1], wk); - float t_max_bound = sharp::lerp( + const float t_max_bound = sharp::lerp( m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k0], m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k1], wk); @@ -350,29 +408,7 @@ struct lifter_tbl { m_use_lifter = false; - float fi_lo = 0.0f; - float fi_hi = static_cast(m_data.num_thetae - 1.001f); - - for (int step = 0; step < 20; ++step) { - m_fi = 0.5f * (fi_lo + fi_hi); - std::size_t i0 = static_cast(m_fi); - std::size_t i1 = i0 + 1; - float wi = m_fi - static_cast(i0); - - float t0 = sharp::lerp( - m_data.m_LUT_pcl_tmpk[i0 * m_data.num_logp + k0], - m_data.m_LUT_pcl_tmpk[i0 * m_data.num_logp + k1], wk); - float t1 = sharp::lerp( - m_data.m_LUT_pcl_tmpk[i1 * m_data.num_logp + k0], - m_data.m_LUT_pcl_tmpk[i1 * m_data.num_logp + k1], wk); - float t_interp = sharp::lerp(t0, t1, wi); - - if (t_interp < lcl_tmpk) { - fi_lo = m_fi; - } else { - fi_hi = m_fi; - } - } + m_fi = m_data.find_thetae_index(lcl_tmpk, k0, k1, wk); if constexpr (Lft::tracks_moisture) { m_rv = sharp::MISSING; @@ -390,38 +426,21 @@ struct lifter_tbl { if ((new_pres < m_data.p_min) || (new_pres > m_data.p_max)) return MISSING; - const std::size_t i0 = static_cast(m_fi); + const std::size_t i0 = + std::min(static_cast(m_fi), m_data.num_thetae - 2); const std::size_t i1 = i0 + 1; - const float wi = m_fi - static_cast(i0); - - const float target_logp = std::log(new_pres); - const float logp_max = std::log(m_data.p_max); - - float fk = (target_logp - logp_max) * m_data.m_delta_logp_inv; - fk = std::clamp(fk, 0.0f, static_cast(m_data.num_logp - 1.001f)); - - const std::size_t k0 = static_cast(fk); - const std::size_t k1 = k0 + 1; - const float wk = fk - static_cast(k0); - - auto bilinear_interp = [&](const std::vector& lut) -> float { - const float val00 = lut[i0 * m_data.num_logp + k0]; - const float val01 = lut[i0 * m_data.num_logp + k1]; - const float val10 = lut[i1 * m_data.num_logp + k0]; - const float val11 = lut[i1 * m_data.num_logp + k1]; - - const float val0 = sharp::lerp(val00, val01, wk); - const float val1 = sharp::lerp(val10, val11, wk); - return sharp::lerp(val0, val1, wi); - }; + const float wi = std::clamp(m_fi - static_cast(i0), 0.0f, 1.0f); + const auto [k0, k1, wk] = m_data.get_logp_indices(new_pres); if constexpr (Lft::tracks_moisture) { - m_rv = bilinear_interp(m_data.m_LUT_pcl_rv); + m_rv = m_data.bilinear_interp(m_data.m_LUT_pcl_rv, i0, i1, wi, k0, + k1, wk); m_rl = 0.0f; m_ri = 0.0f; } - return bilinear_interp(m_data.m_LUT_pcl_tmpk); + return m_data.bilinear_interp(m_data.m_LUT_pcl_tmpk, i0, i1, wi, k0, k1, + wk); } [[nodiscard]] inline float parcel_virtual_temperature( From 61acbf96af24e63214965c74081308b5045fd842 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 16:25:05 -0500 Subject: [PATCH 32/55] explicitely added headers of importance --- include/SHARPlib/parcel.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 0ade89a8..c75cbf1d 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include namespace sharp { From 7e4fe0c7015d2468523cd1cb35283a8a009e78d6 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 25 Mar 2026 18:26:25 -0500 Subject: [PATCH 33/55] removed the need for a vapor pressure table entirely --- include/SHARPlib/parcel.h | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index c75cbf1d..c781d287 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -51,7 +51,6 @@ namespace sharp { */ struct lifter_wobus { static constexpr bool lift_from_lcl = true; - static constexpr bool tracks_moisture = false; /** * \brief The iterative convergence criteria (K) @@ -115,7 +114,6 @@ struct lifter_wobus { */ struct lifter_cm1 { static constexpr bool lift_from_lcl = false; - static constexpr bool tracks_moisture = true; /** * \brief The type of moist adiabat to use, as defined by sharp::adiabat @@ -213,7 +211,6 @@ struct tbl_data { std::vector m_logp_coord; std::vector m_thetae_coord; std::vector m_LUT_pcl_tmpk; - std::vector m_LUT_pcl_rv; tbl_data(Lft lifter) : m_lifter(std::move(lifter)) { if (m_lifter.ma_type == sharp::adiabat::adiab_liq || @@ -247,7 +244,6 @@ struct tbl_data { const std::size_t size_2D = num_logp * num_thetae; m_LUT_pcl_tmpk.resize(size_2D); - m_LUT_pcl_rv.resize(size_2D); for (std::size_t i = 0; i < num_thetae; ++i) { const float thetae_target = m_thetae_coord[i]; @@ -266,12 +262,6 @@ struct tbl_data { const std::size_t idx_2D = i * num_logp + k; m_LUT_pcl_tmpk[idx_2D] = tmpk_curr; - if constexpr (Lft::tracks_moisture) { - m_LUT_pcl_rv[idx_2D] = m_lifter.rv; - } else { - m_LUT_pcl_rv[idx_2D] = sharp::mixratio(pres, tmpk_curr); - } - pres_curr = pres; } } @@ -375,7 +365,6 @@ template struct lifter_tbl { const tbl_data& m_data; static constexpr bool lift_from_lcl = Lft::lift_from_lcl; - static constexpr bool tracks_moisture = Lft::tracks_moisture; bool m_use_lifter = false; Lft m_lifter; @@ -411,12 +400,6 @@ struct lifter_tbl { m_use_lifter = false; m_fi = m_data.find_thetae_index(lcl_tmpk, k0, k1, wk); - - if constexpr (Lft::tracks_moisture) { - m_rv = sharp::MISSING; - m_rl = sharp::MISSING; - m_ri = sharp::MISSING; - } } [[nodiscard]] inline float operator()(const float pres, const float tmpk, @@ -434,13 +417,6 @@ struct lifter_tbl { const float wi = std::clamp(m_fi - static_cast(i0), 0.0f, 1.0f); const auto [k0, k1, wk] = m_data.get_logp_indices(new_pres); - if constexpr (Lft::tracks_moisture) { - m_rv = m_data.bilinear_interp(m_data.m_LUT_pcl_rv, i0, i1, wi, k0, - k1, wk); - m_rl = 0.0f; - m_ri = 0.0f; - } - return m_data.bilinear_interp(m_data.m_LUT_pcl_tmpk, i0, i1, wi, k0, k1, wk); } @@ -449,10 +425,6 @@ struct lifter_tbl { const float pres, const float tmpk) const { if (m_use_lifter) { return m_lifter.parcel_virtual_temperature(pres, tmpk); - } - - if constexpr (Lft::tracks_moisture) { - return sharp::virtual_temperature(tmpk, m_rv, m_rl, m_ri); } else { return sharp::virtual_temperature(tmpk, sharp::mixratio(pres, tmpk)); From 61b69268508338053782eb8e2709bc78fc4470a0 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 11:19:06 -0500 Subject: [PATCH 34/55] renamed the table and lifter, performed a major cleanup and refactor pass, renamed several variables, improved const correctness and data sharing --- include/SHARPlib/params/convective.h | 16 +- include/SHARPlib/params/fire.h | 8 +- include/SHARPlib/parcel.h | 282 ++++++++++++++------------- include/SHARPlib/thermo.h | 18 +- src/SHARPlib/params/convective.cpp | 16 +- src/SHARPlib/params/fire.cpp | 8 +- src/SHARPlib/parcel.cpp | 32 +-- src/SHARPlib/thermo.cpp | 14 +- 8 files changed, 204 insertions(+), 190 deletions(-) diff --git a/include/SHARPlib/params/convective.h b/include/SHARPlib/params/convective.h index a8674f53..65f63e50 100644 --- a/include/SHARPlib/params/convective.h +++ b/include/SHARPlib/params/convective.h @@ -597,15 +597,15 @@ extern template PressureLayer effective_inflow_layer( const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, Parcel* mupcl); -extern template PressureLayer effective_inflow_layer>( - lifter_tbl& lifter, const float pressure[], +extern template PressureLayer effective_inflow_layer>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float dewpoint[], const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, Parcel* mupcl); -extern template PressureLayer effective_inflow_layer>( - lifter_tbl& lifter, const float pressure[], +extern template PressureLayer effective_inflow_layer>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float dewpoint[], const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, @@ -623,14 +623,14 @@ extern template float convective_temperature( float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); -extern template float convective_temperature>( - lifter_tbl& lifter, const float pressure[], +extern template float convective_temperature>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); -extern template float convective_temperature>( - lifter_tbl& lifter, const float pressure[], +extern template float convective_temperature>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); diff --git a/include/SHARPlib/params/fire.h b/include/SHARPlib/params/fire.h index 7a0895af..67a09c30 100644 --- a/include/SHARPlib/params/fire.h +++ b/include/SHARPlib/params/fire.h @@ -258,8 +258,8 @@ extern template float pyrocumulonimbus_firepower_threshold( float beta_incr); extern template float -pyrocumulonimbus_firepower_threshold>( - lifter_tbl& lifter, PressureLayer mix_layer, +pyrocumulonimbus_firepower_threshold>( + lifter_lut& lifter, PressureLayer mix_layer, const float pressure[], const float height[], const float temperature[], const float mixratio[], const float virtemp[], const float uwin[], const float vwin[], const float potential_temperature[], @@ -267,8 +267,8 @@ pyrocumulonimbus_firepower_threshold>( float phi, float beta_incr); extern template float -pyrocumulonimbus_firepower_threshold>( - lifter_tbl& lifter, PressureLayer mix_layer, +pyrocumulonimbus_firepower_threshold>( + lifter_lut& lifter, PressureLayer mix_layer, const float pressure[], const float height[], const float temperature[], const float mixratio[], const float virtemp[], const float uwin[], const float vwin[], const float potential_temperature[], diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index c781d287..5e27d04e 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -195,66 +196,150 @@ struct lifter_cm1 { }; template -struct tbl_data { +struct lut_data { Lft m_lifter; - float p_max = 110000.0f; - float p_min = 5000.0f; - float thetae_min = 210.0f; - float thetae_max = 430.0f; - float m_delta_thetae_inv = 0.0f; - float m_delta_logp_inv = 0.0f; + const float pres_min; + const float pres_max; + const float thetae_min; + const float thetae_max; - std::size_t num_logp = 201; - std::size_t num_thetae = 221; + const std::size_t num_logp; + const std::size_t num_thetae; + + const float m_logp_max; + const float m_delta_thetae_inv; + const float m_delta_logp_inv; - std::vector m_logp_coord; - std::vector m_thetae_coord; std::vector m_LUT_pcl_tmpk; - tbl_data(Lft lifter) : m_lifter(std::move(lifter)) { + explicit lut_data(Lft lifter, float pmin = 5000.0f, float pmax = 110000.0f, + float thte_min = 210.0f, float thte_max = 430.0f, + std::size_t n_logp = 201, std::size_t n_thetae = 221) + : m_lifter(std::move(lifter)), + pres_min(pmin), + pres_max(pmax), + thetae_min(thte_min), + thetae_max(thte_max), + num_logp(n_logp), + num_thetae(n_thetae), + m_logp_max(std::log(pmax)), + m_delta_thetae_inv(static_cast(n_thetae - 1) / + (thte_max - thte_min)), + m_delta_logp_inv(static_cast(n_logp - 1) / + (std::log(pmin) - std::log(pmax))) { if (m_lifter.ma_type == sharp::adiabat::adiab_liq || m_lifter.ma_type == sharp::adiabat::adiab_ice) { throw std::invalid_argument( - "CRITICAL ERROR: sharp::tbl_data does not support reversible " + "CRITICAL ERROR: sharp::lut_data does not support reversible " "adiabatic ascent (adiab_liq or adiab_ice). The 2D lookup " "table cannot track conserved total water mass. Please " "configure the lifter to use pseudo_liq or pseudo_ice " "instead."); } + generate_table(); + } + + [[nodiscard]] inline std::tuple + get_logp_indices(const float pres) const { + const float target_logp = std::log(pres); + + const float frac_k = + std::max((target_logp - m_logp_max) * m_delta_logp_inv, 0.0f); + const std::size_t k0 = + std::min(static_cast(frac_k), num_logp - 2); + + const std::size_t k1 = k0 + 1; + const float weight_k = + std::clamp(frac_k - static_cast(k0), 0.0f, 1.0f); + + return {k0, k1, weight_k}; } + [[nodiscard]] inline float find_thetae_index(const float lcl_tmpk, + const std::size_t k0, + const std::size_t k1, + const float weight_k) const { + float frac_i_lo = 0.0f; + float frac_i_hi = static_cast(num_thetae - 1); + float frac_i_mid = 0.0f; + + constexpr int max_iter = 20; + constexpr float tol = 0.005f; // Kelvin + const float* lut = m_LUT_pcl_tmpk.data(); + for (int step = 0; step < max_iter; ++step) { + frac_i_mid = 0.5f * (frac_i_lo + frac_i_hi); + + const std::size_t i0 = + std::min(static_cast(frac_i_mid), num_thetae - 2); + const std::size_t i1 = i0 + 1; + const float weight_i = + std::clamp(frac_i_mid - static_cast(i0), 0.0f, 1.0f); + + const std::size_t idx1 = i0 * num_logp + k0; + const std::size_t idx2 = i0 * num_logp + k1; + const std::size_t idx3 = i1 * num_logp + k0; + const std::size_t idx4 = i1 * num_logp + k1; + + const float t0 = sharp::lerp(lut[idx1], lut[idx2], weight_k); + const float t1 = sharp::lerp(lut[idx3], lut[idx4], weight_k); + const float t_interp = sharp::lerp(t0, t1, weight_i); + + const float residual = t_interp - lcl_tmpk; + if (std::abs(residual) < tol) break; + + if (residual > 0.0f) { + frac_i_hi = frac_i_mid; + } else { + frac_i_lo = frac_i_mid; + } + } + return frac_i_mid; + } + + [[nodiscard]] inline float bilinear_interp( + const std::size_t i0, const std::size_t i1, const float wi, + const std::size_t k0, const std::size_t k1, const float wk) const { + const float* lut = m_LUT_pcl_tmpk.data(); + const float val00 = lut[i0 * num_logp + k0]; + const float val01 = lut[i0 * num_logp + k1]; + const float val10 = lut[i1 * num_logp + k0]; + const float val11 = lut[i1 * num_logp + k1]; + + const float val0 = sharp::lerp(val00, val01, wk); + const float val1 = sharp::lerp(val10, val11, wk); + return sharp::lerp(val0, val1, wi); + } + + private: inline void generate_table() { - m_logp_coord.resize(num_logp); - m_thetae_coord.resize(num_thetae); + std::vector logp_coord(num_logp); + std::vector thetae_coord(num_thetae); const float delta_thetae = (thetae_max - thetae_min) / (num_thetae - 1); - m_delta_thetae_inv = 1.0f / delta_thetae; for (std::size_t i = 0; i < num_thetae; ++i) { - m_thetae_coord[i] = thetae_min + i * delta_thetae; + thetae_coord[i] = thetae_min + i * delta_thetae; } - const float logp_max = std::log(p_max); - const float logp_min = std::log(p_min); - const float delta_logp = (logp_min - logp_max) / (num_logp - 1); - m_delta_logp_inv = 1.0f / delta_logp; + const float logp_min = std::log(pres_min); + const float delta_logp = (logp_min - m_logp_max) / (num_logp - 1); for (std::size_t i = 0; i < num_logp; ++i) { - m_logp_coord[i] = logp_max + i * delta_logp; + logp_coord[i] = m_logp_max + i * delta_logp; } const std::size_t size_2D = num_logp * num_thetae; m_LUT_pcl_tmpk.resize(size_2D); for (std::size_t i = 0; i < num_thetae; ++i) { - const float thetae_target = m_thetae_coord[i]; - const float tmpk = solve_tmpk_for_thetae(p_max, thetae_target); + const float thetae_target = thetae_coord[i]; + const float tmpk = solve_tmpk_for_thetae(pres_max, thetae_target); - m_lifter.setup(p_max, tmpk); - float pres_curr = p_max; + m_lifter.setup(pres_max, tmpk); + float pres_curr = pres_max; float tmpk_curr = tmpk; for (std::size_t k = 0; k < num_logp; ++k) { - const float pres = std::exp(m_logp_coord[k]); + const float pres = std::exp(logp_coord[k]); if (k > 0) { tmpk_curr = m_lifter(pres_curr, tmpk_curr, pres); } @@ -271,7 +356,7 @@ struct tbl_data { float thetae_target) const { float tmpk_lo = 200.0f; float tmpk_hi = 330.0f; - float tmpk_mid = sharp::ZEROCNK; + float tmpk_mid = ZEROCNK; const float tol = 0.001f; const std::size_t max_iters = 50; @@ -294,112 +379,41 @@ struct tbl_data { fmt::println("Failed to converge on a LCL temperature for thetae."); return tmpk_mid; } - - [[nodiscard]] inline std::tuple - get_logp_indices(const float pres) const { - const float target_logp = std::log(pres); - const float logp_max = std::log(p_max); - - const float fk = - std::max((target_logp - logp_max) * m_delta_logp_inv, 0.0f); - const std::size_t k0 = - std::min(static_cast(fk), num_logp - 2); - - const std::size_t k1 = k0 + 1; - const float wk = std::clamp(fk - static_cast(k0), 0.0f, 1.0f); - - return {k0, k1, wk}; - } - - [[nodiscard]] inline float find_thetae_index(const float lcl_tmpk, - const std::size_t k0, - const std::size_t k1, - const float wk) const { - float fi_lo = 0.0f; - float fi_hi = static_cast(num_thetae - 1); - float fi_mid = 0.0f; - - for (int step = 0; step < 20; ++step) { - fi_mid = 0.5f * (fi_lo + fi_hi); - - const std::size_t i0 = - std::min(static_cast(fi_mid), num_thetae - 2); - - const std::size_t i1 = i0 + 1; - const float wi = - std::clamp(fi_mid - static_cast(i0), 0.0f, 1.0f); - - const float t0 = - sharp::lerp(m_LUT_pcl_tmpk[i0 * num_logp + k0], - m_LUT_pcl_tmpk[i0 * num_logp + k1], wk); - const float t1 = - sharp::lerp(m_LUT_pcl_tmpk[i1 * num_logp + k0], - m_LUT_pcl_tmpk[i1 * num_logp + k1], wk); - const float t_interp = sharp::lerp(t0, t1, wi); - - if (t_interp < lcl_tmpk) { - fi_lo = fi_mid; - } else { - fi_hi = fi_mid; - } - } - return fi_mid; - } - - [[nodiscard]] inline float bilinear_interp( - const std::vector& lut, const std::size_t i0, - const std::size_t i1, const float wi, const std::size_t k0, - const std::size_t k1, const float wk) const { - const float val00 = lut[i0 * num_logp + k0]; - const float val01 = lut[i0 * num_logp + k1]; - const float val10 = lut[i1 * num_logp + k0]; - const float val11 = lut[i1 * num_logp + k1]; - - const float val0 = sharp::lerp(val00, val01, wk); - const float val1 = sharp::lerp(val10, val11, wk); - return sharp::lerp(val0, val1, wi); - } }; template -struct lifter_tbl { - const tbl_data& m_data; +struct lifter_lut { + std::shared_ptr> m_data; static constexpr bool lift_from_lcl = Lft::lift_from_lcl; bool m_use_lifter = false; - Lft m_lifter; - float m_fi = 0.0f; - float m_rv = sharp::MISSING; - float m_rl = sharp::MISSING; - float m_ri = sharp::MISSING; + Lft m_lifter; - lifter_tbl(const tbl_data& data) - : m_data(data), m_lifter(data.m_lifter) {} + explicit lifter_lut(std::shared_ptr> data) + : m_data(std::move(data)), m_lifter(m_data->m_lifter) {} inline void setup(const float lcl_pres, const float lcl_tmpk) { - const auto [k0, k1, wk] = m_data.get_logp_indices(lcl_pres); - - const std::size_t i_max = m_data.num_thetae - 1; + const auto [k0, k1, wk] = m_data->get_logp_indices(lcl_pres); + const std::size_t i_max = m_data->num_thetae - 1; const float t_min_bound = - sharp::lerp(m_data.m_LUT_pcl_tmpk[0 * m_data.num_logp + k0], - m_data.m_LUT_pcl_tmpk[0 * m_data.num_logp + k1], wk); + sharp::lerp(m_data->m_LUT_pcl_tmpk[0 * m_data->num_logp + k0], + m_data->m_LUT_pcl_tmpk[0 * m_data->num_logp + k1], wk); const float t_max_bound = sharp::lerp( - m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k0], - m_data.m_LUT_pcl_tmpk[i_max * m_data.num_logp + k1], wk); + m_data->m_LUT_pcl_tmpk[i_max * m_data->num_logp + k0], + m_data->m_LUT_pcl_tmpk[i_max * m_data->num_logp + k1], wk); if (lcl_tmpk < t_min_bound || lcl_tmpk > t_max_bound || - lcl_pres < m_data.p_min || lcl_pres > m_data.p_max) { + lcl_pres < m_data->pres_min || lcl_pres > m_data->pres_max) { m_use_lifter = true; m_lifter.setup(lcl_pres, lcl_tmpk); return; } m_use_lifter = false; - - m_fi = m_data.find_thetae_index(lcl_tmpk, k0, k1, wk); + m_fi = m_data->find_thetae_index(lcl_tmpk, k0, k1, wk); } [[nodiscard]] inline float operator()(const float pres, const float tmpk, @@ -408,17 +422,17 @@ struct lifter_tbl { return m_lifter(pres, tmpk, new_pres); } - if ((new_pres < m_data.p_min) || (new_pres > m_data.p_max)) + if ((new_pres < m_data->pres_min) || (new_pres > m_data->pres_max)) return MISSING; const std::size_t i0 = - std::min(static_cast(m_fi), m_data.num_thetae - 2); + std::min(static_cast(m_fi), m_data->num_thetae - 2); const std::size_t i1 = i0 + 1; - const float wi = std::clamp(m_fi - static_cast(i0), 0.0f, 1.0f); - const auto [k0, k1, wk] = m_data.get_logp_indices(new_pres); + const float weight_i = + std::clamp(m_fi - static_cast(i0), 0.0f, 1.0f); + const auto [k0, k1, weight_k] = m_data->get_logp_indices(new_pres); - return m_data.bilinear_interp(m_data.m_LUT_pcl_tmpk, i0, i1, wi, k0, k1, - wk); + return m_data->bilinear_interp(i0, i1, weight_i, k0, k1, weight_k); } [[nodiscard]] inline float parcel_virtual_temperature( @@ -1040,29 +1054,29 @@ extern template Parcel Parcel::most_unstable_parcel( const std::ptrdiff_t N); extern template Parcel -Parcel::most_unstable_parcel>( - PressureLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); extern template Parcel -Parcel::most_unstable_parcel>( - HeightLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); extern template Parcel -Parcel::most_unstable_parcel>( - PressureLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); extern template Parcel -Parcel::most_unstable_parcel>( - HeightLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); @@ -1086,12 +1100,12 @@ extern template void Parcel::lift_parcel(lifter_cm1& liftpcl, float pcl_vtmpk_arr[], const std::ptrdiff_t N); -extern template void Parcel::lift_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +extern template void Parcel::lift_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_vtmpk_arr[], const std::ptrdiff_t N); -extern template void Parcel::lift_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +extern template void Parcel::lift_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_vtmpk_arr[], const std::ptrdiff_t N); extern template void DowndraftParcel::lower_parcel( @@ -1102,12 +1116,12 @@ extern template void DowndraftParcel::lower_parcel( lifter_cm1& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); -extern template void DowndraftParcel::lower_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +extern template void DowndraftParcel::lower_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); -extern template void DowndraftParcel::lower_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +extern template void DowndraftParcel::lower_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); /// @endcond diff --git a/include/SHARPlib/thermo.h b/include/SHARPlib/thermo.h index c03955a7..ea0dad57 100644 --- a/include/SHARPlib/thermo.h +++ b/include/SHARPlib/thermo.h @@ -727,7 +727,7 @@ struct lifter_wobus; struct lifter_cm1; template -struct lifter_tbl; +struct lifter_lut; extern template float wetbulb(lifter_wobus& lifter, float pressure, float temperature, @@ -736,12 +736,12 @@ extern template float wetbulb(lifter_wobus& lifter, extern template float wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); -extern template float wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +extern template float wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); -extern template float wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +extern template float wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); extern template float theta_wetbulb(lifter_cm1& lifter, @@ -754,12 +754,12 @@ extern template float theta_wetbulb(lifter_wobus& lifter, float temperature, float dewpoint); -extern template float theta_wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +extern template float theta_wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); -extern template float theta_wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +extern template float theta_wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); /// @endcond diff --git a/src/SHARPlib/params/convective.cpp b/src/SHARPlib/params/convective.cpp index b77e4886..33208f11 100644 --- a/src/SHARPlib/params/convective.cpp +++ b/src/SHARPlib/params/convective.cpp @@ -468,15 +468,15 @@ template PressureLayer effective_inflow_layer( const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, Parcel* mupcl); -template PressureLayer effective_inflow_layer>( - lifter_tbl& lifter, const float pressure[], +template PressureLayer effective_inflow_layer>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float dewpoint[], const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, Parcel* mupcl); -template PressureLayer effective_inflow_layer>( - lifter_tbl& lifter, const float pressure[], +template PressureLayer effective_inflow_layer>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float dewpoint[], const float virtemp_arr[], float pcl_vtmpk_arr[], float buoy_arr[], const std::ptrdiff_t N, const float cape_thresh, const float cinh_thresh, @@ -494,14 +494,14 @@ template float convective_temperature( float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); -template float convective_temperature>( - lifter_tbl& lifter, const float pressure[], +template float convective_temperature>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); -template float convective_temperature>( - lifter_tbl& lifter, const float pressure[], +template float convective_temperature>( + lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float mixratio[], float pcl_virtemp[], float pcl_buoyancy[], const std::ptrdiff_t N, float cinh_thresh); diff --git a/src/SHARPlib/params/fire.cpp b/src/SHARPlib/params/fire.cpp index 5ed75abe..83f94363 100644 --- a/src/SHARPlib/params/fire.cpp +++ b/src/SHARPlib/params/fire.cpp @@ -114,16 +114,16 @@ template float pyrocumulonimbus_firepower_threshold( float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, float phi, float beta_incr); -template float pyrocumulonimbus_firepower_threshold>( - lifter_tbl& lifter, PressureLayer mix_layer, +template float pyrocumulonimbus_firepower_threshold>( + lifter_lut& lifter, PressureLayer mix_layer, const float pressure[], const float height[], const float temperature[], const float mixratio[], const float virtemp[], const float uwin[], const float vwin[], const float potential_temperature[], float pcl_vtmpk_arr[], float pcl_buoy_arr[], std::ptrdiff_t N, Parcel* pcl, float phi, float beta_incr); -template float pyrocumulonimbus_firepower_threshold>( - lifter_tbl& lifter, PressureLayer mix_layer, +template float pyrocumulonimbus_firepower_threshold>( + lifter_lut& lifter, PressureLayer mix_layer, const float pressure[], const float height[], const float temperature[], const float mixratio[], const float virtemp[], const float uwin[], const float vwin[], const float potential_temperature[], diff --git a/src/SHARPlib/parcel.cpp b/src/SHARPlib/parcel.cpp index 5b3e852d..946d1fe1 100644 --- a/src/SHARPlib/parcel.cpp +++ b/src/SHARPlib/parcel.cpp @@ -272,29 +272,29 @@ template Parcel Parcel::most_unstable_parcel( const std::ptrdiff_t N); template Parcel -Parcel::most_unstable_parcel>( - PressureLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); template Parcel -Parcel::most_unstable_parcel>( - HeightLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); template Parcel -Parcel::most_unstable_parcel>( - PressureLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + PressureLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); template Parcel -Parcel::most_unstable_parcel>( - HeightLayer& search_layer, lifter_tbl& lifter, +Parcel::most_unstable_parcel>( + HeightLayer& search_layer, lifter_lut& lifter, const float pressure[], const float height[], const float temperature[], const float virtemp[], const float dewpoint[], float pcl_virtemp[], float buoy_arr[], const std::ptrdiff_t N); @@ -319,12 +319,12 @@ template void Parcel::lift_parcel(lifter_cm1& liftpcl, float pcl_vtmpk_arr[], const std::ptrdiff_t N); -template void Parcel::lift_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +template void Parcel::lift_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_vtmpk_arr[], const std::ptrdiff_t N); -template void Parcel::lift_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +template void Parcel::lift_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_vtmpk_arr[], const std::ptrdiff_t N); template void DowndraftParcel::lower_parcel( @@ -335,12 +335,12 @@ template void DowndraftParcel::lower_parcel( lifter_cm1& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); -template void DowndraftParcel::lower_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +template void DowndraftParcel::lower_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); -template void DowndraftParcel::lower_parcel>( - lifter_tbl& liftpcl, const float pressure_arr[], +template void DowndraftParcel::lower_parcel>( + lifter_lut& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); /// @endcond diff --git a/src/SHARPlib/thermo.cpp b/src/SHARPlib/thermo.cpp index db04851b..0e630964 100644 --- a/src/SHARPlib/thermo.cpp +++ b/src/SHARPlib/thermo.cpp @@ -648,11 +648,11 @@ template float wetbulb(lifter_wobus& lifter, float pressure, template float wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); -template float wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +template float wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); -template float wetbulb>(lifter_tbl& lifter, +template float wetbulb>(lifter_lut& lifter, float pressure, float temperature, float dewpoint); @@ -663,12 +663,12 @@ template float theta_wetbulb(lifter_wobus& lifter, float pressure, template float theta_wetbulb(lifter_cm1& lifter, float pressure, float temperature, float dewpoint); -template float theta_wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +template float theta_wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); -template float theta_wetbulb>( - lifter_tbl& lifter, float pressure, float temperature, +template float theta_wetbulb>( + lifter_lut& lifter, float pressure, float temperature, float dewpoint); /// @endcond From 5f4ae1b26e1bba4a1cce88e39ea00afd17fa6427 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 13:15:52 -0500 Subject: [PATCH 35/55] Added the documentation to the new parcel lifter and LUT struct --- include/SHARPlib/parcel.h | 201 +++++++++++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 1 deletion(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 5e27d04e..6b94964a 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -52,6 +52,7 @@ namespace sharp { */ struct lifter_wobus { static constexpr bool lift_from_lcl = true; + static constexpr adiabat ma_type = adiabat::pseudo_liq; /** * \brief The iterative convergence criteria (K) @@ -195,24 +196,97 @@ struct lifter_cm1 { } }; +/** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Pseudoadiabatic ascent lookup table (LUT) + * + * This data structure contains everything necessary to construct, + * store, and interrogate a pseudoadiabatic ascent lookup table (LUT). + * The LUT is generated using the passed lifter (e.g. sharp::lifter_cm1). + * Due to the complex nature of reversible adiabatic ascent, it is + * not supported in this lookup table. Any sharp::adiabat::adiab_liq + * or sharp::adiabat::adiab_ice configured lifters will throw an error. + * + * This is intended to be used by sharp::lifter_lut for actual parcel + * lifting operations. While this data structure can be accessed by + * multiple threads, each thread must have its own sharp::lifter_lut. + */ template struct lut_data { + /** + * \brief The moist adiabatic ascent solver to use + */ Lft m_lifter; + /** + * \brief The minimum pressure of the lookup table + */ const float pres_min; + + /** + * \brief The maximum pressure of the lookup table + */ const float pres_max; + + /** + * \brief The minimum thetae of the lookup table + */ const float thetae_min; + + /** + * \brief The maximum thetae of the lookup table + */ const float thetae_max; + /** + * \brief The number of pressure levels (in log space) for the LUT + */ const std::size_t num_logp; + + /** + * \brief The number of thetae levels for the LUT + */ const std::size_t num_thetae; + /** + * \brief Storage for the maximum logp + */ const float m_logp_max; + + /** + * \brief Storage for inverse delta theta + */ const float m_delta_thetae_inv; + + /** + * \brief Storage for inverse delta logarithmic pressure + */ const float m_delta_logp_inv; + /** + * \brief The parcel temperature LUT + */ std::vector m_LUT_pcl_tmpk; + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Create a pseudoadiabatic lookup table (LUT) + * + * Construct the pseudoadiabatic lookup table (LUT) using + * the provided lifter and LUT configuration options. This + * lookup data is safe to share with many threads, but each + * thread must use its own sharp::lifter_lut. + * + * \param lifter Parcel lifting function/functor + * \param pmin Minimum Pressure (Pa) + * \param pmax Maximum pressure (Pa) + * \param thte_min Minimum theta-e (K) + * \param thte_max Maximum theta-e (K) + * \param n_logp Number of logarithmic pressure levels + * \param n_thetae Number of theta-e levels + */ explicit lut_data(Lft lifter, float pmin = 5000.0f, float pmax = 110000.0f, float thte_min = 210.0f, float thte_max = 430.0f, std::size_t n_logp = 201, std::size_t n_thetae = 221) @@ -240,6 +314,19 @@ struct lut_data { generate_table(); } + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Get the bounding vertical indices for a given pressure + * + * Returns the bounding vertical indices for the LUT given a + * pressure value, along with the interpolation weight. + * + * \param pres Pa + * + * \return tuple of (k0, k1, weight_k): bottom index, top index, + * and interpolation weight + */ [[nodiscard]] inline std::tuple get_logp_indices(const float pres) const { const float target_logp = std::log(pres); @@ -256,6 +343,23 @@ struct lut_data { return {k0, k1, weight_k}; } + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Get the fractional thetae coordinate index + * + * Returns the fractional index for the thetae coordinate + * of the LUT given the lcl temperature and the bounding + * vertical coordinate indices, along with the vertical + * interpolation weight. + * + * \param lcl_tmpk lifted condensation level temperature (K) + * \param k0 The bottom bounding index of pressure + * \param k1 The top bounding index of pressure + * \param weight_k The vertical interpolation weight + * + * \return fractional index of thetae axis for LUT + */ [[nodiscard]] inline float find_thetae_index(const float lcl_tmpk, const std::size_t k0, const std::size_t k1, @@ -297,6 +401,23 @@ struct lut_data { return frac_i_mid; } + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Perform bilinear interpolation of LUT + * + * Performs the bilinear interpolation of the LUT to + * return a parcel temperature along a pseudoadiabat. + * + * \param i0 thetae left bounding index + * \param i1 thetae right bounding index + * \param wi thetae interpolation weight + * \param k0 pressure bottom bounding index + * \param k1 pressure top bounding index + * \param wk pressure interpolation weight + * + * \return parcel temperature (K) + */ [[nodiscard]] inline float bilinear_interp( const std::size_t i0, const std::size_t i1, const float wi, const std::size_t k0, const std::size_t k1, const float wk) const { @@ -312,6 +433,14 @@ struct lut_data { } private: + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Constructs pseudoadiabatic LUT + * + * Called by the constructor, this creates and fills + * the pseudoadiabatic parcel ascent LUT. + */ inline void generate_table() { std::vector logp_coord(num_logp); std::vector thetae_coord(num_thetae); @@ -352,6 +481,20 @@ struct lut_data { } } + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Finds and returns a temperature for a given pressure and thetae. + * + * Iteratively inverts theta-e to find the saturated temperature at a + * single pressure level. Used for finding the starting parcel + * temperature during LUT construction. + * + * \param pressure (Pa) + * \param thetae_target (K) + * + * \return lcl_temperature (K) + */ [[nodiscard]] float solve_tmpk_for_thetae(float pressure, float thetae_target) const { float tmpk_lo = 200.0f; @@ -381,18 +524,58 @@ struct lut_data { } }; +/** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Parcel lifter functor for LUT based calculations. + * + * This functor is used to wrap sharp::lut_data, which creates + * a pseudoadiabatic ascent lookup table based on the type of + * parcel lifter used to construct the table. Instead of + * directly solving the moist ascent ODEs, this lifter uses + * the LUT to perform bilinear interpolation to get the + * parcel temperature. + * + * LUT based parcel ascent only works for pseudoadiabats. + * Constructing the LUT with a reversible adiabat type will + * result in an error being thrown. + */ template struct lifter_lut { - std::shared_ptr> m_data; static constexpr bool lift_from_lcl = Lft::lift_from_lcl; + /** + * \brief Shared pointer to a previously created LUT + */ + std::shared_ptr> m_data; + + /** + * Used to signal a fallback to the direct solver + */ bool m_use_lifter = false; + + /** + * Fractional index used to determine the pseudoadiabat + */ float m_fi = 0.0f; + + /** + * An instantiation of a parcel lifter for fallback computation + */ Lft m_lifter; explicit lifter_lut(std::shared_ptr> data) : m_data(std::move(data)), m_lifter(m_data->m_lifter) {} + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Performs a setup step based on the LCL attributes + * + * Computes the fractional index needed to select the + * correct pseudoadiabat for lookup. If the LCL is outside + * the table bounds, it falls back to the direct solver. + */ inline void setup(const float lcl_pres, const float lcl_tmpk) { const auto [k0, k1, wk] = m_data->get_logp_indices(lcl_pres); const std::size_t i_max = m_data->num_thetae - 1; @@ -416,6 +599,15 @@ struct lifter_lut { m_fi = m_data->find_thetae_index(lcl_tmpk, k0, k1, wk); } + /** + * \brief Overloads operator() to perform LUT interpolation + * + * \param pres Parcel pressure (Pa) + * \param tmpk Parcel temperature (K) + * \param new_pres Final level of parcel after lift (Pa) + * + * \return The temperature of the lifted parcel + */ [[nodiscard]] inline float operator()(const float pres, const float tmpk, const float new_pres) { if (m_use_lifter) { @@ -435,6 +627,13 @@ struct lifter_lut { return m_data->bilinear_interp(i0, i1, weight_i, k0, k1, weight_k); } + /** + * \brief Computes the virtual temperature of the parcel + * \param pres Parcel pressure (Pa) + * \param tmpk Parcel temperature (K) + * + * \return The virtual temperature of the parcel + */ [[nodiscard]] inline float parcel_virtual_temperature( const float pres, const float tmpk) const { if (m_use_lifter) { From 08a15aefc55e535ebee49f66eaa53a5d29ae40ca Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 14:24:16 -0500 Subject: [PATCH 36/55] added python bindings for lifter_lut --- src/nanobind/params_bindings.h | 434 ++++++++++++++++++++++++ src/nanobind/parcel_bindings.h | 583 ++++++++++++++++++++++++++++++++- src/nanobind/thermo_bindings.h | 364 ++++++++++++++++++++ 3 files changed, 1376 insertions(+), 5 deletions(-) diff --git a/src/nanobind/params_bindings.h b/src/nanobind/params_bindings.h index 8c4576cd..e09b34f4 100644 --- a/src/nanobind/params_bindings.h +++ b/src/nanobind/params_bindings.h @@ -163,6 +163,152 @@ cinh_thresh : float, default = -250.0 The CINH threshold used to compute the Effective Inflow Layer mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional +Returns +------- +nwsspc.sharp.calc.layer.PressureLayer + The Effective Inflow Layer + )pbdoc"); + + m_params.def( + "effective_inflow_layer", + [](sharp::lifter_lut& lifter, + const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t dewpoint, + const_prof_arr_t virtemp, float cape_thresh, float cinh_thresh, + sharp::Parcel* mupcl) { + if ((pressure.shape(0) != height.shape(0)) || + (pressure.shape(0) != temperature.shape(0)) || + (pressure.shape(0) != dewpoint.shape(0)) || + (pressure.shape(0) != virtemp.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + float* pcl_vtmp = new float[height.size()]; + float* pcl_buoy = new float[height.size()]; + + sharp::PressureLayer eil = sharp::effective_inflow_layer( + lifter, pressure.data(), height.data(), temperature.data(), + dewpoint.data(), virtemp.data(), pcl_vtmp, pcl_buoy, + height.size(), cape_thresh, cinh_thresh, mupcl); + + delete[] pcl_vtmp; + delete[] pcl_buoy; + + return eil; + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), + nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("virtemp"), + nb::arg("cape_thresh") = 100.0f, nb::arg("cinh_thresh") = -250.0f, + nb::arg("mupcl") = nb::none(), + R"pbdoc( +Computes the Effective Inflow Layer, or the layer of the atmosphere +beliefed to be the primary source of inflow for supercell thunderstorms. +The Effective Inflow Layer, and its use in computing shear and storm +relative helicity, is described by Thompson et al. 2007: +https://www.spc.noaa.gov/publications/thompson/effective.pdf + +Standard/default values for cape_thresh and cinh_thresh have been +experimentally determined to be cape_thresh = 100 J/kg and +cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the +'mupcl' kwarg, the Most Unstable parcel found during the EIL search will +be returned. + +References +---------- +Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut +pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) +temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + A 1D NumPy array of dewpoint values (K) +virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) +cape_thresh : float, default = 100.0 + The CAPE threshold used to compute the Effective Inflow Layer +cinh_thresh : float, default = -250.0 + The CINH threshold used to compute the Effective Inflow Layer +mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + +Returns +------- +nwsspc.sharp.calc.layer.PressureLayer + The Effective Inflow Layer + )pbdoc"); + + m_params.def( + "effective_inflow_layer", + [](sharp::lifter_lut& lifter, + const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t dewpoint, + const_prof_arr_t virtemp, float cape_thresh, float cinh_thresh, + sharp::Parcel* mupcl) { + if ((pressure.shape(0) != height.shape(0)) || + (pressure.shape(0) != temperature.shape(0)) || + (pressure.shape(0) != dewpoint.shape(0)) || + (pressure.shape(0) != virtemp.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + float* pcl_vtmp = new float[height.size()]; + float* pcl_buoy = new float[height.size()]; + + sharp::PressureLayer eil = sharp::effective_inflow_layer( + lifter, pressure.data(), height.data(), temperature.data(), + dewpoint.data(), virtemp.data(), pcl_vtmp, pcl_buoy, + height.size(), cape_thresh, cinh_thresh, mupcl); + + delete[] pcl_vtmp; + delete[] pcl_buoy; + + return eil; + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), + nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("virtemp"), + nb::arg("cape_thresh") = 100.0f, nb::arg("cinh_thresh") = -250.0f, + nb::arg("mupcl") = nb::none(), + R"pbdoc( +Computes the Effective Inflow Layer, or the layer of the atmosphere +beliefed to be the primary source of inflow for supercell thunderstorms. +The Effective Inflow Layer, and its use in computing shear and storm +relative helicity, is described by Thompson et al. 2007: +https://www.spc.noaa.gov/publications/thompson/effective.pdf + +Standard/default values for cape_thresh and cinh_thresh have been +experimentally determined to be cape_thresh = 100 J/kg and +cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the +'mupcl' kwarg, the Most Unstable parcel found during the EIL search will +be returned. + +References +---------- +Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut +pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) +temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + A 1D NumPy array of dewpoint values (K) +virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) +cape_thresh : float, default = 100.0 + The CAPE threshold used to compute the Effective Inflow Layer +cinh_thresh : float, default = -250.0 + The CINH threshold used to compute the Effective Inflow Layer +mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + Returns ------- nwsspc.sharp.calc.layer.PressureLayer @@ -747,6 +893,116 @@ mixratio : numpy.ndarray[dtype=float32] cinh_thresh : float, default = -1.0 The CINH threshold used to compute the convective temperature (J/kg) +Returns +------- +float + The convective temperature (K) +)pbdoc"); + + m_params.def( + "convective_temperature", + [](sharp::lifter_lut& lifter, + const_prof_arr_t pres, const_prof_arr_t hght, const_prof_arr_t tmpk, + const_prof_arr_t vtmpk, const_prof_arr_t mixr, float cinh_thresh) { + if ((pres.shape(0) != hght.shape(0)) || + (pres.shape(0) != tmpk.shape(0)) || + (pres.shape(0) != mixr.shape(0)) || + (pres.shape(0) != vtmpk.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + float* pcl_vtmp = new float[hght.size()]; + float* pcl_buoy = new float[hght.size()]; + + float cnvtv_tmpk = sharp::convective_temperature( + lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), + mixr.data(), pcl_vtmp, pcl_buoy, pres.size()); + + delete[] pcl_vtmp; + delete[] pcl_buoy; + + return cnvtv_tmpk; + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), + nb::arg("temperature"), nb::arg("virtual_temperature"), + nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, + R"pbdoc( +Computes the convective temperature by iteratively lifting parcels from +the surface using a lowest 100 hPa mean mixing ratio and increasing +surface temperatures to find the first parcel that reaches the CINH +threshold. The first guess is the current surface temperature. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut +pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) +temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) +virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) +mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) +cinh_thresh : float, default = -1.0 + The CINH threshold used to compute the convective temperature (J/kg) + +Returns +------- +float + The convective temperature (K) +)pbdoc"); + + m_params.def( + "convective_temperature", + [](sharp::lifter_lut& lifter, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t vtmpk, + const_prof_arr_t mixr, float cinh_thresh) { + if ((pres.shape(0) != hght.shape(0)) || + (pres.shape(0) != tmpk.shape(0)) || + (pres.shape(0) != mixr.shape(0)) || + (pres.shape(0) != vtmpk.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + float* pcl_vtmp = new float[hght.size()]; + float* pcl_buoy = new float[hght.size()]; + + float cnvtv_tmpk = sharp::convective_temperature( + lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), + mixr.data(), pcl_vtmp, pcl_buoy, pres.size()); + + delete[] pcl_vtmp; + delete[] pcl_buoy; + + return cnvtv_tmpk; + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), + nb::arg("temperature"), nb::arg("virtual_temperature"), + nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, + R"pbdoc( +Computes the convective temperature by iteratively lifting parcels from +the surface using a lowest 100 hPa mean mixing ratio and increasing +surface temperatures to find the first parcel that reaches the CINH +threshold. The first guess is the current surface temperature. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut +pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) +temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) +virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) +mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) +cinh_thresh : float, default = -1.0 + The CINH threshold used to compute the convective temperature (J/kg) + Returns ------- float @@ -1171,6 +1427,184 @@ Returns float The PyroCB Firepower Threshold (Watts) +)pbdoc"); + + m_params.def( + "pyrocumulonimbus_firepower_threshold", + [](sharp::lifter_lut& lifter, + sharp::PressureLayer& mix_layer, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t mixr, + const_prof_arr_t vtmpk, const_prof_arr_t uwin, const_prof_arr_t vwin, + const_prof_arr_t theta, sharp::Parcel* pcl, float phi, + float beta_incr) { + if ((pres.shape(0) != hght.shape(0)) || + (pres.shape(0) != tmpk.shape(0)) || + (pres.shape(0) != mixr.shape(0)) || + (pres.shape(0) != vtmpk.shape(0)) || + (pres.shape(0) != uwin.shape(0)) || + (pres.shape(0) != vwin.shape(0)) || + (pres.shape(0) != theta.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + float* pcl_vtmp = new float[hght.size()]; + float* pcl_buoy = new float[hght.size()]; + + float pft = sharp::pyrocumulonimbus_firepower_threshold( + lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), + mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), + theta.data(), pcl_vtmp, pcl_buoy, pres.size(), pcl, phi, + beta_incr); + + delete[] pcl_vtmp; + delete[] pcl_buoy; + + return pft; + }, + nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), + nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), + nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), + nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, + R"pbdoc( +Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum +amount of firepower required to generate pyrocumulonimbus clouds for a +given atmospheric profile. Requires a PressureLayer to define a mixing +layer used to average values of potential temperature, mixing ratio, +and wind speed. The beta increment determines how to vary the plume +buoyancy factor, with smaller values resulting in more iteration steps. +Phi is the fire moisture to potential temperature increment ratio. + +Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. +If a parcel is passed, the values will be set with the PFT fire parcel. + +References +---------- +Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml +Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut +mix_layer : nwsspc.sharp.calc.layer.PressureLayer + A mixing layer to take average values of the environment from +pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) +temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) +mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) +virtual_temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) +u_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of U wind component values (m/s) +v_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of V wind compnent values (m/s) +potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of potential temperature values (K) +pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + If a parcel is provided, returns the parcel for the PFT +phi : float + The fire moisture to potential temperature increment ratio (kg/(kg*K)) +beta_incr : float + The fire plume buoyancy factor (unitless) + +Returns +------- +float + The PyroCB Firepower Threshold (Watts) + +)pbdoc"); + + m_params.def( + "pyrocumulonimbus_firepower_threshold", + [](sharp::lifter_lut& lifter, + sharp::PressureLayer& mix_layer, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t mixr, + const_prof_arr_t vtmpk, const_prof_arr_t uwin, const_prof_arr_t vwin, + const_prof_arr_t theta, sharp::Parcel* pcl, float phi, + float beta_incr) { + if ((pres.shape(0) != hght.shape(0)) || + (pres.shape(0) != tmpk.shape(0)) || + (pres.shape(0) != mixr.shape(0)) || + (pres.shape(0) != vtmpk.shape(0)) || + (pres.shape(0) != uwin.shape(0)) || + (pres.shape(0) != vwin.shape(0)) || + (pres.shape(0) != theta.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + float* pcl_vtmp = new float[hght.size()]; + float* pcl_buoy = new float[hght.size()]; + + float pft = sharp::pyrocumulonimbus_firepower_threshold( + lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), + mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), + theta.data(), pcl_vtmp, pcl_buoy, pres.size(), pcl, phi, + beta_incr); + + delete[] pcl_vtmp; + delete[] pcl_buoy; + + return pft; + }, + nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), + nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), + nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), + nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, + R"pbdoc( +Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum +amount of firepower required to generate pyrocumulonimbus clouds for a +given atmospheric profile. Requires a PressureLayer to define a mixing +layer used to average values of potential temperature, mixing ratio, +and wind speed. The beta increment determines how to vary the plume +buoyancy factor, with smaller values resulting in more iteration steps. +Phi is the fire moisture to potential temperature increment ratio. + +Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. +If a parcel is passed, the values will be set with the PFT fire parcel. + +References +---------- +Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml +Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut +mix_layer : nwsspc.sharp.calc.layer.PressureLayer + A mixing layer to take average values of the environment from +pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) +temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) +mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) +virtual_temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) +u_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of U wind component values (m/s) +v_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of V wind compnent values (m/s) +potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of potential temperature values (K) +pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + If a parcel is provided, returns the parcel for the PFT +phi : float + The fire moisture to potential temperature increment ratio (kg/(kg*K)) +beta_incr : float + The fire plume buoyancy factor (unitless) + +Returns +------- +float + The PyroCB Firepower Threshold (Watts) + )pbdoc"); } diff --git a/src/nanobind/parcel_bindings.h b/src/nanobind/parcel_bindings.h index db72fb2c..56590230 100644 --- a/src/nanobind/parcel_bindings.h +++ b/src/nanobind/parcel_bindings.h @@ -4,6 +4,7 @@ // clang-format off #include #include +#include // clang-format on #include @@ -168,6 +169,203 @@ float The virtual temperature of the parcel (K) )pbdoc"); + nb::class_>(m_parcel, "lut_data") + .def(nb::init(), + nb::arg("lifter"), nb::arg("pmin") = 5000.0f, + nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, + nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, + nb::arg("n_thetae") = 221) + .def_ro("pres_min", &sharp::lut_data::pres_min) + .def_ro("pres_max", &sharp::lut_data::pres_max) + .def_ro("thte_min", &sharp::lut_data::thetae_min) + .def_ro("thte_max", &sharp::lut_data::thetae_max) + .def_ro("num_logp", &sharp::lut_data::num_logp) + .def_ro("num_thetae", + &sharp::lut_data::num_thetae); + + nb::class_>(m_parcel, "lut_data") + .def(nb::init(), + nb::arg("lifter"), nb::arg("pmin") = 5000.0f, + nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, + nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, + nb::arg("n_thetae") = 221) + .def_ro("pres_min", &sharp::lut_data::pres_min) + .def_ro("pres_max", &sharp::lut_data::pres_max) + .def_ro("thte_min", &sharp::lut_data::thetae_min) + .def_ro("thte_max", &sharp::lut_data::thetae_max) + .def_ro("num_logp", &sharp::lut_data::num_logp) + .def_ro("num_thetae", &sharp::lut_data::num_thetae); + + nb::class_>(m_parcel, "lifter_lut", + R"pbdoc( +A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) +for fast moist adiabatic ascent calculations. + +Instead of directly solving the moist ascent ODEs, this lifter uses +bilinear interpolation of a precomputed lookup table to determine the +parcel temperature. If the parcel's LCL falls outside the table bounds, +it falls back to the direct Wobus solver automatically. + +LUT based parcel ascent only works for pseudoadiabats. Constructing the +LUT with a reversible adiabat type will result in an error being thrown. + +Parameters +---------- +data : nwsspc.sharp.calc.parcel.lut_data + A shared lookup table constructed with a lifter_wobus instance. +)pbdoc") + .def(nb::init< + std::shared_ptr>>(), + nb::arg("data")) + .def_ro_static("lift_from_lcl", + &sharp::lifter_lut::lift_from_lcl, + R"pbdoc( +A static flag that helps the parcel lifting functions know where to lift from. +)pbdoc") + .def("setup", &sharp::lifter_lut::setup, + nb::arg("lcl_pres"), nb::arg("lcl_tmpk"), R"pbdoc( +Performs a setup step based on the LCL attributes. + +Computes the fractional index needed to select the correct pseudoadiabat +for lookup. If the LCL is outside the table bounds, it falls back to the +direct solver. + +Parameters +---------- +lcl_pres : float + The LCL pressure (Pa) +lcl_tmpk : float + The LCL temperature (K) + +Returns +------- +None +)pbdoc") + .def("__call__", &sharp::lifter_lut::operator(), + nb::is_operator(), nb::arg("pres"), nb::arg("tmpk"), + nb::arg("new_pres"), R"pbdoc( +Performs LUT interpolation to lift a parcel moist adiabatically. + +Parameters +---------- +pres : float + Parcel pressure (Pa) +tmpk : float + Parcel temperature (K) +new_pres : float + Final level of parcel after lift (Pa) + +Returns +------- +float + The temperature of the lifted parcel (K) +)pbdoc") + .def( + "parcel_virtual_temperature", + &sharp::lifter_lut::parcel_virtual_temperature, + nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( +Computes the virtual temperature of the parcel (after saturation). + +Parameters +---------- +pres : float + Parcel pressure (Pa) +tmpk : float + Parcel temperature (K) + +Returns +------- +float + The virtual temperature of the parcel (K) +)pbdoc"); + + nb::class_>(m_parcel, "lifter_lut", + R"pbdoc( +A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) +for fast moist adiabatic ascent calculations. + +Instead of directly solving the moist ascent ODEs, this lifter uses +bilinear interpolation of a precomputed lookup table to determine the +parcel temperature. If the parcel's LCL falls outside the table bounds, +it falls back to the direct CM1 solver automatically. + +LUT based parcel ascent only works for pseudoadiabats. Constructing the +LUT with a reversible adiabat type will result in an error being thrown. + +Parameters +---------- +data : lut_data + A shared lookup table constructed with a lifter_cm1 instance. +)pbdoc") + .def(nb::init< + std::shared_ptr>>(), + nb::arg("data")) + .def_ro_static("lift_from_lcl", + &sharp::lifter_lut::lift_from_lcl, + R"pbdoc( +A static flag that helps the parcel lifting functions know where to lift from. +The lifter_cm1 lifts from the last lifted level, rather than the LCL, because +it is an iterative solver. This results in major performance improvements while +maintaining accuracy. +)pbdoc") + .def("setup", &sharp::lifter_lut::setup, + nb::arg("lcl_pres"), nb::arg("lcl_tmpk"), R"pbdoc( +Performs a setup step based on the LCL attributes. + +Computes the fractional index needed to select the correct pseudoadiabat +for lookup. If the LCL is outside the table bounds, it falls back to the +direct solver. + +Parameters +---------- +lcl_pres : float + The LCL pressure (Pa) +lcl_tmpk : float + The LCL temperature (K) + +Returns +------- +None +)pbdoc") + .def("__call__", &sharp::lifter_lut::operator(), + nb::is_operator(), nb::arg("pres"), nb::arg("tmpk"), + nb::arg("new_pres"), R"pbdoc( +Performs LUT interpolation to lift a parcel moist adiabatically. + +Parameters +---------- +pres : float + Parcel pressure (Pa) +tmpk : float + Parcel temperature (K) +new_pres : float + Final level of parcel after lift (Pa) + +Returns +------- +float + The temperature of the lifted parcel (K) +)pbdoc") + .def("parcel_virtual_temperature", + &sharp::lifter_lut::parcel_virtual_temperature, + nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( +Computes the virtual temperature of the parcel (after saturation). + +Parameters +---------- +pres : float + Parcel pressure (Pa) +tmpk : float + Parcel temperature (K) + +Returns +------- +float + The virtual temperature of the parcel (K) +)pbdoc"); + nb::enum_(m_parcel, "LPL") .value("SFC", sharp::LPL::SFC, "A Surface Based Parcel") .value("FCST", sharp::LPL::FCST, "A Forecast Surface Parcel") @@ -262,14 +460,14 @@ numpy.ndarray[dtype=float32] "lift_parcel", [](sharp::Parcel& pcl, sharp::lifter_cm1& lifter, const_prof_arr_t pressure) { - const size_t NZ = pressure.size(); + const std::ptrdiff_t NZ = pressure.size(); float* virtemp_arr = new float[NZ]; pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); nb::capsule owner(virtemp_arr, [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(virtemp_arr, {NZ}, owner); + return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); }, nb::arg("lifter"), nb::arg("pressure"), R"pbdoc( @@ -286,6 +484,73 @@ lifter : nwsspc.sharp.calc.parcel.lifter_cm1 pressure : numpy.ndarray[dtype=float32] 1D NumPy array of Pressure levels for lifting (Pa) +Returns +------- +numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + )pbdoc") + .def( + "lift_parcel", + [](sharp::Parcel& pcl, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure) { + const std::ptrdiff_t NZ = pressure.size(); + float* virtemp_arr = new float[NZ]; + + pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); + + nb::capsule owner(virtemp_arr, + [](void* p) noexcept { delete[] (float*)p; }); + return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), + R"pbdoc( +Lifts a Parcel dry adiabatically from its LPL to its LCL dry +adiabatically, and then moist adiabatically from the LCL to +the top of the profile. The moist adiabat used is determined +by the type of lifting functor passed to the function (i.e. +lifter_wobus or lifter_cm1). + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + +Returns +------- +numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + )pbdoc") + .def( + "lift_parcel", + [](sharp::Parcel& pcl, sharp::lifter_lut& lifter, + const_prof_arr_t pressure) { + const std::ptrdiff_t NZ = pressure.size(); + float* virtemp_arr = new float[NZ]; + + pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); + + nb::capsule owner(virtemp_arr, + [](void* p) noexcept { delete[] (float*)p; }); + return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), + R"pbdoc( +Lifts a Parcel dry adiabatically from its LPL to its LCL dry +adiabatically, and then moist adiabatically from the LCL to +the top of the profile. The moist adiabat used is determined +by the type of lifting functor passed to the function (i.e. +lifter_wobus or lifter_cm1). + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + Returns ------- numpy.ndarray[dtype=float32] @@ -350,7 +615,7 @@ tuple[float, float] R"pbdoc( Find the pressure of the Maximum Parcel Level (MPL). -The Maximum Parcel Level (MPL) is the level a parcel woud reach +The Maximum Parcel Level (MPL) is the level a parcel would reach if it expended all of its integrated positive buoyancy past the Equilibrium Level. It is found by integrating negatively buoyant area above the Equilibrium Level until the integrated negative @@ -362,9 +627,9 @@ For valid calculations, nwsspc.sharp.calc.parcel.Parcel.cape_cinh must be called first, or nwsspc.sharp.calc.parcel.Parcel.cape and nwsspc.sharp.calc.parcel.Parcel.eql_pressure must be set. -A values of nwsspc.sharp.calc.constants.MISSING is returned if: +A value of nwsspc.sharp.calc.constants.MISSING is returned if: * CAPE is 0 - * nwsspc.sharp.calc.parce.Parcel.eql_pressure is MISSING + * nwsspc.sharp.calc.parcel.Parcel.eql_pressure is MISSING * No valid MPL candidate is found within the profile In this scenario, it likely exceeds the top of the available data @@ -736,6 +1001,7 @@ dewpoint : numpy.ndarray[dtype=float32] Returns ------- nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values )pbdoc") .def_static( "most_unstable_parcel", @@ -790,6 +1056,241 @@ virtual_temperature : numpy.ndarray[dtype=float32] dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of profile dewpoint values (K) +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + )pbdoc") + .def_static( + "most_unstable_parcel", + [](sharp::PressureLayer& layer, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t virtemp, + const_prof_arr_t dewpoint) { + if ((pressure.shape(0) != temperature.shape(0)) || + (pressure.shape(0) != dewpoint.shape(0)) || + (pressure.shape(0) != height.shape(0)) || + (pressure.shape(0) != virtemp.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + const std::ptrdiff_t NZ = height.size(); + float* pcl_virtemp_arr = new float[NZ]; + float* pcl_buoy_arr = new float[NZ]; + + sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( + layer, lifter, pressure.data(), height.data(), + temperature.data(), virtemp.data(), dewpoint.data(), + pcl_virtemp_arr, pcl_buoy_arr, NZ); + + delete[] pcl_virtemp_arr; + delete[] pcl_buoy_arr; + + return max_pcl; + }, + nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), + nb::arg("virtual_temperature"), nb::arg("dewpoint"), + R"pbdoc( +Given input arrays of pressure, height, temperature, virtual temperature, +and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and +parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + +Parameters +---------- +layer : nwsspc.sharp.calc.layer.PressureLayer + PressureLayer for which to search for the Most Unstable Parcel +lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) +virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + )pbdoc") + .def_static( + "most_unstable_parcel", + [](sharp::HeightLayer& layer, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t virtemp, + const_prof_arr_t dewpoint) { + if ((pressure.shape(0) != temperature.shape(0)) || + (pressure.shape(0) != dewpoint.shape(0)) || + (pressure.shape(0) != height.shape(0)) || + (pressure.shape(0) != virtemp.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + const std::ptrdiff_t NZ = height.size(); + + float* pcl_virtemp_arr = new float[NZ]; + float* pcl_buoy_arr = new float[NZ]; + + sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( + layer, lifter, pressure.data(), height.data(), + temperature.data(), virtemp.data(), dewpoint.data(), + pcl_virtemp_arr, pcl_buoy_arr, NZ); + + delete[] pcl_virtemp_arr; + delete[] pcl_buoy_arr; + + return max_pcl; + }, + nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), + nb::arg("virtual_temperature"), nb::arg("dewpoint"), + R"pbdoc( +Given input arrays of pressure, height, temperature, virtual temperature, +and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and +parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + +Parameters +---------- +layer : nwsspc.sharp.calc.layer.HeightLayer + HeightLayer for which to search for the Most Unstable Parcel +lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) +virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + )pbdoc") + .def_static( + "most_unstable_parcel", + [](sharp::PressureLayer& layer, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t virtemp, + const_prof_arr_t dewpoint) { + if ((pressure.shape(0) != temperature.shape(0)) || + (pressure.shape(0) != dewpoint.shape(0)) || + (pressure.shape(0) != height.shape(0)) || + (pressure.shape(0) != virtemp.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + const std::ptrdiff_t NZ = height.size(); + + float* pcl_virtemp_arr = new float[NZ]; + float* pcl_buoy_arr = new float[NZ]; + + sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( + layer, lifter, pressure.data(), height.data(), + temperature.data(), virtemp.data(), dewpoint.data(), + pcl_virtemp_arr, pcl_buoy_arr, NZ); + + delete[] pcl_virtemp_arr; + delete[] pcl_buoy_arr; + + return max_pcl; + }, + nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), + nb::arg("virtual_temperature"), nb::arg("dewpoint"), + R"pbdoc( +Given input arrays of pressure, height, temperature, virtual temperature, +and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and +parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + +Parameters +---------- +layer : nwsspc.sharp.calc.layer.PressureLayer + PressureLayer for which to search for the Most Unstable Parcel +lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) +virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + )pbdoc") + .def_static( + "most_unstable_parcel", + [](sharp::HeightLayer& layer, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t virtemp, + const_prof_arr_t dewpoint) { + if ((pressure.shape(0) != temperature.shape(0)) || + (pressure.shape(0) != dewpoint.shape(0)) || + (pressure.shape(0) != height.shape(0)) || + (pressure.shape(0) != virtemp.shape(0))) { + throw nb::buffer_error( + "All input arrays must have the same size!"); + } + const std::ptrdiff_t NZ = height.size(); + + float* pcl_virtemp_arr = new float[NZ]; + float* pcl_buoy_arr = new float[NZ]; + + sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( + layer, lifter, pressure.data(), height.data(), + temperature.data(), virtemp.data(), dewpoint.data(), + pcl_virtemp_arr, pcl_buoy_arr, NZ); + + delete[] pcl_virtemp_arr; + delete[] pcl_buoy_arr; + + return max_pcl; + }, + nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), + nb::arg("virtual_temperature"), nb::arg("dewpoint"), + R"pbdoc( +Given input arrays of pressure, height, temperature, virtual temperature, +and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and +parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + +Parameters +---------- +layer : nwsspc.sharp.calc.layer.HeightLayer + HeightLayer for which to search for the Most Unstable Parcel +lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) +virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + Returns ------- nwsspc.sharp.calc.parcel.Parcel @@ -896,6 +1397,78 @@ lifter : nwsspc.sharp.calc.parcel.lifter_cm1 pressure : numpy.ndarray[dtype=float32] 1D NumPy array of Pressure levels for lifting (Pa) +Returns +------- +numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) + )pbdoc") + .def( + "lower_parcel", + [](sharp::DowndraftParcel& pcl, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure) { + const std::ptrdiff_t NZ = pressure.size(); + float* tmpk_arr = new float[NZ]; + + pcl.lower_parcel(lifter, pressure.data(), tmpk_arr, NZ); + + nb::capsule owner(tmpk_arr, + [](void* p) noexcept { delete[] (float*)p; }); + return out_arr_t(tmpk_arr, {pressure.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), + R"pbdoc( +Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist +adiabatically from its LPL to the surface. The moist adiabat used +is determined by the type of lifting functor passed to the function +(i.e. lifter_wobus or lifter_cm1). + +Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual +temperature correction is not used for downdraft parcels. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + +Returns +------- +numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) + )pbdoc") + .def( + "lower_parcel", + [](sharp::DowndraftParcel& pcl, + sharp::lifter_lut& lifter, + const_prof_arr_t pressure) { + const std::ptrdiff_t NZ = pressure.size(); + float* tmpk_arr = new float[NZ]; + + pcl.lower_parcel(lifter, pressure.data(), tmpk_arr, NZ); + + nb::capsule owner(tmpk_arr, + [](void* p) noexcept { delete[] (float*)p; }); + return out_arr_t(tmpk_arr, {pressure.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), + R"pbdoc( +Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist +adiabatically from its LPL to the surface. The moist adiabat used +is determined by the type of lifting functor passed to the function +(i.e. lifter_wobus or lifter_cm1). + +Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual +temperature correction is not used for downdraft parcels. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + Returns ------- numpy.ndarray[dtype=float32] diff --git a/src/nanobind/thermo_bindings.h b/src/nanobind/thermo_bindings.h index 72357423..343f6d04 100644 --- a/src/nanobind/thermo_bindings.h +++ b/src/nanobind/thermo_bindings.h @@ -1132,6 +1132,82 @@ temperature : float dewpoint : float The ambient dewpoint temperature (K) +Returns +------- +float + The wetbulb temperature (K) + )pbdoc"); + + m_therm.def( + "wetbulb", + [](sharp::lifter_lut &lifter, float pressure, + float temperature, float dewpoint) { + return sharp::wetbulb(lifter, pressure, temperature, dewpoint); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet bulb temperature (K) given the ambient pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, and +dewpoint temperature to its Lifted Condensation Level (LCL). To compute the +temperature and pressure of the LCL, an approximation is used. See the +lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to its initial pressure level along a moist adiabat or +pseudoadiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : float + The ambient pressure (Pa) +temperature : float + The ambient temperature (K) +dewpoint : float + The ambient dewpoint temperature (K) + +Returns +------- +float + The wetbulb temperature (K) + )pbdoc"); + + m_therm.def( + "wetbulb", + [](sharp::lifter_lut &lifter, float pressure, + float temperature, float dewpoint) { + return sharp::wetbulb(lifter, pressure, temperature, dewpoint); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet bulb temperature (K) given the ambient pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, and +dewpoint temperature to its Lifted Condensation Level (LCL). To compute the +temperature and pressure of the LCL, an approximation is used. See the +lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to its initial pressure level along a moist adiabat or +pseudoadiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : float + The ambient pressure (Pa) +temperature : float + The ambient temperature (K) +dewpoint : float + The ambient dewpoint temperature (K) + Returns ------- float @@ -1234,6 +1310,110 @@ temperature : numpy.ndarray[dtype=float32] dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of ambient dewpoint temperatures (K) +Returns +------- +numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) + )pbdoc"); + + m_therm.def( + "wetbulb", + [](sharp::lifter_lut &lifter, + const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, + const_prof_arr_t dwpk_arr) { + auto pres = pres_arr.view(); + auto tmpk = tmpk_arr.view(); + auto dwpk = dwpk_arr.view(); + + float *wb_out = new float[pres.shape(0)]; + + for (size_t k = 0; k < pres.shape(0); ++k) { + wb_out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); + } + + nb::capsule owner(wb_out, + [](void *p) noexcept { delete[] (float *)p; }); + + return out_arr_t(wb_out, {tmpk.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet bulb temperature (K) given the ambient pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, and +dewpoint temperature to its Lifted Condensation Level (LCL). To compute the +temperature and pressure of the LCL, an approximation is used. See the +lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to its initial pressure level along a moist adiabat or +pseudoadiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatureds (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) + +Returns +------- +numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) + )pbdoc"); + + m_therm.def( + "wetbulb", + [](sharp::lifter_lut &lifter, + const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, + const_prof_arr_t dwpk_arr) { + auto pres = pres_arr.view(); + auto tmpk = tmpk_arr.view(); + auto dwpk = dwpk_arr.view(); + + float *wb_out = new float[pres.shape(0)]; + + for (size_t k = 0; k < pres.shape(0); ++k) { + wb_out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); + } + + nb::capsule owner(wb_out, + [](void *p) noexcept { delete[] (float *)p; }); + + return out_arr_t(wb_out, {tmpk.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet bulb temperature (K) given the ambient pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, and +dewpoint temperature to its Lifted Condensation Level (LCL). To compute the +temperature and pressure of the LCL, an approximation is used. See the +lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to its initial pressure level along a moist adiabat or +pseudoadiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatureds (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) + Returns ------- numpy.ndarray[dtype=float32] @@ -1312,6 +1492,84 @@ temperature : float dewpoint : float the ambient dewpoint temperature +Returns +------- +float + The wet-bulb potential temperature (K) + )pbdoc"); + + m_therm.def( + "theta_wetbulb", + [](sharp::lifter_lut &lifter, float pressure, + float temperature, float dewpoint) { + return sharp::theta_wetbulb(lifter, pressure, temperature, + dewpoint); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet-bulb potential temperature (K) given the pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, +and dewpoint temperature to its Lifted Condensation Level (LCL). +To compute the temperature and pressure of the LCL, an approximation +is used. See the lcl_temperature fuction for further detail. + +After the parcel has reached the LCL, the lifted passed to the function +lowers the parcel to the standard parcel reference pressure level +(1000 hPa) along a moist adiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : float + the ambient air pressure (Pa) +temperature : float + the ambient air temperature (K) +dewpoint : float + the ambient dewpoint temperature + +Returns +------- +float + The wet-bulb potential temperature (K) + )pbdoc"); + + m_therm.def( + "theta_wetbulb", + [](sharp::lifter_lut &lifter, float pressure, + float temperature, float dewpoint) { + return sharp::theta_wetbulb(lifter, pressure, temperature, + dewpoint); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet-bulb potential temperature (K) given the pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, +and dewpoint temperature to its Lifted Condensation Level (LCL). +To compute the temperature and pressure of the LCL, an approximation +is used. See the lcl_temperature fuction for further detail. + +After the parcel has reached the LCL, the lifted passed to the function +lowers the parcel to the standard parcel reference pressure level +(1000 hPa) along a moist adiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : float + the ambient air pressure (Pa) +temperature : float + the ambient air temperature (K) +dewpoint : float + the ambient dewpoint temperature + Returns ------- float @@ -1416,6 +1674,112 @@ temperature : numpy.ndarray[dtype=float32] dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of ambient dewpoint temperature +Returns +------- +numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) + )pbdoc"); + + m_therm.def( + "theta_wetbulb", + [](sharp::lifter_lut &lifter, + const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, + const_prof_arr_t dwpk_arr) { + auto pres = pres_arr.view(); + auto tmpk = tmpk_arr.view(); + auto dwpk = dwpk_arr.view(); + + float *theta_wb = new float[pres.shape(0)]; + + for (size_t k = 0; k < pres.shape(0); ++k) { + theta_wb[k] = + sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); + } + + nb::capsule owner(theta_wb, + [](void *p) noexcept { delete[] (float *)p; }); + + return out_arr_t(theta_wb, {tmpk.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet-bulb potential temperature (K) given the pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, +and dewpoint temperature to its Lifted Condensation Level (LCL). +To compute the temperature and pressure of the LCL, an approximation +is used. See the lcl_temperature fuction for further detail. + +After the parcel has reached the LCL, the lifted passed to the function +lowers the parcel to the standard parcel reference pressure level +(1000 hPa) along a moist adiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature + +Returns +------- +numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) + )pbdoc"); + + m_therm.def( + "theta_wetbulb", + [](sharp::lifter_lut &lifter, + const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, + const_prof_arr_t dwpk_arr) { + auto pres = pres_arr.view(); + auto tmpk = tmpk_arr.view(); + auto dwpk = dwpk_arr.view(); + + float *theta_wb = new float[pres.shape(0)]; + + for (size_t k = 0; k < pres.shape(0); ++k) { + theta_wb[k] = + sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); + } + + nb::capsule owner(theta_wb, + [](void *p) noexcept { delete[] (float *)p; }); + + return out_arr_t(theta_wb, {tmpk.shape(0)}, owner); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( +Compute the wet-bulb potential temperature (K) given the pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, +and dewpoint temperature to its Lifted Condensation Level (LCL). +To compute the temperature and pressure of the LCL, an approximation +is used. See the lcl_temperature fuction for further detail. + +After the parcel has reached the LCL, the lifted passed to the function +lowers the parcel to the standard parcel reference pressure level +(1000 hPa) along a moist adiabat. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature + Returns ------- numpy.ndarray[dtype=float32] From de3ae1723ce3c5e76717bb0c365eac7df010f740 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 14:36:53 -0500 Subject: [PATCH 37/55] added LUT lifter to docs --- docs/reference/cpp_api/parcel.rst | 6 ++++++ docs/reference/python_api/parcel.rst | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/reference/cpp_api/parcel.rst b/docs/reference/cpp_api/parcel.rst index accb454e..b18d58b6 100644 --- a/docs/reference/cpp_api/parcel.rst +++ b/docs/reference/cpp_api/parcel.rst @@ -9,6 +9,12 @@ Parcel Lifters .. doxygenstruct:: sharp::lifter_cm1 :members: +.. doxygenstruct:: sharp::lut_data + :members: + +.. doxygenstruct:: sharp::lifter_lut + :members: + Parcels ------- .. doxygenenum:: sharp::LPL diff --git a/docs/reference/python_api/parcel.rst b/docs/reference/python_api/parcel.rst index 48baae86..2918b995 100644 --- a/docs/reference/python_api/parcel.rst +++ b/docs/reference/python_api/parcel.rst @@ -20,6 +20,18 @@ SHARPlib supports configurable parcel ascent via parcel lifter 'functors'. These .. autofunction:: nwsspc.sharp.calc.parcel.lifter_cm1.__call__ .. autofunction:: nwsspc.sharp.calc.parcel.lifter_cm1.parcel_virtual_temperature +.. autoclass:: nwsspc.sharp.calc.parcel.lut_data + + .. autofunction:: nwsspc.sharp.calc.parcel.lut_data.get_logp_indices + .. autofunction:: nwsspc.sharp.calc.parcel.lut_data.find_thetae_index + .. autofunction:: nwsspc.sharp.calc.parcel.lut_data.bilinear_interp + + +.. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut + + .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.__call__ + .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.parcel_virtual_temperature + Parcels ------- Parcel creation, parcel ascent, and integrated CAPE/CINH. From e7a991dc9e63ca1edd993650ad3cae87f2996471 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 15:01:58 -0500 Subject: [PATCH 38/55] updated documentation for new lifter_lut --- docs/reference/cpp_api/thermo.rst | 4 +- docs/reference/python_api/parcel.rst | 9 +- include/SHARPlib/parcel.h | 23 ++ src/nanobind/nwsspc/sharp/calc/params.pyi | 254 +++++++++++++++++ src/nanobind/nwsspc/sharp/calc/parcel.pyi | 332 +++++++++++++++++++++- src/nanobind/nwsspc/sharp/calc/thermo.pyi | 256 +++++++++++++++++ src/nanobind/parcel_bindings.h | 4 + 7 files changed, 872 insertions(+), 10 deletions(-) diff --git a/docs/reference/cpp_api/thermo.rst b/docs/reference/cpp_api/thermo.rst index d134c35f..bec93dde 100644 --- a/docs/reference/cpp_api/thermo.rst +++ b/docs/reference/cpp_api/thermo.rst @@ -32,8 +32,8 @@ Moist Variables .. doxygenfunction:: sharp::buoyancy(const float, const float) .. doxygenfunction:: sharp::buoyancy(const float[], const float[], float[], std::ptrdiff_t) .. doxygenfunction:: sharp::thetae -.. doxygenfunction:: sharp::wetbulb(Lft, float, float, float) -.. doxygenfunction:: sharp::theta_wetbulb(Lft, float, float, float) +.. doxygenfunction:: sharp::wetbulb(Lft&, float, float, float) +.. doxygenfunction:: sharp::theta_wetbulb(Lft&, float, float, float) Moist Ascent ------------ diff --git a/docs/reference/python_api/parcel.rst b/docs/reference/python_api/parcel.rst index 2918b995..cf6e5c44 100644 --- a/docs/reference/python_api/parcel.rst +++ b/docs/reference/python_api/parcel.rst @@ -9,6 +9,8 @@ SHARPlib supports configurable parcel ascent via parcel lifter 'functors'. These .. autoclass:: nwsspc.sharp.calc.parcel.lifter_wobus + .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_wobus.ma_type + .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_wobus.lift_from_lcl .. autofunction:: nwsspc.sharp.calc.parcel.lifter_wobus.__call__ .. autofunction:: nwsspc.sharp.calc.parcel.lifter_wobus.parcel_virtual_temperature @@ -16,19 +18,16 @@ SHARPlib supports configurable parcel ascent via parcel lifter 'functors'. These .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_cm1.converge .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_cm1.ma_type + .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_cm1.lift_from_lcl .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_cm1.pressure_incr .. autofunction:: nwsspc.sharp.calc.parcel.lifter_cm1.__call__ .. autofunction:: nwsspc.sharp.calc.parcel.lifter_cm1.parcel_virtual_temperature .. autoclass:: nwsspc.sharp.calc.parcel.lut_data - .. autofunction:: nwsspc.sharp.calc.parcel.lut_data.get_logp_indices - .. autofunction:: nwsspc.sharp.calc.parcel.lut_data.find_thetae_index - .. autofunction:: nwsspc.sharp.calc.parcel.lut_data.bilinear_interp - - .. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut + .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_lut.lift_from_lcl .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.__call__ .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.parcel_virtual_temperature diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 6b94964a..be50f797 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -51,7 +51,14 @@ namespace sharp { * the parcel lifting algorithm to their specifications. */ struct lifter_wobus { + /** + * \brief Whether the parcel should be lifted from the LCL or last level + */ static constexpr bool lift_from_lcl = true; + + /** + * \brief The type of moist adiabat to use, as defined by sharp::adiabat + */ static constexpr adiabat ma_type = adiabat::pseudo_liq; /** @@ -115,6 +122,9 @@ struct lifter_wobus { * the parcel lifting algorithm to their specifications. */ struct lifter_cm1 { + /** + * \brief Whether the parcel should be lifted from the LCL or last level + */ static constexpr bool lift_from_lcl = false; /** @@ -542,6 +552,9 @@ struct lut_data { */ template struct lifter_lut { + /** + * \brief Whether the parcel should be lifted from the LCL or last level + */ static constexpr bool lift_from_lcl = Lft::lift_from_lcl; /** @@ -564,6 +577,16 @@ struct lifter_lut { */ Lft m_lifter; + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Constructor for lifter_lut + * + * Constructs lifter_lut given a sared pointer to a previously + * generated table. + * + * \param data Shared pointer to a sharp::lut_data + */ explicit lifter_lut(std::shared_ptr> data) : m_data(std::move(data)), m_lifter(m_data->m_lifter) {} diff --git a/src/nanobind/nwsspc/sharp/calc/params.pyi b/src/nanobind/nwsspc/sharp/calc/params.pyi index 1c3aeb3b..aa9f2f38 100644 --- a/src/nanobind/nwsspc/sharp/calc/params.pyi +++ b/src/nanobind/nwsspc/sharp/calc/params.pyi @@ -96,6 +96,94 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure The Effective Inflow Layer """ +@overload +def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: + """ + Computes the Effective Inflow Layer, or the layer of the atmosphere + beliefed to be the primary source of inflow for supercell thunderstorms. + The Effective Inflow Layer, and its use in computing shear and storm + relative helicity, is described by Thompson et al. 2007: + https://www.spc.noaa.gov/publications/thompson/effective.pdf + + Standard/default values for cape_thresh and cinh_thresh have been + experimentally determined to be cape_thresh = 100 J/kg and + cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the + 'mupcl' kwarg, the Most Unstable parcel found during the EIL search will + be returned. + + References + ---------- + Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) + temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) + dewpoint : numpy.ndarray[dtype=float32] + A 1D NumPy array of dewpoint values (K) + virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) + cape_thresh : float, default = 100.0 + The CAPE threshold used to compute the Effective Inflow Layer + cinh_thresh : float, default = -250.0 + The CINH threshold used to compute the Effective Inflow Layer + mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + + Returns + ------- + nwsspc.sharp.calc.layer.PressureLayer + The Effective Inflow Layer + """ + +@overload +def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: + """ + Computes the Effective Inflow Layer, or the layer of the atmosphere + beliefed to be the primary source of inflow for supercell thunderstorms. + The Effective Inflow Layer, and its use in computing shear and storm + relative helicity, is described by Thompson et al. 2007: + https://www.spc.noaa.gov/publications/thompson/effective.pdf + + Standard/default values for cape_thresh and cinh_thresh have been + experimentally determined to be cape_thresh = 100 J/kg and + cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the + 'mupcl' kwarg, the Most Unstable parcel found during the EIL search will + be returned. + + References + ---------- + Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) + temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) + dewpoint : numpy.ndarray[dtype=float32] + A 1D NumPy array of dewpoint values (K) + virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) + cape_thresh : float, default = 100.0 + The CAPE threshold used to compute the Effective Inflow Layer + cinh_thresh : float, default = -250.0 + The CINH threshold used to compute the Effective Inflow Layer + mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + + Returns + ------- + nwsspc.sharp.calc.layer.PressureLayer + The Effective Inflow Layer + """ + @overload def storm_motion_bunkers(pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mean_wind_layer_agl: nwsspc.sharp.calc.layer.HeightLayer, wind_shear_layer_agl: nwsspc.sharp.calc.layer.HeightLayer, leftMover: bool = False, pressureWeighted: bool = False) -> nwsspc.sharp.calc.winds.WindComponents: """ @@ -512,6 +600,66 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure The convective temperature (K) """ +@overload +def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: + """ + Computes the convective temperature by iteratively lifting parcels from + the surface using a lowest 100 hPa mean mixing ratio and increasing + surface temperatures to find the first parcel that reaches the CINH + threshold. The first guess is the current surface temperature. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) + temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) + virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) + mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) + cinh_thresh : float, default = -1.0 + The CINH threshold used to compute the convective temperature (J/kg) + + Returns + ------- + float + The convective temperature (K) + """ + +@overload +def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: + """ + Computes the convective temperature by iteratively lifting parcels from + the surface using a lowest 100 hPa mean mixing ratio and increasing + surface temperatures to find the first parcel that reaches the CINH + threshold. The first guess is the current surface temperature. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) + temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) + virtemp : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) + mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) + cinh_thresh : float, default = -1.0 + The CINH threshold used to compute the convective temperature (J/kg) + + Returns + ------- + float + The convective temperature (K) + """ + def precipitable_water(layer: nwsspc.sharp.calc.layer.PressureLayer, pres: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixr: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> float: """ Given a PressureLayer to integrate over, compute the precipitable water @@ -784,3 +932,109 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter float The PyroCB Firepower Threshold (Watts) """ + +@overload +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: + """ + Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum + amount of firepower required to generate pyrocumulonimbus clouds for a + given atmospheric profile. Requires a PressureLayer to define a mixing + layer used to average values of potential temperature, mixing ratio, + and wind speed. The beta increment determines how to vary the plume + buoyancy factor, with smaller values resulting in more iteration steps. + Phi is the fire moisture to potential temperature increment ratio. + + Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. + If a parcel is passed, the values will be set with the PFT fire parcel. + + References + ---------- + Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml + Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + mix_layer : nwsspc.sharp.calc.layer.PressureLayer + A mixing layer to take average values of the environment from + pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) + temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) + mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) + virtual_temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) + u_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of U wind component values (m/s) + v_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of V wind compnent values (m/s) + potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of potential temperature values (K) + pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + If a parcel is provided, returns the parcel for the PFT + phi : float + The fire moisture to potential temperature increment ratio (kg/(kg*K)) + beta_incr : float + The fire plume buoyancy factor (unitless) + + Returns + ------- + float + The PyroCB Firepower Threshold (Watts) + """ + +@overload +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: + """ + Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum + amount of firepower required to generate pyrocumulonimbus clouds for a + given atmospheric profile. Requires a PressureLayer to define a mixing + layer used to average values of potential temperature, mixing ratio, + and wind speed. The beta increment determines how to vary the plume + buoyancy factor, with smaller values resulting in more iteration steps. + Phi is the fire moisture to potential temperature increment ratio. + + Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. + If a parcel is passed, the values will be set with the PFT fire parcel. + + References + ---------- + Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml + Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + mix_layer : nwsspc.sharp.calc.layer.PressureLayer + A mixing layer to take average values of the environment from + pressure : numpy.ndarray[dtype=float32] + A 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + A 1D NumPy array of height values (Pa) + temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of temperature values (K) + mixratio : numpy.ndarray[dtype=float32] + A 1D NumPy array of water vapor mixing ratio values (g/g) + virtual_temperature : numpy.ndarray[dtype=float32] + A 1D NumPy array of virtual temperature values (K) + u_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of U wind component values (m/s) + v_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of V wind compnent values (m/s) + potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of potential temperature values (K) + pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + If a parcel is provided, returns the parcel for the PFT + phi : float + The fire moisture to potential temperature increment ratio (kg/(kg*K)) + beta_incr : float + The fire plume buoyancy factor (unitless) + + Returns + ------- + float + The PyroCB Firepower Threshold (Watts) + """ diff --git a/src/nanobind/nwsspc/sharp/calc/parcel.pyi b/src/nanobind/nwsspc/sharp/calc/parcel.pyi index f769fada..f7050ee7 100644 --- a/src/nanobind/nwsspc/sharp/calc/parcel.pyi +++ b/src/nanobind/nwsspc/sharp/calc/parcel.pyi @@ -34,6 +34,9 @@ class lifter_wobus: A static flag that helps the parcel lifting functions know where to lift from. """ + ma_type: nwsspc.sharp.calc.thermo.adiabat = ... + """A static flag for the type of solver this is""" + @property def converge(self) -> float: """The iterative convergence criteria (K)""" @@ -179,6 +182,112 @@ class lifter_cm1: The virtual temperature of the parcel (K) """ +class lut_data: + def __init__(self, lifter: lifter_cm1, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> None: ... + + @property + def pres_min(self) -> float: ... + + @property + def pres_max(self) -> float: ... + + @property + def thte_min(self) -> float: ... + + @property + def thte_max(self) -> float: ... + + @property + def num_logp(self) -> int: ... + + @property + def num_thetae(self) -> int: ... + +class lifter_lut: + """ + A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) + for fast moist adiabatic ascent calculations. + + Instead of directly solving the moist ascent ODEs, this lifter uses + bilinear interpolation of a precomputed lookup table to determine the + parcel temperature. If the parcel's LCL falls outside the table bounds, + it falls back to the direct CM1 solver automatically. + + LUT based parcel ascent only works for pseudoadiabats. Constructing the + LUT with a reversible adiabat type will result in an error being thrown. + + Parameters + ---------- + data : lut_data + A shared lookup table constructed with a lifter_cm1 instance. + """ + + def __init__(self, data: lut_data) -> None: ... + + lift_from_lcl: bool = ... + """ + A static flag that helps the parcel lifting functions know where to lift from. + The lifter_cm1 lifts from the last lifted level, rather than the LCL, because + it is an iterative solver. This results in major performance improvements while + maintaining accuracy. + """ + + def setup(self, lcl_pres: float, lcl_tmpk: float) -> None: + """ + Performs a setup step based on the LCL attributes. + + Computes the fractional index needed to select the correct pseudoadiabat + for lookup. If the LCL is outside the table bounds, it falls back to the + direct solver. + + Parameters + ---------- + lcl_pres : float + The LCL pressure (Pa) + lcl_tmpk : float + The LCL temperature (K) + + Returns + ------- + None + """ + + def __call__(self, pres: float, tmpk: float, new_pres: float) -> float: + """ + Performs LUT interpolation to lift a parcel moist adiabatically. + + Parameters + ---------- + pres : float + Parcel pressure (Pa) + tmpk : float + Parcel temperature (K) + new_pres : float + Final level of parcel after lift (Pa) + + Returns + ------- + float + The temperature of the lifted parcel (K) + """ + + def parcel_virtual_temperature(self, pres: float, tmpk: float) -> float: + """ + Computes the virtual temperature of the parcel (after saturation). + + Parameters + ---------- + pres : float + Parcel pressure (Pa) + tmpk : float + Parcel temperature (K) + + Returns + ------- + float + The virtual temperature of the parcel (K) + """ + class LPL(enum.Enum): SFC = 1 """A Surface Based Parcel""" @@ -331,6 +440,50 @@ class Parcel: A 1D NumPy array of parcel virtual temperature values (K) """ + @overload + def lift_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Lifts a Parcel dry adiabatically from its LPL to its LCL dry + adiabatically, and then moist adiabatically from the LCL to + the top of the profile. The moist adiabat used is determined + by the type of lifting functor passed to the function (i.e. + lifter_wobus or lifter_cm1). + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + + Returns + ------- + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + """ + + @overload + def lift_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Lifts a Parcel dry adiabatically from its LPL to its LCL dry + adiabatically, and then moist adiabatically from the LCL to + the top of the profile. The moist adiabat used is determined + by the type of lifting functor passed to the function (i.e. + lifter_wobus or lifter_cm1). + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + + Returns + ------- + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + """ + def find_lfc_el(self, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], buoyancy: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: """ Searches the buoyancy array for the LFC and EL combination that results @@ -365,7 +518,7 @@ class Parcel: """ Find the pressure of the Maximum Parcel Level (MPL). - The Maximum Parcel Level (MPL) is the level a parcel woud reach + The Maximum Parcel Level (MPL) is the level a parcel would reach if it expended all of its integrated positive buoyancy past the Equilibrium Level. It is found by integrating negatively buoyant area above the Equilibrium Level until the integrated negative @@ -377,9 +530,9 @@ class Parcel: must be called first, or nwsspc.sharp.calc.parcel.Parcel.cape and nwsspc.sharp.calc.parcel.Parcel.eql_pressure must be set. - A values of nwsspc.sharp.calc.constants.MISSING is returned if: + A value of nwsspc.sharp.calc.constants.MISSING is returned if: * CAPE is 0 - * nwsspc.sharp.calc.parce.Parcel.eql_pressure is MISSING + * nwsspc.sharp.calc.parcel.Parcel.eql_pressure is MISSING * No valid MPL candidate is found within the profile In this scenario, it likely exceeds the top of the available data @@ -619,6 +772,7 @@ class Parcel: Returns ------- nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values """ @overload @@ -652,6 +806,130 @@ class Parcel: Parcel with most-unstable values """ + @overload + @staticmethod + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + """ + Given input arrays of pressure, height, temperature, virtual temperature, + and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + layer : nwsspc.sharp.calc.layer.PressureLayer + PressureLayer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) + virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + + Returns + ------- + nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + """ + + @overload + @staticmethod + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + """ + Given input arrays of pressure, height, temperature, virtual temperature, + and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + layer : nwsspc.sharp.calc.layer.HeightLayer + HeightLayer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) + virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + + Returns + ------- + nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + """ + + @overload + @staticmethod + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + """ + Given input arrays of pressure, height, temperature, virtual temperature, + and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + layer : nwsspc.sharp.calc.layer.PressureLayer + PressureLayer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) + virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + + Returns + ------- + nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + """ + + @overload + @staticmethod + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + """ + Given input arrays of pressure, height, temperature, virtual temperature, + and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + layer : nwsspc.sharp.calc.layer.HeightLayer + HeightLayer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut + Parcel lifting routine to use for moist ascent + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) + virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + + Returns + ------- + nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + """ + class DowndraftParcel: """ Contains information about a DowndraftParcel's starting level and @@ -760,6 +1038,54 @@ class DowndraftParcel: A 1D NumPy array of parcel temperature values (K) """ + @overload + def lower_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist + adiabatically from its LPL to the surface. The moist adiabat used + is determined by the type of lifting functor passed to the function + (i.e. lifter_wobus or lifter_cm1). + + Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual + temperature correction is not used for downdraft parcels. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + + Returns + ------- + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) + """ + + @overload + def lower_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist + adiabatically from its LPL to the surface. The moist adiabat used + is determined by the type of lifting functor passed to the function + (i.e. lifter_wobus or lifter_cm1). + + Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual + temperature correction is not used for downdraft parcels. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + An instantiated lifter_lut functor + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + + Returns + ------- + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) + """ + def cape_cinh(self, pres: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], hght: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], buoy: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: """ Assuming that nwsspc.sharp.calc.parcel.DowndraftParcel.lower_parcel diff --git a/src/nanobind/nwsspc/sharp/calc/thermo.pyi b/src/nanobind/nwsspc/sharp/calc/thermo.pyi index a5e91548..909c8ae4 100644 --- a/src/nanobind/nwsspc/sharp/calc/thermo.pyi +++ b/src/nanobind/nwsspc/sharp/calc/thermo.pyi @@ -781,6 +781,70 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temper The wetbulb temperature (K) """ +@overload +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: + """ + Compute the wet bulb temperature (K) given the ambient pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, and + dewpoint temperature to its Lifted Condensation Level (LCL). To compute the + temperature and pressure of the LCL, an approximation is used. See the + lcl_temperature function for further detail. + + After the parcel has reached the LCL, the lifter passed to the function + lowers the parcel to its initial pressure level along a moist adiabat or + pseudoadiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : float + The ambient pressure (Pa) + temperature : float + The ambient temperature (K) + dewpoint : float + The ambient dewpoint temperature (K) + + Returns + ------- + float + The wetbulb temperature (K) + """ + +@overload +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: + """ + Compute the wet bulb temperature (K) given the ambient pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, and + dewpoint temperature to its Lifted Condensation Level (LCL). To compute the + temperature and pressure of the LCL, an approximation is used. See the + lcl_temperature function for further detail. + + After the parcel has reached the LCL, the lifter passed to the function + lowers the parcel to its initial pressure level along a moist adiabat or + pseudoadiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : float + The ambient pressure (Pa) + temperature : float + The ambient temperature (K) + dewpoint : float + The ambient dewpoint temperature (K) + + Returns + ------- + float + The wetbulb temperature (K) + """ + @overload def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ @@ -845,6 +909,70 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDA 1D NumPy array of wetbulb temperatures (K) """ +@overload +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Compute the wet bulb temperature (K) given the ambient pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, and + dewpoint temperature to its Lifted Condensation Level (LCL). To compute the + temperature and pressure of the LCL, an approximation is used. See the + lcl_temperature function for further detail. + + After the parcel has reached the LCL, the lifter passed to the function + lowers the parcel to its initial pressure level along a moist adiabat or + pseudoadiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatureds (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) + + Returns + ------- + numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) + """ + +@overload +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Compute the wet bulb temperature (K) given the ambient pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, and + dewpoint temperature to its Lifted Condensation Level (LCL). To compute the + temperature and pressure of the LCL, an approximation is used. See the + lcl_temperature function for further detail. + + After the parcel has reached the LCL, the lifter passed to the function + lowers the parcel to its initial pressure level along a moist adiabat or + pseudoadiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatureds (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) + + Returns + ------- + numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) + """ + @overload def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: float, temperature: float, dewpoint: float) -> float: """ @@ -909,6 +1037,70 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, The wet-bulb potential temperature (K) """ +@overload +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: + """ + Compute the wet-bulb potential temperature (K) given the pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, + and dewpoint temperature to its Lifted Condensation Level (LCL). + To compute the temperature and pressure of the LCL, an approximation + is used. See the lcl_temperature fuction for further detail. + + After the parcel has reached the LCL, the lifted passed to the function + lowers the parcel to the standard parcel reference pressure level + (1000 hPa) along a moist adiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : float + the ambient air pressure (Pa) + temperature : float + the ambient air temperature (K) + dewpoint : float + the ambient dewpoint temperature + + Returns + ------- + float + The wet-bulb potential temperature (K) + """ + +@overload +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: + """ + Compute the wet-bulb potential temperature (K) given the pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, + and dewpoint temperature to its Lifted Condensation Level (LCL). + To compute the temperature and pressure of the LCL, an approximation + is used. See the lcl_temperature fuction for further detail. + + After the parcel has reached the LCL, the lifted passed to the function + lowers the parcel to the standard parcel reference pressure level + (1000 hPa) along a moist adiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : float + the ambient air pressure (Pa) + temperature : float + the ambient air temperature (K) + dewpoint : float + the ambient dewpoint temperature + + Returns + ------- + float + The wet-bulb potential temperature (K) + """ + @overload def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ @@ -973,6 +1165,70 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotat 1D NumPy array of wet-bulb potential temperatures (K) """ +@overload +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Compute the wet-bulb potential temperature (K) given the pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, + and dewpoint temperature to its Lifted Condensation Level (LCL). + To compute the temperature and pressure of the LCL, an approximation + is used. See the lcl_temperature fuction for further detail. + + After the parcel has reached the LCL, the lifted passed to the function + lowers the parcel to the standard parcel reference pressure level + (1000 hPa) along a moist adiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature + + Returns + ------- + numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) + """ + +@overload +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Compute the wet-bulb potential temperature (K) given the pressure (Pa), + temperature (K), and dewpoint temperature (K). + + First, it lifts a parcel with the given pressure, temperature, + and dewpoint temperature to its Lifted Condensation Level (LCL). + To compute the temperature and pressure of the LCL, an approximation + is used. See the lcl_temperature fuction for further detail. + + After the parcel has reached the LCL, the lifted passed to the function + lowers the parcel to the standard parcel reference pressure level + (1000 hPa) along a moist adiabat. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature + + Returns + ------- + numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) + """ + @overload def thetae(pressure: float, temperature: float, dewpoint: float) -> float: """ diff --git a/src/nanobind/parcel_bindings.h b/src/nanobind/parcel_bindings.h index 56590230..a4ffe0b1 100644 --- a/src/nanobind/parcel_bindings.h +++ b/src/nanobind/parcel_bindings.h @@ -44,6 +44,10 @@ specifications. .def_ro_static("lift_from_lcl", &sharp::lifter_wobus::lift_from_lcl, R"pbdoc( A static flag that helps the parcel lifting functions know where to lift from. +)pbdoc") + .def_ro_static("ma_type", &sharp::lifter_wobus::ma_type, + R"pbdoc( +A static flag for the type of solver this is )pbdoc") .def_rw("converge", &sharp::lifter_wobus::converge, R"pbdoc( The iterative convergence criteria (K) From 9a54d0f0f365644bb7f10117f06a4c94fb5ed77c Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 15:47:40 -0500 Subject: [PATCH 39/55] changed python bindings to reflect CPP api where a sincle call can provide either LUT/lifter_lut. Added missing and NaN data checks. Updated documentation. Added test for LUTs --- docs/reference/python_api/parcel.rst | 8 +- include/SHARPlib/parcel.h | 81 +++++---- src/nanobind/nwsspc/sharp/calc/params.pyi | 12 +- src/nanobind/nwsspc/sharp/calc/parcel.pyi | 207 ++++++++++++++++++++-- src/nanobind/nwsspc/sharp/calc/thermo.pyi | 16 +- src/nanobind/parcel_bindings.h | 117 +++++++++++- tests/python/test_parcel.py | 37 +++- 7 files changed, 414 insertions(+), 64 deletions(-) diff --git a/docs/reference/python_api/parcel.rst b/docs/reference/python_api/parcel.rst index cf6e5c44..77c3b4ee 100644 --- a/docs/reference/python_api/parcel.rst +++ b/docs/reference/python_api/parcel.rst @@ -23,9 +23,13 @@ SHARPlib supports configurable parcel ascent via parcel lifter 'functors'. These .. autofunction:: nwsspc.sharp.calc.parcel.lifter_cm1.__call__ .. autofunction:: nwsspc.sharp.calc.parcel.lifter_cm1.parcel_virtual_temperature -.. autoclass:: nwsspc.sharp.calc.parcel.lut_data +.. autofunction:: nwsspc.sharp.calc.parcel.lut_data +.. autoclass:: nwsspc.sharp.calc.parcel.lut_data_wobus +.. autoclass:: nwsspc.sharp.calc.parcel.lut_data_cm1 -.. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut +.. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut +.. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut_wobus +.. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut_cm1 .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_lut.lift_from_lcl .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.__call__ diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index be50f797..1f879966 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -633,6 +633,14 @@ struct lifter_lut { */ [[nodiscard]] inline float operator()(const float pres, const float tmpk, const float new_pres) { + if ((pres == MISSING) || (tmpk == MISSING) || (new_pres == MISSING)) { + return MISSING; + } + + if (std::isnan(pres) || std::isnan(tmpk) || std::isnan(new_pres)) { + return std::nanf(""); + } + if (m_use_lifter) { return m_lifter(pres, tmpk, new_pres); } @@ -674,24 +682,24 @@ struct lifter_lut { /** * \brief Enum that defines the lifted parcel level (LPL) of origin. * - * The SFC parcel is a surface-based parcel, where the parcel initial attributes - * are set to the surface pressure, temperature, and dewpoint. + * The SFC parcel is a surface-based parcel, where the parcel initial + * attributes are set to the surface pressure, temperature, and dewpoint. * - * The FCST parcel is a forecast-surface-based parcel, in which the afternoon - * surface temperature and dewpoint are estimated and set as the parcel starting - * values. + * The FCST parcel is a forecast-surface-based parcel, in which the + * afternoon surface temperature and dewpoint are estimated and set as the + * parcel starting values. * - * The MU parcel is the most unstable parcel, in which the parcel attributes are - * set to the pressure, temperature, and dewpoint of the maximum Theta-E level - * within the bottom 400 hPa of the profile. + * The MU parcel is the most unstable parcel, in which the parcel attributes + * are set to the pressure, temperature, and dewpoint of the maximum Theta-E + * level within the bottom 400 hPa of the profile. * - * The ML parcel is the mixed-layer parcel, in which the mean theta and water - * vapor mixing ratio within the lowest 100 hPa are used to estimate a boundary - * layer mean, and lifted from the surface. + * The ML parcel is the mixed-layer parcel, in which the mean theta and + * water vapor mixing ratio within the lowest 100 hPa are used to estimate a + * boundary layer mean, and lifted from the surface. * - * The USR parcel means that the parcel initial lifting attributes have already - * been set by the programmer or user, and there is no need for them to be - * set or modified. + * The USR parcel means that the parcel initial lifting attributes have + * already been set by the programmer or user, and there is no need for them + * to be set or modified. */ enum class LPL : int { /** @@ -724,7 +732,8 @@ enum class LPL : int { /** * \author Kelton Halbert - NWS Storm Prediction Center * - * \brief Data that defines a Parcel, its attributes, and derived quantities. + * \brief Data that defines a Parcel, its attributes, and derived + * quantities. * * Contains information about a Parcel's starting level and * thermodynamic attributes, as well as paramaters computed @@ -767,8 +776,8 @@ struct Parcel { float mpl_pressure = MISSING; /** - * \brief Parcel Convective Available Potential Energy (J/kg) between the - * LFC and EL + * \brief Parcel Convective Available Potential Energy (J/kg) between + * the LFC and EL */ float cape = 0.0; @@ -824,7 +833,8 @@ struct Parcel { PressureLayer dry_lyr = {this->pres, this->lcl_pressure}; PressureLayer sat_lyr = {this->lcl_pressure, pressure_arr[N - 1]}; - // The LayerIndex excludes the top and bottom for interpolation reasons + // The LayerIndex excludes the top and bottom for interpolation + // reasons const LayerIndex dry_idx = get_layer_index(dry_lyr, pressure_arr, N); const LayerIndex sat_idx = get_layer_index(sat_lyr, pressure_arr, N); @@ -864,19 +874,20 @@ struct Parcel { /** * \author Kelton Halbert - NWS Storm Prediction Center * - * \brief Find the LFC and EL that bounds the layer with the maximum CAPE + * \brief Find the LFC and EL that bounds the layer with the maximum + * CAPE * * Searches the buoyancy array for the LFC and EL combination that * results in the most CAPE in the given profile. The buoyancy array is - * typically computed by calling sharp::Parcel::lift_parcel. Once the LFC - * and EL are found, the values are set in sharp::Parcel::lfc_pres and - * sharp::Parcel::eql_pres. + * typically computed by calling sharp::Parcel::lift_parcel. Once the + * LFC and EL are found, the values are set in sharp::Parcel::lfc_pres + * and sharp::Parcel::eql_pres. * * The value of sharp::Parcel::eql_pres is sharp::MISSING if there there * is no qualifying level found within the data bounds (e.g. incomplete * data, or an EL above the available data). Any calls to - * sharp::Parcel::cape_cinh will still compute CAPE without the presence of - * an EL, using the best-available data. + * sharp::Parcel::cape_cinh will still compute CAPE without the presence + * of an EL, using the best-available data. * * \param pres_arr The pressure coordinate array (Pa) * \param hght_arr The height coordinate array (meters) @@ -907,7 +918,8 @@ struct Parcel { * - CAPE is 0 * - sharp::Parcel::eql_pressure is sharp::MISSING * - No valid MPL candidate is found within the profile. - * - In this scenario, it likely exceeds the top of the available data. + * - In this scenario, it likely exceeds the top of the available + * data. * * In addition to being returned, the result is stored inside of * sharp::Parcel::mpl_pressure. @@ -1003,11 +1015,13 @@ struct Parcel { * sharp::HeightLayer, compute and return a mixed-layer * Parcel. * - * \param mix_layer sharp::PressureLayer or sharp::HeightLayer + * \param mix_layer sharp::PressureLayer or + * sharp::HeightLayer * \param pressure Array of pressure (Pa) * \param height Array of height (meters) * \param pot_temperature Array of potential temperature (K) - * \param wv_mixratio Array of water vapor mixing ratio (unitless) + * \param wv_mixratio Array of water vapor mixing ratio + * (unitless) * \param N Length of arrays * \return sharp::Parcel with mixed-layer values */ @@ -1052,11 +1066,14 @@ struct Parcel { * \param temperature Array of temperature (K) * \param virtemp Array of virtual temperature (K) * \param dewpoint Array of dewpoint temperature (K) - * \param pcl_virtemp Writeable array for parcel lifting calcs (K) - * \param buoy_arr Writeable array for buoyancy calcs (m/s^2) + * \param pcl_virtemp Writeable array for parcel lifting calcs + * (K) + * \param buoy_arr Writeable array for buoyancy calcs + * (m/s^2) * \param N Length of arrays * \param search_layer sharp::PressureLayer or sharp::HeightLay - * \param lifter The parcel moist adiabatic ascent function + * \param lifter The parcel moist adiabatic ascent + * function * \return The most unstable sharp::Parcel within the search layer */ template @@ -1123,8 +1140,8 @@ struct DowndraftParcel { float cape = 0.0; /** - * \brief DowndraftParcel Convective Inhibition (J/kg) between the LFC and - * EL + * \brief DowndraftParcel Convective Inhibition (J/kg) between the LFC + * and EL */ float cinh = std::nanf(""); diff --git a/src/nanobind/nwsspc/sharp/calc/params.pyi b/src/nanobind/nwsspc/sharp/calc/params.pyi index aa9f2f38..8c4fa735 100644 --- a/src/nanobind/nwsspc/sharp/calc/params.pyi +++ b/src/nanobind/nwsspc/sharp/calc/params.pyi @@ -97,7 +97,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure """ @overload -def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: +def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: """ Computes the Effective Inflow Layer, or the layer of the atmosphere beliefed to be the primary source of inflow for supercell thunderstorms. @@ -141,7 +141,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure """ @overload -def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: +def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: """ Computes the Effective Inflow Layer, or the layer of the atmosphere beliefed to be the primary source of inflow for supercell thunderstorms. @@ -601,7 +601,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure """ @overload -def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: +def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: """ Computes the convective temperature by iteratively lifting parcels from the surface using a lowest 100 hPa mean mixing ratio and increasing @@ -631,7 +631,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure """ @overload -def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: +def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: """ Computes the convective temperature by iteratively lifting parcels from the surface using a lowest 100 hPa mean mixing ratio and increasing @@ -934,7 +934,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter """ @overload -def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: """ Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a @@ -987,7 +987,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter """ @overload -def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: """ Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a diff --git a/src/nanobind/nwsspc/sharp/calc/parcel.pyi b/src/nanobind/nwsspc/sharp/calc/parcel.pyi index f7050ee7..f90d6027 100644 --- a/src/nanobind/nwsspc/sharp/calc/parcel.pyi +++ b/src/nanobind/nwsspc/sharp/calc/parcel.pyi @@ -182,7 +182,28 @@ class lifter_cm1: The virtual temperature of the parcel (K) """ -class lut_data: +class lut_data_wobus: + def __init__(self, lifter: lifter_wobus, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> None: ... + + @property + def pres_min(self) -> float: ... + + @property + def pres_max(self) -> float: ... + + @property + def thte_min(self) -> float: ... + + @property + def thte_max(self) -> float: ... + + @property + def num_logp(self) -> int: ... + + @property + def num_thetae(self) -> int: ... + +class lut_data_cm1: def __init__(self, lifter: lifter_cm1, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> None: ... @property @@ -203,7 +224,143 @@ class lut_data: @property def num_thetae(self) -> int: ... -class lifter_lut: +@overload +def lut_data(lifter: lifter_wobus, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> lut_data_wobus: + """ + Constructs the lookup table (LUT) data for a parcel lifter. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_wobus + pmin : float + The minimum pressure of the lookup table (Pa) + pmax : float + The maximum pressure of the lookup table (Pa) + thte_min : float + The minimum thetae of the lookup table (K) + thte_max : float + The maximum thetae of the lookup table (K) + n_logp : uint + The number of logp levels for the lookup table + n_thetae : uint + The number of thetae levels for the lookup table + + Returns + ------- + nwsspc.sharp.calc.parcel.lut_data_wobus + A lookup table for nwsspc.sharp.calc.parcel.lifter_wobus + """ + +@overload +def lut_data(lifter: lifter_cm1, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> lut_data_cm1: + """ + Constructs the lookup table (LUT) data for a parcel lifter. + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + pmin : float + The minimum pressure of the lookup table (Pa) + pmax : float + The maximum pressure of the lookup table (Pa) + thte_min : float + The minimum thetae of the lookup table (K) + thte_max : float + The maximum thetae of the lookup table (K) + n_logp : uint + The number of logp levels for the lookup table + n_thetae : uint + The number of thetae levels for the lookup table + + Returns + ------- + nwsspc.sharp.calc.parcel.lut_data_wobus + A lookup table for nwsspc.sharp.calc.parcel.lifter_cm1 + """ + +class lifter_lut_wobus: + """ + A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) + for fast moist adiabatic ascent calculations. + + Instead of directly solving the moist ascent ODEs, this lifter uses + bilinear interpolation of a precomputed lookup table to determine the + parcel temperature. If the parcel's LCL falls outside the table bounds, + it falls back to the direct Wobus solver automatically. + + LUT based parcel ascent only works for pseudoadiabats. Constructing the + LUT with a reversible adiabat type will result in an error being thrown. + + Parameters + ---------- + data : nwsspc.sharp.calc.parcel.lut_data + A shared lookup table constructed with a lifter_wobus instance. + """ + + def __init__(self, data: lut_data_wobus) -> None: ... + + lift_from_lcl: bool = ... + """ + A static flag that helps the parcel lifting functions know where to lift from. + """ + + def setup(self, lcl_pres: float, lcl_tmpk: float) -> None: + """ + Performs a setup step based on the LCL attributes. + + Computes the fractional index needed to select the correct pseudoadiabat + for lookup. If the LCL is outside the table bounds, it falls back to the + direct solver. + + Parameters + ---------- + lcl_pres : float + The LCL pressure (Pa) + lcl_tmpk : float + The LCL temperature (K) + + Returns + ------- + None + """ + + def __call__(self, pres: float, tmpk: float, new_pres: float) -> float: + """ + Performs LUT interpolation to lift a parcel moist adiabatically. + + Parameters + ---------- + pres : float + Parcel pressure (Pa) + tmpk : float + Parcel temperature (K) + new_pres : float + Final level of parcel after lift (Pa) + + Returns + ------- + float + The temperature of the lifted parcel (K) + """ + + def parcel_virtual_temperature(self, pres: float, tmpk: float) -> float: + """ + Computes the virtual temperature of the parcel (after saturation). + + Parameters + ---------- + pres : float + Parcel pressure (Pa) + tmpk : float + Parcel temperature (K) + + Returns + ------- + float + The virtual temperature of the parcel (K) + """ + +class lifter_lut_cm1: """ A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) for fast moist adiabatic ascent calculations. @@ -222,7 +379,7 @@ class lifter_lut: A shared lookup table constructed with a lifter_cm1 instance. """ - def __init__(self, data: lut_data) -> None: ... + def __init__(self, data: lut_data_cm1) -> None: ... lift_from_lcl: bool = ... """ @@ -288,6 +445,34 @@ class lifter_lut: The virtual temperature of the parcel (K) """ +@overload +def lifter_lut(lut: lut_data_wobus) -> lifter_lut_wobus: + """ + Constructs the parcel lifter from a LUT. + + Parameters + ---------- + lut : nwsspc.sharp.calc.parcel.lut_data_wobus + + Returns + ------- + nwsspc.sharp.calc.parcel.lifter_lut_wobus + """ + +@overload +def lifter_lut(lut: lut_data_cm1) -> lifter_lut_cm1: + """ + Constructs the parcel lifter from a LUT. + + Parameters + ---------- + lut : nwsspc.sharp.calc.parcel.lut_data_cm1 + + Returns + ------- + nwsspc.sharp.calc.parcel.lifter_lut_cm1 + """ + class LPL(enum.Enum): SFC = 1 """A Surface Based Parcel""" @@ -441,7 +626,7 @@ class Parcel: """ @overload - def lift_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + def lift_parcel(self, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Lifts a Parcel dry adiabatically from its LPL to its LCL dry adiabatically, and then moist adiabatically from the LCL to @@ -463,7 +648,7 @@ class Parcel: """ @overload - def lift_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + def lift_parcel(self, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Lifts a Parcel dry adiabatically from its LPL to its LCL dry adiabatically, and then moist adiabatically from the LCL to @@ -808,7 +993,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -839,7 +1024,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -870,7 +1055,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -901,7 +1086,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -1039,7 +1224,7 @@ class DowndraftParcel: """ @overload - def lower_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + def lower_parcel(self, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist adiabatically from its LPL to the surface. The moist adiabat used @@ -1063,7 +1248,7 @@ class DowndraftParcel: """ @overload - def lower_parcel(self, lifter: lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + def lower_parcel(self, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist adiabatically from its LPL to the surface. The moist adiabat used diff --git a/src/nanobind/nwsspc/sharp/calc/thermo.pyi b/src/nanobind/nwsspc/sharp/calc/thermo.pyi index 909c8ae4..8a0a1d01 100644 --- a/src/nanobind/nwsspc/sharp/calc/thermo.pyi +++ b/src/nanobind/nwsspc/sharp/calc/thermo.pyi @@ -782,7 +782,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temper """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -814,7 +814,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temper """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -910,7 +910,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDA """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -942,7 +942,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDA """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1038,7 +1038,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1070,7 +1070,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: float, temperature: float, dewpoint: float) -> float: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1166,7 +1166,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotat """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1198,7 +1198,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotat """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). diff --git a/src/nanobind/parcel_bindings.h b/src/nanobind/parcel_bindings.h index a4ffe0b1..87e49661 100644 --- a/src/nanobind/parcel_bindings.h +++ b/src/nanobind/parcel_bindings.h @@ -173,7 +173,7 @@ float The virtual temperature of the parcel (K) )pbdoc"); - nb::class_>(m_parcel, "lut_data") + nb::class_>(m_parcel, "lut_data_wobus") .def(nb::init(), nb::arg("lifter"), nb::arg("pmin") = 5000.0f, @@ -188,7 +188,7 @@ float .def_ro("num_thetae", &sharp::lut_data::num_thetae); - nb::class_>(m_parcel, "lut_data") + nb::class_>(m_parcel, "lut_data_cm1") .def(nb::init(), nb::arg("lifter"), nb::arg("pmin") = 5000.0f, @@ -202,7 +202,80 @@ float .def_ro("num_logp", &sharp::lut_data::num_logp) .def_ro("num_thetae", &sharp::lut_data::num_thetae); - nb::class_>(m_parcel, "lifter_lut", + m_parcel.def( + "lut_data", + [](sharp::lifter_wobus& lifter, float pmin, float pmax, float thte_min, + float thte_max, std::size_t n_logp, std::size_t n_thetae) { + return sharp::lut_data(lifter, pmin, pmax, thte_min, thte_max, + n_logp, n_thetae); + }, + nb::arg("lifter"), nb::arg("pmin") = 5000.0f, + nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, + nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, + nb::arg("n_thetae") = 221, + R"pbdoc( +Constructs the lookup table (LUT) data for a parcel lifter. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_wobus +pmin : float + The minimum pressure of the lookup table (Pa) +pmax : float + The maximum pressure of the lookup table (Pa) +thte_min : float + The minimum thetae of the lookup table (K) +thte_max : float + The maximum thetae of the lookup table (K) +n_logp : uint + The number of logp levels for the lookup table +n_thetae : uint + The number of thetae levels for the lookup table + +Returns +------- +nwsspc.sharp.calc.parcel.lut_data_wobus + A lookup table for nwsspc.sharp.calc.parcel.lifter_wobus +)pbdoc"); + + m_parcel.def( + "lut_data", + [](sharp::lifter_cm1& lifter, float pmin, float pmax, float thte_min, + float thte_max, std::size_t n_logp, std::size_t n_thetae) { + return sharp::lut_data(lifter, pmin, pmax, thte_min, thte_max, + n_logp, n_thetae); + }, + nb::arg("lifter"), nb::arg("pmin") = 5000.0f, + nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, + nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, + nb::arg("n_thetae") = 221, + R"pbdoc( +Constructs the lookup table (LUT) data for a parcel lifter. + +Parameters +---------- +lifter : nwsspc.sharp.calc.parcel.lifter_cm1 +pmin : float + The minimum pressure of the lookup table (Pa) +pmax : float + The maximum pressure of the lookup table (Pa) +thte_min : float + The minimum thetae of the lookup table (K) +thte_max : float + The maximum thetae of the lookup table (K) +n_logp : uint + The number of logp levels for the lookup table +n_thetae : uint + The number of thetae levels for the lookup table + +Returns +------- +nwsspc.sharp.calc.parcel.lut_data_wobus + A lookup table for nwsspc.sharp.calc.parcel.lifter_cm1 +)pbdoc"); + + nb::class_>(m_parcel, + "lifter_lut_wobus", R"pbdoc( A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) for fast moist adiabatic ascent calculations. @@ -285,7 +358,7 @@ float The virtual temperature of the parcel (K) )pbdoc"); - nb::class_>(m_parcel, "lifter_lut", + nb::class_>(m_parcel, "lifter_lut_cm1", R"pbdoc( A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) for fast moist adiabatic ascent calculations. @@ -370,6 +443,42 @@ float The virtual temperature of the parcel (K) )pbdoc"); + m_parcel.def( + "lifter_lut", + [](std::shared_ptr>& lut) { + return sharp::lifter_lut(std::move(lut)); + }, + nb::arg("lut"), + R"pbdoc( +Constructs the parcel lifter from a LUT. + +Parameters +---------- +lut : nwsspc.sharp.calc.parcel.lut_data_wobus + +Returns +------- +nwsspc.sharp.calc.parcel.lifter_lut_wobus +)pbdoc"); + + m_parcel.def( + "lifter_lut", + [](std::shared_ptr>& lut) { + return sharp::lifter_lut(std::move(lut)); + }, + nb::arg("lut"), + R"pbdoc( +Constructs the parcel lifter from a LUT. + +Parameters +---------- +lut : nwsspc.sharp.calc.parcel.lut_data_cm1 + +Returns +------- +nwsspc.sharp.calc.parcel.lifter_lut_cm1 +)pbdoc"); + nb::enum_(m_parcel, "LPL") .value("SFC", sharp::LPL::SFC, "A Surface Based Parcel") .value("FCST", sharp::LPL::FCST, "A Forecast Surface Parcel") diff --git a/tests/python/test_parcel.py b/tests/python/test_parcel.py index eb0084cf..e16deadc 100644 --- a/tests/python/test_parcel.py +++ b/tests/python/test_parcel.py @@ -82,11 +82,11 @@ def test_lifter_wobus(): def test_lifter_cm1(): lifter = parcel.lifter_cm1() lifter.ma_type = thermo.adiabat.pseudo_liq - lifter.converge = 100.0 pres = 100000.0 tmpk = 320.0 new_pres = 50000.0 + lifter.setup(pres, tmpk) assert (lifter(constants.MISSING, tmpk, new_pres) == constants.MISSING) assert (lifter(pres, constants.MISSING, new_pres) == constants.MISSING) @@ -95,6 +95,41 @@ def test_lifter_cm1(): assert (np.isnan(lifter(np.nan, tmpk, new_pres))) assert (np.isnan(lifter(pres, np.nan, new_pres))) + assert (lifter(pres, tmpk, new_pres) == pytest.approx(302.0066833496094)) + +def test_lifter_lut(): + lifter_wobf = parcel.lifter_wobus() + lifter_cm1 = parcel.lifter_cm1() + lifter_cm1.ma_type = thermo.adiabat.pseudo_liq + + lut_wobf = parcel.lut_data(lifter_wobf) + lut_cm1 = parcel.lut_data(lifter_cm1) + + lifter_lut1 = parcel.lifter_lut(lut_wobf) + lifter_lut2 = parcel.lifter_lut(lut_cm1) + + pres = 100000.0 + tmpk = 320.0 + new_pres = 50000.0 + + lifter_lut1.setup(pres, tmpk) + lifter_lut2.setup(pres, tmpk) + + assert (lifter_lut1(constants.MISSING, tmpk, new_pres) == constants.MISSING) + assert (lifter_lut1(pres, constants.MISSING, new_pres) == constants.MISSING) + assert (lifter_lut1(pres, tmpk, constants.MISSING) == constants.MISSING) + + assert (np.isnan(lifter_lut1(np.nan, tmpk, new_pres))) + assert (np.isnan(lifter_lut1(pres, np.nan, new_pres))) + assert (lifter_lut1(pres, tmpk, new_pres) == pytest.approx(301.872528)) + + assert (lifter_lut2(constants.MISSING, tmpk, new_pres) == constants.MISSING) + assert (lifter_lut2(pres, constants.MISSING, new_pres) == constants.MISSING) + assert (lifter_lut2(pres, tmpk, constants.MISSING) == constants.MISSING) + + assert (np.isnan(lifter_lut2(np.nan, tmpk, new_pres))) + assert (np.isnan(lifter_lut2(pres, np.nan, new_pres))) + assert (lifter_lut2(pres, tmpk, new_pres) == pytest.approx(302.0087890625)) def test_surface_parcel(): pres = snd_data["pres"][0] From 8e03a687664a41f6212ec0209dd8ee0756746b84 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 15:54:39 -0500 Subject: [PATCH 40/55] added a LUT call to sb_parcel test --- tests/python/test_parcel.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/python/test_parcel.py b/tests/python/test_parcel.py index e16deadc..56d17606 100644 --- a/tests/python/test_parcel.py +++ b/tests/python/test_parcel.py @@ -154,6 +154,7 @@ def test_surface_parcel(): assert (pcl.mpl_pressure == pytest.approx(9065, abs=1e-0)) lifter = parcel.lifter_cm1() + lifter.converge = 0.001 lifter.ma_type = thermo.adiabat.pseudo_liq vtmpk = pcl.lift_parcel(lifter, snd_data["pres"]) buoy = thermo.buoyancy(vtmpk, snd_data["vtmp"]) @@ -168,6 +169,21 @@ def test_surface_parcel(): assert (pcl.eql_pressure == pytest.approx(18969, abs=1e-0)) assert (pcl.mpl_pressure == pytest.approx(9529, abs=1e-0)) + lut_cm1 = parcel.lut_data(lifter) + lifter = parcel.lifter_lut(lut_cm1) + vtmpk = pcl.lift_parcel(lifter, snd_data["pres"]) + buoy = thermo.buoyancy(vtmpk, snd_data["vtmp"]) + cape, cinh = pcl.cape_cinh(snd_data["pres"], snd_data["hght"], buoy) + mpl = pcl.maximum_parcel_level(snd_data["pres"], snd_data["hght"], buoy) + li = pcl.lifted_index(50000.0, snd_data["pres"], snd_data["vtmp"], vtmpk) + + assert (li == pytest.approx(-9.12, abs=1e-2)) + assert (cape == pytest.approx(3111.147216, abs=5e-1)) + assert (cinh == pytest.approx(-36.4, abs=5e-1)) + assert (pcl.lfc_pressure == pytest.approx(71482, abs=1e-0)) + assert (pcl.eql_pressure == pytest.approx(18933, abs=1e-0)) + assert (pcl.mpl_pressure == pytest.approx(9529, abs=1e-0)) + def test_mixed_layer_parcel(): mix_lyr = layer.PressureLayer( From 122a1617e05d6ee599106cfe387b1ebc394288c3 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:18:10 -0500 Subject: [PATCH 41/55] removed fmt from parcel.h --- include/SHARPlib/parcel.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index 1f879966..a1a3e50a 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -529,7 +528,6 @@ struct lut_data { tmpk_hi = tmpk_mid; } } - fmt::println("Failed to converge on a LCL temperature for thetae."); return tmpk_mid; } }; From 9c6bae4f74e105e29ea713183dbeb713ca17c3fe Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:18:30 -0500 Subject: [PATCH 42/55] fixed include format --- tests/unit/test_parcel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_parcel.cpp b/tests/unit/test_parcel.cpp index 5b11546e..c655cded 100644 --- a/tests/unit/test_parcel.cpp +++ b/tests/unit/test_parcel.cpp @@ -1,4 +1,4 @@ -#include "SHARPlib/thermo.h" +#include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include From 219ea2f57a30fc2a3fc16db53047c7ec1108d25a Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:19:06 -0500 Subject: [PATCH 43/55] fixed include format --- tests/unit/test_parcel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_parcel.cpp b/tests/unit/test_parcel.cpp index c655cded..37540647 100644 --- a/tests/unit/test_parcel.cpp +++ b/tests/unit/test_parcel.cpp @@ -1,9 +1,9 @@ -#include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include #include +#include #include #include From 895a38ea1827b3282e2c381e7f10da37b3c1fbe2 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:30:21 -0500 Subject: [PATCH 44/55] handle NaN case --- src/SHARPlib/thermo.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SHARPlib/thermo.cpp b/src/SHARPlib/thermo.cpp index 0e630964..eb42b476 100644 --- a/src/SHARPlib/thermo.cpp +++ b/src/SHARPlib/thermo.cpp @@ -319,6 +319,10 @@ float moist_adiabat_cm1(float pressure, float temperature, float new_pressure, (new_pressure == MISSING)) { return MISSING; } + if (std::isnan(temperature) || std::isnan(pressure) || + std::isnan(new_pressure)) { + return MISSING; + } #endif // set up solver variables From 6c7d5d436f24e5ccbbbb8d5989c0fe8e5bdb0ed0 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:31:31 -0500 Subject: [PATCH 45/55] reset the state between calls --- tests/python/test_parcel.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/python/test_parcel.py b/tests/python/test_parcel.py index 56d17606..2b5a041e 100644 --- a/tests/python/test_parcel.py +++ b/tests/python/test_parcel.py @@ -86,15 +86,23 @@ def test_lifter_cm1(): pres = 100000.0 tmpk = 320.0 new_pres = 50000.0 - lifter.setup(pres, tmpk) + lifter.setup(pres, tmpk) assert (lifter(constants.MISSING, tmpk, new_pres) == constants.MISSING) + + lifter.setup(pres, tmpk) assert (lifter(pres, constants.MISSING, new_pres) == constants.MISSING) + + lifter.setup(pres, tmpk) assert (lifter(pres, tmpk, constants.MISSING) == constants.MISSING) + lifter.setup(pres, tmpk) assert (np.isnan(lifter(np.nan, tmpk, new_pres))) + + lifter.setup(pres, tmpk) assert (np.isnan(lifter(pres, np.nan, new_pres))) + lifter.setup(pres, tmpk) assert (lifter(pres, tmpk, new_pres) == pytest.approx(302.0066833496094)) def test_lifter_lut(): From d599c27a1494666b04f41ea729fc5e3853e8688b Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:33:27 -0500 Subject: [PATCH 46/55] nan should return nan --- src/SHARPlib/thermo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SHARPlib/thermo.cpp b/src/SHARPlib/thermo.cpp index eb42b476..8e421e7b 100644 --- a/src/SHARPlib/thermo.cpp +++ b/src/SHARPlib/thermo.cpp @@ -321,7 +321,7 @@ float moist_adiabat_cm1(float pressure, float temperature, float new_pressure, } if (std::isnan(temperature) || std::isnan(pressure) || std::isnan(new_pressure)) { - return MISSING; + return std::nanf(""); } #endif From b47eff13e28720ea592ffe56d72c71c972c5678b Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Thu, 26 Mar 2026 16:35:59 -0500 Subject: [PATCH 47/55] make tests a little less restrictive --- tests/python/test_parcel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/python/test_parcel.py b/tests/python/test_parcel.py index 2b5a041e..d33a985c 100644 --- a/tests/python/test_parcel.py +++ b/tests/python/test_parcel.py @@ -103,7 +103,7 @@ def test_lifter_cm1(): assert (np.isnan(lifter(pres, np.nan, new_pres))) lifter.setup(pres, tmpk) - assert (lifter(pres, tmpk, new_pres) == pytest.approx(302.0066833496094)) + assert (lifter(pres, tmpk, new_pres) == pytest.approx(302.0066833496094, abs=1e-1)) def test_lifter_lut(): lifter_wobf = parcel.lifter_wobus() @@ -137,7 +137,7 @@ def test_lifter_lut(): assert (np.isnan(lifter_lut2(np.nan, tmpk, new_pres))) assert (np.isnan(lifter_lut2(pres, np.nan, new_pres))) - assert (lifter_lut2(pres, tmpk, new_pres) == pytest.approx(302.0087890625)) + assert (lifter_lut2(pres, tmpk, new_pres) == pytest.approx(302.0087890625, abs=1e-1)) def test_surface_parcel(): pres = snd_data["pres"][0] From 703bc154c1a2b749d5a9065e3bda4f25c8565e3a Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Fri, 27 Mar 2026 12:09:08 -0500 Subject: [PATCH 48/55] fix documentation --- docs/reference/python_api/parcel.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/reference/python_api/parcel.rst b/docs/reference/python_api/parcel.rst index 77c3b4ee..bb43c91e 100644 --- a/docs/reference/python_api/parcel.rst +++ b/docs/reference/python_api/parcel.rst @@ -26,14 +26,18 @@ SHARPlib supports configurable parcel ascent via parcel lifter 'functors'. These .. autofunction:: nwsspc.sharp.calc.parcel.lut_data .. autoclass:: nwsspc.sharp.calc.parcel.lut_data_wobus .. autoclass:: nwsspc.sharp.calc.parcel.lut_data_cm1 - .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut .. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut_wobus + + .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_lut_wobus.lift_from_lcl + .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut_wobus.__call__ + .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut_wobus.parcel_virtual_temperature + .. autoclass:: nwsspc.sharp.calc.parcel.lifter_lut_cm1 - .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_lut.lift_from_lcl - .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.__call__ - .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut.parcel_virtual_temperature + .. autoproperty:: nwsspc.sharp.calc.parcel.lifter_lut_cm1.lift_from_lcl + .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut_cm1.__call__ + .. autofunction:: nwsspc.sharp.calc.parcel.lifter_lut_cm1.parcel_virtual_temperature Parcels ------- From cc487f32758af8bbbb81addc135575239cf74311 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 31 Mar 2026 15:20:43 -0500 Subject: [PATCH 49/55] Refactor nanobind bindings (#102, #107, #108) This PR implements a major refactor of how the nanobind python bindings are generated. With the introduction of the parcel ascent lookup tables, the amount of explicit instantiations for various functions increased dramatically... and would have a much larger increase per additional lifter added in the future. So, it was prudent and worthwhile to look at refactoring the nanobind python bindings. The primary goal was to leverage the strong recurring patterns that occur in the binding functions to generalize code. A new header file, binding_utils.h, is the destination for template metaprogramming code that leverages the compiler to generate bindings based on known structures. This approach has reduced duplicate code, simplified the process for adding new bindings in the future, and better standardized the bindings to be more consistent. It has also had the effect of helping reduce duplicate documentation and documentation errors. --- include/SHARPlib/winds.h | 11 +- src/nanobind/binding_utils.h | 56 + src/nanobind/constants_bindings.h | 2 +- src/nanobind/interp_bindings.h | 15 +- src/nanobind/layer_bindings.h | 365 ++-- src/nanobind/nwsspc/sharp/calc/interp.pyi | 2 +- src/nanobind/nwsspc/sharp/calc/layer.pyi | 49 +- src/nanobind/nwsspc/sharp/calc/params.pyi | 69 +- src/nanobind/nwsspc/sharp/calc/parcel.pyi | 543 +++--- src/nanobind/nwsspc/sharp/calc/thermo.pyi | 220 +-- src/nanobind/nwsspc/sharp/calc/winds.pyi | 56 +- src/nanobind/params_bindings.h | 995 ++--------- src/nanobind/parcel_bindings.h | 1968 +++++++-------------- src/nanobind/thermo_bindings.h | 1346 ++++---------- src/nanobind/winds_bindings.h | 414 ++--- tests/python/test_parcel.py | 21 + 16 files changed, 2069 insertions(+), 4063 deletions(-) create mode 100644 src/nanobind/binding_utils.h diff --git a/include/SHARPlib/winds.h b/include/SHARPlib/winds.h index 97b8ed38..da28424f 100644 --- a/include/SHARPlib/winds.h +++ b/include/SHARPlib/winds.h @@ -244,7 +244,7 @@ struct WindComponents { * sharp::HeightLayer, returning the vector components. * * \param layer {bottom, top} - * \param coordinate (pressure (Pa) or height (m)) + * \param coordinate (pressure (Pa) or height (m AGL)) * \param u_wind (m/s) * \param v_wind (m/s) * \param N (length of arrays) @@ -261,11 +261,20 @@ template float u_bot, u_top; float v_bot, v_top; if constexpr (layer.coord == LayerCoordinate::pressure) { + if (layer.bottom > coordinate[0]) layer.bottom = coordinate[0]; + if (layer.top < coordinate[N - 1]) layer.top = coordinate[N - 1]; + u_bot = interp_pressure(layer.bottom, coordinate, u_wind, N); v_bot = interp_pressure(layer.bottom, coordinate, v_wind, N); u_top = interp_pressure(layer.top, coordinate, u_wind, N); v_top = interp_pressure(layer.top, coordinate, v_wind, N); } else { + layer.bottom += coordinate[0]; + layer.top += coordinate[0]; + + if (layer.bottom < coordinate[0]) layer.bottom = coordinate[0]; + if (layer.top > coordinate[N - 1]) layer.top = coordinate[N - 1]; + u_bot = interp_height(layer.bottom, coordinate, u_wind, N); v_bot = interp_height(layer.bottom, coordinate, v_wind, N); u_top = interp_height(layer.top, coordinate, u_wind, N); diff --git a/src/nanobind/binding_utils.h b/src/nanobind/binding_utils.h new file mode 100644 index 00000000..67e75850 --- /dev/null +++ b/src/nanobind/binding_utils.h @@ -0,0 +1,56 @@ +#ifndef SHARPLIB_BINDING_UTILS_H +#define SHARPLIB_BINDING_UTILS_H + +#include + +#include + +#include "sharplib_types.h" + +namespace nb = nanobind; + +template +struct type_tag { + using type = T; +}; + +template +void check_equal_sizes(const First& first, const Rest&... rest) { + if (((first.size() != rest.size()) || ...)) { + throw nb::buffer_error("All input arrays must have the same size!"); + } +} + +template +out_arr_t make_output_array(std::size_t NZ, Func&& compute) { + auto buf = std::make_unique(NZ); + compute(buf.get()); + float* raw = buf.release(); + nb::capsule owner(raw, [](void* p) noexcept { delete[] (float*)p; }); + return out_arr_t(raw, {NZ}, owner); +} + +template +void for_each_base_lifter(F&& f) { + f(type_tag{}, "wobus", "Wobus", + "nwsspc.sharp.calc.parcel.lifter_wobus", + "nwsspc.sharp.calc.parcel.lut_data_wobus", + "nwsspc.sharp.calc.parcel.lifter_lut_wobus"); + f(type_tag{}, "cm1", "CM1", + "nwsspc.sharp.calc.parcel.lifter_cm1", + "nwsspc.sharp.calc.parcel.lut_data_cm1", + "nwsspc.sharp.calc.parcel.lifter_lut_cm1"); +} + +template +void for_each_lifter(F&& f) { + for_each_base_lifter([&](auto tag, const char* suffix, const char*, + const char* lifter_fqn, const char*, + const char* lifter_lut_fqn) { + using Lft = typename decltype(tag)::type; + f(type_tag{}, lifter_fqn); + f(type_tag>{}, lifter_lut_fqn); + }); +} + +#endif // SHARPLIB_BINDING_UTILS_H diff --git a/src/nanobind/constants_bindings.h b/src/nanobind/constants_bindings.h index 979937a8..88baa3ec 100644 --- a/src/nanobind/constants_bindings.h +++ b/src/nanobind/constants_bindings.h @@ -9,7 +9,7 @@ namespace nb = nanobind; -void make_constants_bindings(nb::module_ m) { +inline void make_constants_bindings(nb::module_ m) { nb::module_ m_const = m.def_submodule("constants", "Sounding and Hodograph Analysis and Research Program " diff --git a/src/nanobind/interp_bindings.h b/src/nanobind/interp_bindings.h index cccca7db..1b0776f9 100644 --- a/src/nanobind/interp_bindings.h +++ b/src/nanobind/interp_bindings.h @@ -7,6 +7,7 @@ // clang-format on #include +#include "binding_utils.h" #include "sharplib_types.h" namespace nb = nanobind; @@ -21,10 +22,7 @@ inline void make_interp_bindings(nb::module_ m) { "interp_height", [](const float hght_val, const_prof_arr_t hght_arr, const_prof_arr_t data_arr) -> float { - if ((hght_arr.size() != data_arr.size())) { - throw nb::buffer_error( - "hght_arr and data_arr must have the same size!"); - } + check_equal_sizes(hght_arr, data_arr); return sharp::interp_height(hght_val, hght_arr.data(), data_arr.data(), hght_arr.size()); }, @@ -52,10 +50,7 @@ float "interp_pressure", [](const float pres_val, const_prof_arr_t pres_arr, const_prof_arr_t data_arr) -> float { - if ((pres_arr.size() != data_arr.size())) { - throw nb::buffer_error( - "hght_arr and data_arr must have the same size!"); - } + check_equal_sizes(pres_arr, data_arr); return sharp::interp_pressure(pres_val, pres_arr.data(), data_arr.data(), pres_arr.size()); }, @@ -69,7 +64,7 @@ Parameters ---------- pres_val : float The coordinate pressure value to interpolate to (Pa) -pres_arr : nump.ndarray[dtype=float32] +pres_arr : numpy.ndarray[dtype=float32] 1D numpy array of pressure values to interpolate from (Pa) data_arr : numpy.ndarray[dtype=float32] 1D numpy array of data values to interpolate from @@ -84,6 +79,7 @@ float "find_first_pressure", [](float data_val, const_prof_arr_t pres_arr, const_prof_arr_t data_arr) { + check_equal_sizes(pres_arr, data_arr); return sharp::find_first_pressure(data_val, pres_arr.data(), data_arr.data(), pres_arr.size()); }, @@ -111,6 +107,7 @@ float "find_first_height", [](float data_val, const_prof_arr_t hght_arr, const_prof_arr_t data_arr) { + check_equal_sizes(hght_arr, data_arr); return sharp::find_first_height(data_val, hght_arr.data(), data_arr.data(), hght_arr.size()); }, diff --git a/src/nanobind/layer_bindings.h b/src/nanobind/layer_bindings.h index f342c130..21c04f5e 100644 --- a/src/nanobind/layer_bindings.h +++ b/src/nanobind/layer_bindings.h @@ -6,23 +6,109 @@ #include #include -#define FMT_HEADER_ONLY -#include - // clang-format on #include +#include +#include "binding_utils.h" #include "sharplib_types.h" namespace nb = nanobind; +template +void bind_get_layer_index(nb::module_& mod, const char* doc_template, + const char* layer_type, const char* coord_name) { + std::string doc = fmt::format(doc_template, layer_type, coord_name, + coord_name, layer_type); + mod.def( + "get_layer_index", + [](Layer& layer, const_prof_arr_t coord) -> sharp::LayerIndex { + return sharp::get_layer_index(layer, coord.data(), coord.size()); + }, + nb::arg("layer"), nb::arg(coord_name), doc.c_str()); +} + +template +void bind_layer_min(nb::module_& mod, const char* doc_template, + const char* layer_type, const char* coord_name) { + std::string doc = fmt::format(doc_template, layer_type, coord_name); + mod.def( + "layer_min", + [](Layer& layer, const_prof_arr_t coord, const_prof_arr_t data) { + check_equal_sizes(coord, data); + float lvl_of_min; + float min = sharp::layer_min(layer, coord.data(), data.data(), + data.size(), &lvl_of_min); + return std::make_tuple(min, lvl_of_min); + }, + nb::arg("layer"), nb::arg(coord_name), nb::arg("data"), doc.c_str()); +} + +template +void bind_layer_max(nb::module_& mod, const char* doc_template, + const char* layer_type, const char* coord_name) { + std::string doc = fmt::format(doc_template, layer_type, coord_name); + mod.def( + "layer_max", + [](Layer& layer, const_prof_arr_t coord, const_prof_arr_t data) { + check_equal_sizes(coord, data); + float lvl_of_max; + float max = sharp::layer_max(layer, coord.data(), data.data(), + data.size(), &lvl_of_max); + return std::make_tuple(max, lvl_of_max); + }, + nb::arg("layer"), nb::arg(coord_name), nb::arg("data"), doc.c_str()); +} + +template +void bind_layer_mean(nb::module_& mod, const char* doc) { + if constexpr (Layer::coord == sharp::LayerCoordinate::height) { + mod.def( + "layer_mean", + [](Layer layer, const_prof_arr_t height, const_prof_arr_t pressure, + const_prof_arr_t data, const bool isAGL) { + check_equal_sizes(height, pressure, data); + return sharp::layer_mean(layer, height.data(), pressure.data(), + data.data(), data.size(), isAGL); + }, + nb::arg("layer"), nb::arg("height"), nb::arg("pressure"), + nb::arg("data"), nb::arg("isAGL") = false, doc); + } else { + mod.def( + "layer_mean", + [](Layer layer, const_prof_arr_t pressure, const_prof_arr_t data) { + check_equal_sizes(pressure, data); + return sharp::layer_mean(layer, pressure.data(), data.data(), + data.size()); + }, + nb::arg("layer"), nb::arg("pressure"), nb::arg("data"), doc); + } +} + +template +void bind_integrate_layer_trapz(nb::module_& mod, const char* doc_template, + const char* layer_type, + const char* coord_name) { + std::string doc = fmt::format(doc_template, layer_type, coord_name); + mod.def( + "integrate_layer_trapz", + [](Layer layer, const_prof_arr_t data, const_prof_arr_t coord, + const int integ_sign, const bool weighted) { + check_equal_sizes(coord, data); + return sharp::integrate_layer_trapz(layer, data.data(), + coord.data(), data.size(), + integ_sign, weighted); + }, + nb::arg("layer"), nb::arg("data"), nb::arg(coord_name), + nb::arg("integ_sign") = 0, nb::arg("weighted") = false, doc.c_str()); +} + inline void make_layer_bindings(nb::module_ m) { nb::module_ m_layer = m.def_submodule( "layer", "Sounding and Hodograph Analysis and Research Program Library " "(SHARPlib) :: Atmospheric Layer Routines"); - // Bind the constructors, named fields, and default arguments nb::class_(m_layer, "HeightLayer") .def(nb::init<>()) .def(nb::init(), nb::arg("bottom"), nb::arg("top"), @@ -68,16 +154,9 @@ inline void make_layer_bindings(nb::module_ m) { layer.ktop); }); - m_layer.def( - "get_layer_index", - [](sharp::PressureLayer& layer, - const_prof_arr_t pressure_array) -> sharp::LayerIndex { - return sharp::get_layer_index(layer, pressure_array.data(), - pressure_array.size()); - }, - nb::arg("layer"), nb::arg("pressure"), + const char* get_layer_index_template_doc = R"pbdoc( -Finds the interior array indices encapsulated by the given PressureLayer. +Finds the interior array indices encapsulated by the given {0}. More specifically, the returned LayerIndex excludes the exact top and bottom values (in mathematical notation, [bottom, top]). This behavior is due to the fact many algorithms use interpolation to get the exact values of arbitrary @@ -86,56 +165,30 @@ top/bottom locations within a profile. If layer.bottom or layer.top are out of bounds, this function will truncate the layer to the coordinate range of data provided by coord[] in an attempt to gracefully continue and produce a result. -This will modify the value of the given PressureLayer. +This will modify the value of the given {3}. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressures +layer : nwsspc.sharp.calc.layer.{0} +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {2} values Returns ------- nwsspc.sharp.calc.layer.LayerIndex - A LayerIndex with {kbot, ktop}. - )pbdoc"); - - m_layer.def( - "get_layer_index", - [](sharp::HeightLayer& layer, - const_prof_arr_t height_array) -> sharp::LayerIndex { - return sharp::get_layer_index(layer, height_array.data(), - height_array.size()); - }, - nb::arg("layer"), nb::arg("height"), - R"pbdoc( -Finds the interior array indices encapsulated by the given HeightLayer. -More specifically, the returned LayerIndex excludes the exact top and bottom -values (in mathematical notation, [bottom, top]). This behavior is due to the -fact many algorithms use interpolation to get the exact values of arbitrary -top/bottom locations within a profile. - -If layer.bottom or layer.top are out of bounds, this function -will truncate the layer to the coordinate range of data provided -by coord[] in an attempt to gracefully continue and produce a result. -This will modify the value of the given HeightLayer. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer -height : numpy.ndarray[dtype=float32] - 1D NumPy array of heights + A LayerIndex with {{kbot, ktop}}. +)pbdoc"; -Returns -------- -nwsspc.sharp.calc.layer.LayerIndex - A LayerIndex with {kbot, ktop}. - )pbdoc"); + bind_get_layer_index( + m_layer, get_layer_index_template_doc, "PressureLayer", "pressure"); + bind_get_layer_index( + m_layer, get_layer_index_template_doc, "HeightLayer", "height"); m_layer.def( "height_layer_to_pressure", [](sharp::HeightLayer& layer, const_prof_arr_t pressure, const_prof_arr_t height, const bool isAGL) -> sharp::PressureLayer { + check_equal_sizes(pressure, height); return sharp::height_layer_to_pressure( layer, pressure.data(), height.data(), height.size(), isAGL); }, @@ -161,13 +214,14 @@ isAGL : bool Returns ------- nwsspc.sharp.calc.layer.PressureLayer - - )pbdoc"); + The HeightLayer converted to a PressureLayer +)pbdoc"); m_layer.def( "pressure_layer_to_height", [](sharp::PressureLayer& layer, const_prof_arr_t pressure, const_prof_arr_t height, const bool toAGL) -> sharp::HeightLayer { + check_equal_sizes(pressure, height); return sharp::pressure_layer_to_height( layer, pressure.data(), height.data(), height.size(), toAGL); }, @@ -193,89 +247,43 @@ toAGL : bool Returns ------- nwsspc.sharp.calc.layer.HeightLayer + The PressureLayer converted to a HeightLayer +)pbdoc"); - )pbdoc"); - - m_layer.def( - "layer_min", - [](sharp::HeightLayer& layer, const_prof_arr_t height, - const_prof_arr_t data) { - float lvl_of_min; - float min = sharp::layer_min(layer, height.data(), data.data(), - data.size(), &lvl_of_min); - - return std::make_tuple(min, lvl_of_min); - }, - nb::arg("layer"), nb::arg("height"), nb::arg("data"), + const char* layer_min_template_doc = R"pbdoc( -Returns the minimum value of the data array within the given HeightLayer. The +Returns the minimum value of the data array within the given {0}. The function bounds checks the layer by calling get_layer_index. Parameters ---------- -layer : nwsspc.sharp.calc.layer.HeightLayer -height : numpy.ndarray[dtype=float32] - 1D NumPy array of heights +layer : nwsspc.sharp.calc.layer.{0} +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {1} values data : numpy.ndarray[dtype=float32] - 1D array of data + 1D NumPy array of data values Returns ------- tuple[float, float] (min_value, level_of_min) +)pbdoc"; - )pbdoc"); - - m_layer.def( - "layer_min", - [](sharp::PressureLayer& layer, const_prof_arr_t pressure, - const_prof_arr_t data) { - float lvl_of_min; - float min = sharp::layer_min(layer, pressure.data(), data.data(), - data.size(), &lvl_of_min); + bind_layer_min(m_layer, layer_min_template_doc, + "HeightLayer", "height"); + bind_layer_min(m_layer, layer_min_template_doc, + "PressureLayer", "pressure"); - return std::make_tuple(min, lvl_of_min); - }, - nb::arg("layer"), nb::arg("pressure"), nb::arg("data"), + const char* layer_max_template_doc = R"pbdoc( -Returns the minimum value of the data array within the given PressureLayer. The +Returns the maximum value of the data array within the given {0}. The function bounds checks the layer by calling get_layer_index. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure -data : numpy.ndarray[dtype=float32] - 1D array of data - -Returns -------- -tuple[float, float] - (min_value, level_of_min) - )pbdoc"); - - m_layer.def( - "layer_max", - [](sharp::HeightLayer& layer, const_prof_arr_t height, - const_prof_arr_t data) { - float lvl_of_max; - float max = sharp::layer_max(layer, height.data(), data.data(), - data.size(), &lvl_of_max); - - return std::make_tuple(max, lvl_of_max); - }, - nb::arg("layer"), nb::arg("height"), nb::arg("data"), - R"pbdoc( -Returns the maximum value observed within the given data array over -the given HeightLayer. The function bounds checks the layer by calling -get_layer_index. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values +layer : nwsspc.sharp.calc.layer.{0} +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {1} values data : numpy.ndarray[dtype=float32] 1D NumPy array of data values @@ -283,47 +291,14 @@ Returns ------- tuple[float, float] (max_value, level_of_max) +)pbdoc"; - )pbdoc"); - - m_layer.def( - "layer_max", - [](sharp::PressureLayer& layer, const_prof_arr_t pressure, - const_prof_arr_t data) { - float lvl_of_max; - float max = sharp::layer_max(layer, pressure.data(), data.data(), - data.size(), &lvl_of_max); + bind_layer_max(m_layer, layer_max_template_doc, + "HeightLayer", "height"); + bind_layer_max(m_layer, layer_max_template_doc, + "PressureLayer", "pressure"); - return std::make_tuple(max, lvl_of_max); - }, - nb::arg("layer"), nb::arg("pressure"), nb::arg("data"), - R"pbdoc( -Returns the maximum value observed within the given data array over the given -PressureLayer. The function bounds checks the layer by calling get_layer_index. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.PressureLayer -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values -data : numpy.ndarray[dtype=float32] - 1D NumPy array of data values - -Returns -------- -tuple[float, float] - (max_value, level_of_max) - - )pbdoc"); - - m_layer.def( - "layer_mean", - [](sharp::PressureLayer layer, const_prof_arr_t pressure, - const_prof_arr_t data) { - return sharp::layer_mean(layer, pressure.data(), data.data(), - data.size()); - }, - nb::arg("PressureLayer"), nb::arg("pressure"), nb::arg("data"), + const char* layer_mean_pres_doc = R"pbdoc( Computes the pressure-weighted mean value of a field over a given PressureLayer. @@ -339,18 +314,10 @@ data : numpy.ndarray[dtype=float32] Returns ------- float + The layer mean +)pbdoc"; - )pbdoc"); - - m_layer.def( - "layer_mean", - [](sharp::HeightLayer layer, const_prof_arr_t height, - const_prof_arr_t pressure, const_prof_arr_t data, const bool isAGL) { - return sharp::layer_mean(layer, height.data(), pressure.data(), - data.data(), data.size(), isAGL); - }, - nb::arg("HeightLayer"), nb::arg("height"), nb::arg("pressure"), - nb::arg("data"), nb::arg("isAGL") = false, + const char* layer_mean_hght_doc = R"pbdoc( Computes the pressure-weighted mean value of a field over a given HeightLayer. @@ -370,68 +337,27 @@ isAGL : bool Returns ------- float + The layer mean +)pbdoc"; - )pbdoc"); + bind_layer_mean(m_layer, layer_mean_pres_doc); + bind_layer_mean(m_layer, layer_mean_hght_doc); - m_layer.def( - "integrate_layer_trapz", - [](sharp::HeightLayer layer, const_prof_arr_t data, - const_prof_arr_t height, const int integ_sign, const bool weighted) { - return sharp::integrate_layer_trapz(layer, data.data(), - height.data(), data.size(), - integ_sign, weighted); - }, - nb::arg("layer"), nb::arg("data"), nb::arg("height"), - nb::arg("integ_sign") = 0, nb::arg("weighted") = false, - R"pbdoc( -Returns a trapezoidal integration of the given data array over -the given HeightLayer. There is an additional argument that -determines whether this is a weighted average or not. The sign -of the integration may be passed as well, i.e. integrating only -positive or negative area, by passing a 1, 0, or -1 to integ_sign. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer -data : numpy.ndarray[dtype=float32] - 1D NumPy array of data values -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values -integ_sign : int - The sign of the area to integrate (-1: negative; 1: positive; 0: both; default: 0) -weighted : bool - Boolean determining whether or not the integration is weighted by the coordinate array - -Returns -------- -float - )pbdoc"); - - m_layer.def( - "integrate_layer_trapz", - [](sharp::PressureLayer layer, const_prof_arr_t data, - const_prof_arr_t pressure, const int integ_sign, - const bool weighted) { - return sharp::integrate_layer_trapz(layer, data.data(), - pressure.data(), data.size(), - integ_sign, weighted); - }, - nb::arg("layer"), nb::arg("data"), nb::arg("pressure"), - nb::arg("integ_sign") = 0, nb::arg("weighted") = false, + const char* integrate_trapz_template_doc = R"pbdoc( Returns the trapezoidal integration of the given data array over -the given PressureLayer. There is an additional argument that +the given {0}. There is an additional argument that determines whether this is a weighted average or not. The sign of the integration may be passed as well, i.e. integrating only positive or negative area, by passing a 1, 0, or -1 to integ_sign. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer +layer : nwsspc.sharp.calc.layer.{0} data : numpy.ndarray[dtype=float32] 1D NumPy array of data values -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {1} values integ_sign : int The sign of the area to integrate (-1: negative; 1: positive; 0: both; default: 0) weighted : bool @@ -440,8 +366,13 @@ weighted : bool Returns ------- float + The integrated value +)pbdoc"; - )pbdoc"); + bind_integrate_layer_trapz( + m_layer, integrate_trapz_template_doc, "HeightLayer", "height"); + bind_integrate_layer_trapz( + m_layer, integrate_trapz_template_doc, "PressureLayer", "pressure"); } #endif diff --git a/src/nanobind/nwsspc/sharp/calc/interp.pyi b/src/nanobind/nwsspc/sharp/calc/interp.pyi index 42bca11f..bb3cde8f 100644 --- a/src/nanobind/nwsspc/sharp/calc/interp.pyi +++ b/src/nanobind/nwsspc/sharp/calc/interp.pyi @@ -34,7 +34,7 @@ def interp_pressure(pres_val: float, pres_arr: Annotated[NDArray[numpy.float32], ---------- pres_val : float The coordinate pressure value to interpolate to (Pa) - pres_arr : nump.ndarray[dtype=float32] + pres_arr : numpy.ndarray[dtype=float32] 1D numpy array of pressure values to interpolate from (Pa) data_arr : numpy.ndarray[dtype=float32] 1D numpy array of data values to interpolate from diff --git a/src/nanobind/nwsspc/sharp/calc/layer.pyi b/src/nanobind/nwsspc/sharp/calc/layer.pyi index 8717bfd4..ce273049 100644 --- a/src/nanobind/nwsspc/sharp/calc/layer.pyi +++ b/src/nanobind/nwsspc/sharp/calc/layer.pyi @@ -103,7 +103,7 @@ def get_layer_index(layer: PressureLayer, pressure: Annotated[NDArray[numpy.floa ---------- layer : nwsspc.sharp.calc.layer.PressureLayer pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressures + 1D NumPy array of pressure values Returns ------- @@ -129,7 +129,7 @@ def get_layer_index(layer: HeightLayer, height: Annotated[NDArray[numpy.float32] ---------- layer : nwsspc.sharp.calc.layer.HeightLayer height : numpy.ndarray[dtype=float32] - 1D NumPy array of heights + 1D NumPy array of height values Returns ------- @@ -158,6 +158,7 @@ def height_layer_to_pressure(layer: HeightLayer, pressure: Annotated[NDArray[num Returns ------- nwsspc.sharp.calc.layer.PressureLayer + The HeightLayer converted to a PressureLayer """ def pressure_layer_to_height(layer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], toAGL: bool = False) -> HeightLayer: @@ -181,6 +182,7 @@ def pressure_layer_to_height(layer: PressureLayer, pressure: Annotated[NDArray[n Returns ------- nwsspc.sharp.calc.layer.HeightLayer + The PressureLayer converted to a HeightLayer """ @overload @@ -193,9 +195,9 @@ def layer_min(layer: HeightLayer, height: Annotated[NDArray[numpy.float32], dict ---------- layer : nwsspc.sharp.calc.layer.HeightLayer height : numpy.ndarray[dtype=float32] - 1D NumPy array of heights + 1D NumPy array of height values data : numpy.ndarray[dtype=float32] - 1D array of data + 1D NumPy array of data values Returns ------- @@ -212,10 +214,10 @@ def layer_min(layer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], Parameters ---------- layer : nwsspc.sharp.calc.layer.PressureLayer - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure - data : numpy.ndarray[dtype=float32] - 1D array of data + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values + data : numpy.ndarray[dtype=float32] + 1D NumPy array of data values Returns ------- @@ -226,9 +228,8 @@ def layer_min(layer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], @overload def layer_max(layer: HeightLayer, height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: """ - Returns the maximum value observed within the given data array over - the given HeightLayer. The function bounds checks the layer by calling - get_layer_index. + Returns the maximum value of the data array within the given HeightLayer. The + function bounds checks the layer by calling get_layer_index. Parameters ---------- @@ -247,13 +248,13 @@ def layer_max(layer: HeightLayer, height: Annotated[NDArray[numpy.float32], dict @overload def layer_max(layer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: """ - Returns the maximum value observed within the given data array over the given - PressureLayer. The function bounds checks the layer by calling get_layer_index. + Returns the maximum value of the data array within the given PressureLayer. The + function bounds checks the layer by calling get_layer_index. Parameters ---------- layer : nwsspc.sharp.calc.layer.PressureLayer - pressure : numpy.ndarray[dtype=float32] + pressure : numpy.ndarray[dtype=float32] 1D NumPy array of pressure values data : numpy.ndarray[dtype=float32] 1D NumPy array of data values @@ -265,7 +266,7 @@ def layer_max(layer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], """ @overload -def layer_mean(PressureLayer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> float: +def layer_mean(layer: PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> float: """ Computes the pressure-weighted mean value of a field over a given PressureLayer. @@ -281,10 +282,11 @@ def layer_mean(PressureLayer: PressureLayer, pressure: Annotated[NDArray[numpy.f Returns ------- float + The layer mean """ @overload -def layer_mean(HeightLayer: HeightLayer, height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], isAGL: bool = False) -> float: +def layer_mean(layer: HeightLayer, height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], isAGL: bool = False) -> float: """ Computes the pressure-weighted mean value of a field over a given HeightLayer. @@ -304,12 +306,13 @@ def layer_mean(HeightLayer: HeightLayer, height: Annotated[NDArray[numpy.float32 Returns ------- float + The layer mean """ @overload def integrate_layer_trapz(layer: HeightLayer, data: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], integ_sign: int = 0, weighted: bool = False) -> float: """ - Returns a trapezoidal integration of the given data array over + Returns the trapezoidal integration of the given data array over the given HeightLayer. There is an additional argument that determines whether this is a weighted average or not. The sign of the integration may be passed as well, i.e. integrating only @@ -317,19 +320,20 @@ def integrate_layer_trapz(layer: HeightLayer, data: Annotated[NDArray[numpy.floa Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer + layer : nwsspc.sharp.calc.layer.HeightLayer data : numpy.ndarray[dtype=float32] - 1D NumPy array of data values - height : numpy.ndarray[dtype=float32] + 1D NumPy array of data values + height : numpy.ndarray[dtype=float32] 1D NumPy array of height values integ_sign : int The sign of the area to integrate (-1: negative; 1: positive; 0: both; default: 0) weighted : bool - Boolean determining whether or not the integration is weighted by the coordinate array + Boolean determining whether or not the integration is weighted by the coordinate array - Returns + Returns ------- float + The integrated value """ @overload @@ -356,4 +360,5 @@ def integrate_layer_trapz(layer: PressureLayer, data: Annotated[NDArray[numpy.fl Returns ------- float + The integrated value """ diff --git a/src/nanobind/nwsspc/sharp/calc/params.pyi b/src/nanobind/nwsspc/sharp/calc/params.pyi index 8c4fa735..622af650 100644 --- a/src/nanobind/nwsspc/sharp/calc/params.pyi +++ b/src/nanobind/nwsspc/sharp/calc/params.pyi @@ -29,7 +29,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressu Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_wobus pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -53,12 +53,12 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressu """ @overload -def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: +def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: """ Computes the Effective Inflow Layer, or the layer of the atmosphere beliefed to be the primary source of inflow for supercell thunderstorms. The Effective Inflow Layer, and its use in computing shear and storm - relative helicity, is described by Thompson et al. 2007: + relative helicity, is described by Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf Standard/default values for cape_thresh and cinh_thresh have been @@ -73,7 +73,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -97,7 +97,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure """ @overload -def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: +def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtemp: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cape_thresh: float = 100.0, cinh_thresh: float = -250.0, mupcl: nwsspc.sharp.calc.parcel.Parcel | None = None) -> nwsspc.sharp.calc.layer.PressureLayer: """ Computes the Effective Inflow Layer, or the layer of the atmosphere beliefed to be the primary source of inflow for supercell thunderstorms. @@ -117,7 +117,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pr Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -146,7 +146,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pres Computes the Effective Inflow Layer, or the layer of the atmosphere beliefed to be the primary source of inflow for supercell thunderstorms. The Effective Inflow Layer, and its use in computing shear and storm - relative helicity, is described by Thompson et al. 2007: + relative helicity, is described by Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf Standard/default values for cape_thresh and cinh_thresh have been @@ -161,7 +161,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pres Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -314,6 +314,26 @@ def effective_bulk_wind_difference(pressure: Annotated[NDArray[numpy.float32], d of 0-6 km wind shear, but allows more flexibility for elevated convection. Returns MISSING if the effective inflow layer or equilibrium level pressure are MISSING. + + Parameters + ---------- + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of height values (meters) + u_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of u-wind components (m/s) + v_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of v-wind components (m/s) + effective_inflow_layer : nwsspc.sharp.calc.layer.PressureLayer + The PressureLayer that defines the Effective Inflow layer + equilibrium_level_pressure : float + The pressure of the equilibrium level of the most unstable parcel (Pa) + + Returns + ------- + nwsspc.sharp.calc.winds.WindComponents + The (u, v) wind components of the effective bulk wind difference (m/s) """ def energy_helicity_index(cape: float, helicity: float) -> float: @@ -538,6 +558,11 @@ def large_hail_parameter(mu_pcl: nwsspc.sharp.calc.parcel.Parcel, lapse_rate_700 1D NumPy array of u_wind values (m/s) v_wind : numpy.ndarray[dtype=float32] 1D NumPy array of v_wind values (m/s) + + Returns + ------- + float + Large Hail Parameter """ @overload @@ -550,7 +575,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressu Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_wobus pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -571,7 +596,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressu """ @overload -def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: +def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: """ Computes the convective temperature by iteratively lifting parcels from the surface using a lowest 100 hPa mean mixing ratio and increasing @@ -580,7 +605,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -601,7 +626,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure """ @overload -def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: +def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], cinh_thresh: float = -1.0) -> float: """ Computes the convective temperature by iteratively lifting parcels from the surface using a lowest 100 hPa mean mixing ratio and increasing @@ -610,7 +635,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pr Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -640,7 +665,7 @@ def convective_temperature(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pres Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -828,7 +853,7 @@ def fosberg_fire_index(temperature: Annotated[NDArray[numpy.float32], dict(shape """ @overload -def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: """ Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a @@ -848,7 +873,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_wobus mix_layer : nwsspc.sharp.calc.layer.PressureLayer A mixing layer to take average values of the environment from pressure : numpy.ndarray[dtype=float32] @@ -881,7 +906,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter """ @overload -def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: """ Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a @@ -901,7 +926,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus mix_layer : nwsspc.sharp.calc.layer.PressureLayer A mixing layer to take average values of the environment from pressure : numpy.ndarray[dtype=float32] @@ -934,7 +959,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter """ @overload -def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: """ Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a @@ -954,7 +979,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 mix_layer : nwsspc.sharp.calc.layer.PressureLayer A mixing layer to take average values of the environment from pressure : numpy.ndarray[dtype=float32] @@ -987,7 +1012,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter """ @overload -def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: +def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], pcl: nwsspc.sharp.calc.parcel.Parcel | None = None, phi: float = 6.67e-05, beta_incr: float = 0.005) -> float: """ Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a @@ -1007,7 +1032,7 @@ def pyrocumulonimbus_firepower_threshold(lifter: nwsspc.sharp.calc.parcel.lifter Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 mix_layer : nwsspc.sharp.calc.layer.PressureLayer A mixing layer to take average values of the environment from pressure : numpy.ndarray[dtype=float32] diff --git a/src/nanobind/nwsspc/sharp/calc/parcel.pyi b/src/nanobind/nwsspc/sharp/calc/parcel.pyi index f90d6027..844d2ba0 100644 --- a/src/nanobind/nwsspc/sharp/calc/parcel.pyi +++ b/src/nanobind/nwsspc/sharp/calc/parcel.pyi @@ -203,27 +203,6 @@ class lut_data_wobus: @property def num_thetae(self) -> int: ... -class lut_data_cm1: - def __init__(self, lifter: lifter_cm1, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> None: ... - - @property - def pres_min(self) -> float: ... - - @property - def pres_max(self) -> float: ... - - @property - def thte_min(self) -> float: ... - - @property - def thte_max(self) -> float: ... - - @property - def num_logp(self) -> int: ... - - @property - def num_thetae(self) -> int: ... - @overload def lut_data(lifter: lifter_wobus, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> lut_data_wobus: """ @@ -248,7 +227,7 @@ def lut_data(lifter: lifter_wobus, pmin: float = 5000.0, pmax: float = 110000.0, Returns ------- nwsspc.sharp.calc.parcel.lut_data_wobus - A lookup table for nwsspc.sharp.calc.parcel.lifter_wobus + A lookup table for the given lifter """ @overload @@ -274,8 +253,8 @@ def lut_data(lifter: lifter_cm1, pmin: float = 5000.0, pmax: float = 110000.0, t Returns ------- - nwsspc.sharp.calc.parcel.lut_data_wobus - A lookup table for nwsspc.sharp.calc.parcel.lifter_cm1 + nwsspc.sharp.calc.parcel.lut_data_cm1 + A lookup table for the given lifter """ class lifter_lut_wobus: @@ -293,7 +272,7 @@ class lifter_lut_wobus: Parameters ---------- - data : nwsspc.sharp.calc.parcel.lut_data + data : nwsspc.sharp.calc.parcel.lut_data_wobus A shared lookup table constructed with a lifter_wobus instance. """ @@ -360,6 +339,55 @@ class lifter_lut_wobus: The virtual temperature of the parcel (K) """ +@overload +def lifter_lut(lut: lut_data_wobus) -> lifter_lut_wobus: + """ + Constructs the parcel lifter from a LUT. + + Parameters + ---------- + lut : nwsspc.sharp.calc.parcel.lut_data_wobus + + Returns + ------- + nwsspc.sharp.calc.parcel.lifter_lut_wobus + """ + +@overload +def lifter_lut(lut: lut_data_cm1) -> lifter_lut_cm1: + """ + Constructs the parcel lifter from a LUT. + + Parameters + ---------- + lut : nwsspc.sharp.calc.parcel.lut_data_cm1 + + Returns + ------- + nwsspc.sharp.calc.parcel.lifter_lut_cm1 + """ + +class lut_data_cm1: + def __init__(self, lifter: lifter_cm1, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> None: ... + + @property + def pres_min(self) -> float: ... + + @property + def pres_max(self) -> float: ... + + @property + def thte_min(self) -> float: ... + + @property + def thte_max(self) -> float: ... + + @property + def num_logp(self) -> int: ... + + @property + def num_thetae(self) -> int: ... + class lifter_lut_cm1: """ A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) @@ -375,7 +403,7 @@ class lifter_lut_cm1: Parameters ---------- - data : lut_data + data : nwsspc.sharp.calc.parcel.lut_data_cm1 A shared lookup table constructed with a lifter_cm1 instance. """ @@ -384,9 +412,6 @@ class lifter_lut_cm1: lift_from_lcl: bool = ... """ A static flag that helps the parcel lifting functions know where to lift from. - The lifter_cm1 lifts from the last lifted level, rather than the LCL, because - it is an iterative solver. This results in major performance improvements while - maintaining accuracy. """ def setup(self, lcl_pres: float, lcl_tmpk: float) -> None: @@ -445,34 +470,6 @@ class lifter_lut_cm1: The virtual temperature of the parcel (K) """ -@overload -def lifter_lut(lut: lut_data_wobus) -> lifter_lut_wobus: - """ - Constructs the parcel lifter from a LUT. - - Parameters - ---------- - lut : nwsspc.sharp.calc.parcel.lut_data_wobus - - Returns - ------- - nwsspc.sharp.calc.parcel.lifter_lut_wobus - """ - -@overload -def lifter_lut(lut: lut_data_cm1) -> lifter_lut_cm1: - """ - Constructs the parcel lifter from a LUT. - - Parameters - ---------- - lut : nwsspc.sharp.calc.parcel.lut_data_cm1 - - Returns - ------- - nwsspc.sharp.calc.parcel.lifter_lut_cm1 - """ - class LPL(enum.Enum): SFC = 1 """A Surface Based Parcel""" @@ -581,94 +578,6 @@ class Parcel: @cinh.setter def cinh(self, arg: float, /) -> None: ... - @overload - def lift_parcel(self, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: - """ - Lifts a Parcel dry adiabatically from its LPL to its LCL dry - adiabatically, and then moist adiabatically from the LCL to - the top of the profile. The moist adiabat used is determined - by the type of lifting functor passed to the function (i.e. - lifter_wobus or lifter_cm1). - - Parameters - ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus - An instantiated lifter_wobus functor - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - - Returns - ------- - numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - """ - - @overload - def lift_parcel(self, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: - """ - Lifts a Parcel dry adiabatically from its LPL to its LCL dry - adiabatically, and then moist adiabatically from the LCL to - the top of the profile. The moist adiabat used is determined - by the type of lifting functor passed to the function (i.e. - lifter_wobus or lifter_cm1). - - Parameters - ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - An instantiated lifter_cm1 functor - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - - Returns - ------- - numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - """ - - @overload - def lift_parcel(self, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: - """ - Lifts a Parcel dry adiabatically from its LPL to its LCL dry - adiabatically, and then moist adiabatically from the LCL to - the top of the profile. The moist adiabat used is determined - by the type of lifting functor passed to the function (i.e. - lifter_wobus or lifter_cm1). - - Parameters - ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - - Returns - ------- - numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - """ - - @overload - def lift_parcel(self, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: - """ - Lifts a Parcel dry adiabatically from its LPL to its LCL dry - adiabatically, and then moist adiabatically from the LCL to - the top of the profile. The moist adiabat used is determined - by the type of lifting functor passed to the function (i.e. - lifter_wobus or lifter_cm1). - - Parameters - ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - - Returns - ------- - numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - """ - def find_lfc_el(self, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], buoyancy: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: """ Searches the buoyancy array for the LFC and EL combination that results @@ -816,60 +725,96 @@ class Parcel: """ @overload - @staticmethod - def mixed_layer_parcel(mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixing_ratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def lift_parcel(self, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ - Given input arrays of pressure, potential temperature, - and water vapor mixing ratio, as well as a defined PressureLayer, - compute and return a mixed-layer Parcel. + Lifts a Parcel dry adiabatically from its LPL to its LCL dry + adiabatically, and then moist adiabatically from the LCL to + the top of the profile. The moist adiabat used is determined + by the type of lifting functor passed to the function (i.e. + lifter_wobus or lifter_cm1). Parameters ---------- - mix_layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer over which to compute a mixed-layer parcel + lifter : nwsspc.sharp.calc.parcel.lifter_wobus + An instantiated lifter functor pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) - potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile potential temperature (K) - mixing_ratio : numpy.ndarray[dtype=float32] - 1D NumPy array of profile water vapor mixing ratio (unitless) + 1D NumPy array of Pressure levels for lifting (Pa) Returns ------- - nwsspc.sharp.calc.parcel.Parcel - Parcel with mixed layer values + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) """ @overload - @staticmethod - def mixed_layer_parcel(mix_layer: nwsspc.sharp.calc.layer.HeightLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixing_ratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def lift_parcel(self, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ - Given input arrays of pressure, potential temperature, and water - vapor mixing ratio, as well as a defined PressureLayer, compute - and return a mixed-layer Parcel. + Lifts a Parcel dry adiabatically from its LPL to its LCL dry + adiabatically, and then moist adiabatically from the LCL to + the top of the profile. The moist adiabat used is determined + by the type of lifting functor passed to the function (i.e. + lifter_wobus or lifter_cm1). Parameters ---------- - mix_layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer over which to compute a mixed-layer parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus + An instantiated lifter functor pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) - height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) - potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile potential temperature (K) - mixing_ratio : numpy.ndarray[dtype=float32] - 1D NumPy array of profile water vapor mixing ratio (unitless) + 1D NumPy array of Pressure levels for lifting (Pa) Returns ------- - nwsspc.sharp.calc.parcel.Parcel - Parcel with mixed layer values + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + """ + + @overload + def lift_parcel(self, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Lifts a Parcel dry adiabatically from its LPL to its LCL dry + adiabatically, and then moist adiabatically from the LCL to + the top of the profile. The moist adiabat used is determined + by the type of lifting functor passed to the function (i.e. + lifter_wobus or lifter_cm1). + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + An instantiated lifter functor + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + + Returns + ------- + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + """ + + @overload + def lift_parcel(self, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + """ + Lifts a Parcel dry adiabatically from its LPL to its LCL dry + adiabatically, and then moist adiabatically from the LCL to + the top of the profile. The moist adiabat used is determined + by the type of lifting functor passed to the function (i.e. + lifter_wobus or lifter_cm1). + + Parameters + ---------- + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 + An instantiated lifter functor + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + + Returns + ------- + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) """ @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -877,9 +822,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + layer : nwsspc.sharp.calc.layer.PressureLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_wobus Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -900,7 +845,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -908,9 +853,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + layer : nwsspc.sharp.calc.layer.HeightLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_wobus Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -931,7 +876,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -939,9 +884,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + layer : nwsspc.sharp.calc.layer.PressureLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -962,7 +907,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -970,9 +915,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + layer : nwsspc.sharp.calc.layer.HeightLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -993,7 +938,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -1001,9 +946,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_lut + layer : nwsspc.sharp.calc.layer.PressureLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -1024,7 +969,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -1032,9 +977,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_lut + layer : nwsspc.sharp.calc.layer.HeightLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -1055,7 +1000,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.PressureLayer, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -1063,9 +1008,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_lut + layer : nwsspc.sharp.calc.layer.PressureLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -1086,7 +1031,7 @@ class Parcel: @overload @staticmethod - def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + def most_unstable_parcel(layer: nwsspc.sharp.calc.layer.HeightLayer, lifter: lifter_lut_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], virtual_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: """ Given input arrays of pressure, height, temperature, virtual temperature, and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and @@ -1094,9 +1039,9 @@ class Parcel: Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel - lifter : nwsspc.sharp.calc.parcel.lifter_lut + layer : nwsspc.sharp.calc.layer.HeightLayer + Layer for which to search for the Most Unstable Parcel + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -1115,6 +1060,58 @@ class Parcel: Parcel with most-unstable values """ + @overload + @staticmethod + def mixed_layer_parcel(mix_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixing_ratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + """ + Given input arrays of pressure, potential temperature, + and water vapor mixing ratio, as well as a defined PressureLayer, + compute and return a mixed-layer Parcel. + + Parameters + ---------- + mix_layer : nwsspc.sharp.calc.layer.PressureLayer + PressureLayer over which to compute a mixed-layer parcel + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) + potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile potential temperature (K) + mixing_ratio : numpy.ndarray[dtype=float32] + 1D NumPy array of profile water vapor mixing ratio (unitless) + + Returns + ------- + nwsspc.sharp.calc.parcel.Parcel + Parcel with mixed layer values + """ + + @overload + @staticmethod + def mixed_layer_parcel(mix_layer: nwsspc.sharp.calc.layer.HeightLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], potential_temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mixing_ratio: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Parcel: + """ + Given input arrays of pressure, potential temperature, and water + vapor mixing ratio, as well as a defined PressureLayer, compute + and return a mixed-layer Parcel. + + Parameters + ---------- + mix_layer : nwsspc.sharp.calc.layer.HeightLayer + HeightLayer over which to compute a mixed-layer parcel + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) + potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile potential temperature (K) + mixing_ratio : numpy.ndarray[dtype=float32] + 1D NumPy array of profile water vapor mixing ratio (unitless) + + Returns + ------- + nwsspc.sharp.calc.parcel.Parcel + Parcel with mixed layer values + """ + class DowndraftParcel: """ Contains information about a DowndraftParcel's starting level and @@ -1175,6 +1172,62 @@ class DowndraftParcel: @cinh.setter def cinh(self, arg: float, /) -> None: ... + def cape_cinh(self, pres: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], hght: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], buoy: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: + """ + Assuming that nwsspc.sharp.calc.parcel.DowndraftParcel.lower_parcel + has been called, cape_cinh will integrate the area between the LPL + and the surface to compute downdraft CAPE and downdraft CINH. + + The results are stored in nwsspc.sharp.calc.parcel.DowndraftParcel.cape + and nwsspc.sharp.calc.parcel.DowndraftParcel.cinh via the provided parcel. + + Parameters + ---------- + pres : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) + hght : numpy.ndarray[dtype=float32] + 1D NumPy array of height values (meters) + buoyancy : numpy.ndarray[dtype=float32] + 1D NumPy array of buoyancy values (m/s^2) + + Returns + ------- + tuple[float, float] + (DCAPE, DCINH) + """ + + @staticmethod + def min_thetae(search_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], thetae: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mean_depth: float = 10000.0) -> DowndraftParcel: + """ + Define a downdraft parcel. + + Defines a downdraft parcel within a given search layer. + The downdraft parcel is defined as the minimum layer-mean + equivalent potential temperature (Theta-E) within the + search layer. Typical values are to search within the lowest + 400 hPa of the profile, and a mean depth of 100 hPa. + + Parameters + ---------- + search_layer : nwsspc.sharp.calc.layer.PressureLayer + The layer over which to search for the downdraft parcel + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of temperature (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of dewpoint (K) + thetae : numpy.ndarray[dtype=float32] + 1D NumPy array of thetae (K) + mean_depth : float + The layer depth for calculating mean thetae. + + Returns + ------- + nwsspc.sharp.calc.parcel.DowndraftParcel + Downdraft Parcel + """ + @overload def lower_parcel(self, lifter: lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ @@ -1188,8 +1241,8 @@ class DowndraftParcel: Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus - An instantiated lifter_wobus functor + lifter : nwsspc.sharp.calc.parcel.lifter_wobus + An instantiated lifter functor pressure : numpy.ndarray[dtype=float32] 1D NumPy array of Pressure levels for lifting (Pa) @@ -1200,7 +1253,7 @@ class DowndraftParcel: """ @overload - def lower_parcel(self, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + def lower_parcel(self, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist adiabatically from its LPL to the surface. The moist adiabat used @@ -1212,8 +1265,8 @@ class DowndraftParcel: Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - An instantiated lifter_cm1 functor + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus + An instantiated lifter functor pressure : numpy.ndarray[dtype=float32] 1D NumPy array of Pressure levels for lifting (Pa) @@ -1224,7 +1277,7 @@ class DowndraftParcel: """ @overload - def lower_parcel(self, lifter: lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: + def lower_parcel(self, lifter: lifter_cm1, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist adiabatically from its LPL to the surface. The moist adiabat used @@ -1236,8 +1289,8 @@ class DowndraftParcel: Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + An instantiated lifter functor pressure : numpy.ndarray[dtype=float32] 1D NumPy array of Pressure levels for lifting (Pa) @@ -1260,8 +1313,8 @@ class DowndraftParcel: Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 + An instantiated lifter functor pressure : numpy.ndarray[dtype=float32] 1D NumPy array of Pressure levels for lifting (Pa) @@ -1270,59 +1323,3 @@ class DowndraftParcel: numpy.ndarray[dtype=float32] A 1D NumPy array of parcel temperature values (K) """ - - def cape_cinh(self, pres: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], hght: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], buoy: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[float, float]: - """ - Assuming that nwsspc.sharp.calc.parcel.DowndraftParcel.lower_parcel - has been called, cape_cinh will integrate the area between the LPL - and the surface to compute downdraft CAPE and downdraft CINH. - - The results are stored in nwsspc.sharp.calc.parcel.DowndraftParcel.cape - and nwsspc.sharp.calc.parcel.DowndraftParcel.cinh via the provided parcel. - - Parameters - ---------- - pres : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values (Pa) - hght : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) - buoyancy : numpy.ndarray[dtype=float32] - 1D NumPy array of buoyancy values (m/s^2) - - Returns - ------- - tuple[float, float] - (DCAPE, DCINH) - """ - - @staticmethod - def min_thetae(search_layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], thetae: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], mean_depth: float = 10000.0) -> DowndraftParcel: - """ - Define a downdraft parcel. - - Defines a downdraft parcel within a given search layer. - The downdraft parcel is defined as the minimum layer-mean - equivalent potential temperature (Theta-E) within the - search layer. Typical values are to search within the lowest - 400 hPa of the profile, and a mean depth of 100 hPa. - - Parameters - ---------- - search_layer : nwsspc.sharp.calc.layer.PressureLayer - The layer over which to search for the downdraft parcel - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure (Pa) - temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of temperature (K) - dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of dewpoint (K) - thetae : numpy.ndarray[dtype=float32] - 1D NumPy array of thetae (K) - mean_depth : float - The layer depth for calculating mean thetae. - - Returns - ------- - nwsspc.sharp.calc.parcel.DowndraftParcel - Downdraft Parcel - """ diff --git a/src/nanobind/nwsspc/sharp/calc/thermo.pyi b/src/nanobind/nwsspc/sharp/calc/thermo.pyi index 6d40864d..e16528c0 100644 --- a/src/nanobind/nwsspc/sharp/calc/thermo.pyi +++ b/src/nanobind/nwsspc/sharp/calc/thermo.pyi @@ -734,7 +734,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: float, temp Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : float The ambient pressure (Pa) @@ -750,7 +750,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: float, temp """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temperature: float, dewpoint: float) -> float: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -766,19 +766,19 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temper Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : float - The ambient pressure (Pa) - temperature : float - The ambient temperature (K) - dewpoint : float - The ambient dewpoint temperature (K) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatures (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) Returns ------- - float - The wetbulb temperature (K) + numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) """ @overload @@ -798,7 +798,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: float, Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : float The ambient pressure (Pa) @@ -814,7 +814,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: float, """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, temperature: float, dewpoint: float) -> float: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -830,23 +830,23 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, te Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : float - The ambient pressure (Pa) - temperature : float - The ambient temperature (K) - dewpoint : float - The ambient dewpoint temperature (K) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatures (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) Returns ------- - float - The wetbulb temperature (K) + numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -862,19 +862,19 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[N Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient pressures (Pa) - temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) - dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperatures (K) + pressure : float + The ambient pressure (Pa) + temperature : float + The ambient temperature (K) + dewpoint : float + The ambient dewpoint temperature (K) Returns ------- - numpy.ndarray[dtype=float32] - 1D NumPy array of wetbulb temperatures (K) + float + The wetbulb temperature (K) """ @overload @@ -894,12 +894,12 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDA Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : numpy.ndarray[dtype=float32] 1D NumPy array of ambient pressures (Pa) temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) + 1D NumPy array of ambient temperatures (K) dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of ambient dewpoint temperatures (K) @@ -910,7 +910,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotated[NDA """ @overload -def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet bulb temperature (K) given the ambient pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -926,19 +926,19 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotat Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient pressures (Pa) - temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) - dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperatures (K) + pressure : float + The ambient pressure (Pa) + temperature : float + The ambient temperature (K) + dewpoint : float + The ambient dewpoint temperature (K) Returns ------- - numpy.ndarray[dtype=float32] - 1D NumPy array of wetbulb temperatures (K) + float + The wetbulb temperature (K) """ @overload @@ -958,12 +958,12 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: Annotated Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : numpy.ndarray[dtype=float32] 1D NumPy array of ambient pressures (Pa) temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) + 1D NumPy array of ambient temperatures (K) dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of ambient dewpoint temperatures (K) @@ -982,22 +982,22 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: float First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : float the ambient air pressure (Pa) temperature : float the ambient air temperature (K) dewpoint : float - the ambient dewpoint temperature + the ambient dewpoint temperature (K) Returns ------- @@ -1006,7 +1006,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: float """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temperature: float, dewpoint: float) -> float: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1014,27 +1014,27 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : float - the ambient air pressure (Pa) - temperature : float - the ambient air temperature (K) - dewpoint : float - the ambient dewpoint temperature + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature (K) Returns ------- - float - The wet-bulb potential temperature (K) + numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) """ @overload @@ -1046,22 +1046,22 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: f First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : float the ambient air pressure (Pa) temperature : float the ambient air temperature (K) dewpoint : float - the ambient dewpoint temperature + the ambient dewpoint temperature (K) Returns ------- @@ -1070,7 +1070,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: f """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, temperature: float, dewpoint: float) -> float: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1078,31 +1078,31 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: flo First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : float - the ambient air pressure (Pa) - temperature : float - the ambient air temperature (K) - dewpoint : float - the ambient dewpoint temperature + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) + temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) + dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature (K) Returns ------- - float - The wet-bulb potential temperature (K) + numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1110,27 +1110,27 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annot First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_wobus + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air pressure (Pa) - temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air temperature (K) - dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature + pressure : float + the ambient air pressure (Pa) + temperature : float + the ambient air temperature (K) + dewpoint : float + the ambient dewpoint temperature (K) Returns ------- - numpy.ndarray[dtype=float32] - 1D NumPy array of wet-bulb potential temperatures (K) + float + The wet-bulb potential temperature (K) """ @overload @@ -1142,22 +1142,22 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotat First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_cm1 + lifter : nwsspc.sharp.calc.parcel.lifter_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : numpy.ndarray[dtype=float32] 1D NumPy array of ambient air pressure (Pa) temperature : numpy.ndarray[dtype=float32] 1D NumPy array of ambient air temperature (K) dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature + 1D NumPy array of ambient dewpoint temperature (K) Returns ------- @@ -1166,7 +1166,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: Annotat """ @overload -def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], temperature: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], dewpoint: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]: +def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: float, temperature: float, dewpoint: float) -> float: """ Compute the wet-bulb potential temperature (K) given the pressure (Pa), temperature (K), and dewpoint temperature (K). @@ -1174,27 +1174,27 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_wobus, pressure: A First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air pressure (Pa) - temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air temperature (K) - dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature + pressure : float + the ambient air pressure (Pa) + temperature : float + the ambient air temperature (K) + dewpoint : float + the ambient dewpoint temperature (K) Returns ------- - numpy.ndarray[dtype=float32] - 1D NumPy array of wet-bulb potential temperatures (K) + float + The wet-bulb potential temperature (K) """ @overload @@ -1206,22 +1206,22 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_lut_cm1, pressure: Ann First, it lifts a parcel with the given pressure, temperature, and dewpoint temperature to its Lifted Condensation Level (LCL). To compute the temperature and pressure of the LCL, an approximation - is used. See the lcl_temperature fuction for further detail. + is used. See the lcl_temperature function for further detail. - After the parcel has reached the LCL, the lifted passed to the function + After the parcel has reached the LCL, the lifter passed to the function lowers the parcel to the standard parcel reference pressure level (1000 hPa) along a moist adiabat. Parameters ---------- - lifter : nwsspc.sharp.calc.parcel.lifter_lut + lifter : nwsspc.sharp.calc.parcel.lifter_lut_cm1 a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : numpy.ndarray[dtype=float32] 1D NumPy array of ambient air pressure (Pa) temperature : numpy.ndarray[dtype=float32] 1D NumPy array of ambient air temperature (K) dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature + 1D NumPy array of ambient dewpoint temperature (K) Returns ------- diff --git a/src/nanobind/nwsspc/sharp/calc/winds.pyi b/src/nanobind/nwsspc/sharp/calc/winds.pyi index 1367046b..3e016992 100644 --- a/src/nanobind/nwsspc/sharp/calc/winds.pyi +++ b/src/nanobind/nwsspc/sharp/calc/winds.pyi @@ -60,8 +60,8 @@ def helicity(layer: nwsspc.sharp.calc.layer.HeightLayer, storm_motion: WindCompo Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer - A HeightLayer to integrate over (meters AGL) + layer : nwsspc.sharp.calc.layer.HeightLayer + A layer to integrate over (meters AGL) storm_motion : nwsspc.sharp.calc.winds.WindComponents A WindComponents object with the storm motion in m/s height : numpy.ndarray[dtype=float32] @@ -89,12 +89,12 @@ def helicity(layer: nwsspc.sharp.calc.layer.PressureLayer, storm_motion: WindCom Parameters ---------- - layer : nwsspc.sharp.calc.layer.PressureLayer - A PressureLayer to integrate over (Pa) + layer : nwsspc.sharp.calc.layer.PressureLayer + A layer to integrate over (Pa) storm_motion : nwsspc.sharp.calc.winds.WindComponents A WindComponents object with the storm motion in m/s - height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of environment U-wind components (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -116,7 +116,7 @@ def wind_shear(layer: nwsspc.sharp.calc.layer.HeightLayer, height: Annotated[NDA Parameters ---------- layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to compute wind shear + Layer for which to compute wind shear (meters AGL) height : numpy.ndarray[dtype=float32] 1D NumPy array of height values (meters) u_wind : numpy.ndarray[dtype=float32] @@ -140,9 +140,9 @@ def wind_shear(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated Parameters ---------- layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to compute wind shear - height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) + Layer for which to compute wind shear (Pa) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of U-wind components (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -154,7 +154,7 @@ def wind_shear(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated WindComponents of U and V wind shear components (m/s) """ -def mean_wind(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu')], weighted: bool = False) -> WindComponents: +def mean_wind(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], weighted: bool = False) -> WindComponents: """ Computes the mean wind over the given PressureLayer and input profile arrays of pressure, U-wind, and V-wind components. @@ -175,21 +175,21 @@ def mean_wind(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[ Returns ------- nwsspc.sharp.calc.winds.WindComponents - WindComponents of U anf V mean wind components (m/s) + WindComponents of U and V mean wind components (m/s) """ @overload -def max_wind(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> WindComponents: +def max_wind(layer: nwsspc.sharp.calc.layer.HeightLayer, height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> WindComponents: """ - Finds the maximum wind speed over a given nwsspc.sharp.calc.layer.PressureLayer, + Finds the maximum wind speed over a given layer, returning the vector components. Parameters ---------- - layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer over which to compute mean - pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure coordinate values (Pa) + layer : nwsspc.sharp.calc.layer.HeightLayer + Layer over which to compute the max (meters AGL) + height : numpy.ndarray[dtype=float32] + 1D NumPy array of height coordinate values (meters) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of U-wind component values (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -201,17 +201,17 @@ def max_wind(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[N """ @overload -def max_wind(layer: nwsspc.sharp.calc.layer.HeightLayer, height: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> WindComponents: +def max_wind(layer: nwsspc.sharp.calc.layer.PressureLayer, pressure: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], u_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], v_wind: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> WindComponents: """ - Finds the maximum wind speed over a given nwsspc.sharp.calc.layer.HeightLayer, + Finds the maximum wind speed over a given layer, returning the vector components. Parameters ---------- - layer : nwsspc.sharp.calc.layer.HeightLayer - PressureLayer over which to compute mean - height : numpy.ndarray[dtype=float32] - 1D NumPy array of height coordinate values (m) + layer : nwsspc.sharp.calc.layer.PressureLayer + Layer over which to compute the max (Pa) + pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure coordinate values (Pa) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of U-wind component values (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -426,7 +426,7 @@ def components_to_vector(u_comp: Annotated[NDArray[numpy.float32], dict(shape=(N tuple[numpy.ndarray[dtype=float32], numpy.ndarray[dtype=float32]] wspd: 1D NumPy array of wind speeds (m/s) - wdir: 1D NumPy array of wind directiond (degrees from North) + wdir: 1D NumPy array of wind directions (degrees from North) """ @overload @@ -458,7 +458,7 @@ def vector_to_components(wind_vector: WindVector) -> WindComponents: Parameters ---------- wind_vector : nwsspc.sharp.calc.winds.WindVector - WindVector containing wind wpeed (m/s) and direction (degrees from North) + WindVector containing wind speed (m/s) and direction (degrees from North) Returns ------- @@ -470,8 +470,8 @@ def vector_to_components(wind_vector: WindVector) -> WindComponents: def vector_to_components(wspd: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)], wdir: Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C', device='cpu', writable=False)]) -> tuple[Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')], Annotated[NDArray[numpy.float32], dict(shape=(None,), order='C')]]: """ Given 1D NumPy arrays of the wind speed (m/s) and direction (degrees from North), - compute and return 1D NumPy arays of the zonal (U) and meridional (V) vector - componenWindComponents. + compute and return 1D NumPy arrays of the zonal (U) and meridional (V) vector + components. Parameters ---------- diff --git a/src/nanobind/params_bindings.h b/src/nanobind/params_bindings.h index e09b34f4..36086976 100644 --- a/src/nanobind/params_bindings.h +++ b/src/nanobind/params_bindings.h @@ -12,194 +12,111 @@ #include #include #include +#include #include +#include "binding_utils.h" #include "sharplib_types.h" namespace nb = nanobind; -inline void make_params_bindings(nb::module_ m) { - nb::module_ m_params = - m.def_submodule("params", - "Sounding and Hodograph Analysis and Research Program " - "Library (SHARPlib) :: Derived Parameters"); - - m_params.def( +template +void bind_effective_inflow(nb::module_& mod, const char* template_doc, + const char* lifter_name) { + std::string doc = fmt::format(template_doc, lifter_name); + mod.def( "effective_inflow_layer", - [](sharp::lifter_wobus& lifter, const_prof_arr_t pressure, - const_prof_arr_t height, const_prof_arr_t temperature, - const_prof_arr_t dewpoint, const_prof_arr_t virtemp, - float cape_thresh, float cinh_thresh, sharp::Parcel* mupcl) { - if ((pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[height.size()]; - float* pcl_buoy = new float[height.size()]; + [](Lft& lifter, const_prof_arr_t pressure, const_prof_arr_t height, + const_prof_arr_t temperature, const_prof_arr_t dewpoint, + const_prof_arr_t virtemp, const float cape_thresh, + const float cinh_thresh, + sharp::Parcel* mupcl) -> sharp::PressureLayer { + check_equal_sizes(pressure, height, temperature, dewpoint, virtemp); + + std::size_t NZ = height.size(); + auto pcl_vtmp = std::make_unique(NZ); + auto pcl_buoy = std::make_unique(NZ); sharp::PressureLayer eil = sharp::effective_inflow_layer( lifter, pressure.data(), height.data(), temperature.data(), - dewpoint.data(), virtemp.data(), pcl_vtmp, pcl_buoy, - height.size(), cape_thresh, cinh_thresh, mupcl); - - delete[] pcl_vtmp; - delete[] pcl_buoy; + dewpoint.data(), virtemp.data(), pcl_vtmp.get(), pcl_buoy.get(), + NZ, cape_thresh, cinh_thresh, mupcl); return eil; }, nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("virtemp"), nb::arg("cape_thresh") = 100.0f, nb::arg("cinh_thresh") = -250.0f, - nb::arg("mupcl") = nb::none(), - R"pbdoc( -Computes the Effective Inflow Layer, or the layer of the atmosphere -beliefed to be the primary source of inflow for supercell thunderstorms. -The Effective Inflow Layer, and its use in computing shear and storm -relative helicity, is described by Thompson et al. 2007: -https://www.spc.noaa.gov/publications/thompson/effective.pdf - -Standard/default values for cape_thresh and cinh_thresh have been -experimentally determined to be cape_thresh = 100 J/kg and -cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the -'mupcl' kwarg, the Most Unstable parcel found during the EIL search will -be returned. - -References ----------- -Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - A 1D NumPy array of dewpoint values (K) -virtemp : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -cape_thresh : float, default = 100.0 - The CAPE threshold used to compute the Effective Inflow Layer -cinh_thresh : float, default = -250.0 - The CINH threshold used to compute the Effective Inflow Layer -mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional + nb::arg("mupcl") = nb::none(), doc.c_str()); +} -Returns -------- -nwsspc.sharp.calc.layer.PressureLayer - The Effective Inflow Layer - )pbdoc"); +template +void bind_convective_temperature(nb::module_& mod, const char* template_doc, + const char* lifter_name) { + std::string doc = fmt::format(template_doc, lifter_name); - m_params.def( - "effective_inflow_layer", - [](sharp::lifter_cm1& lifter, const_prof_arr_t pressure, - const_prof_arr_t height, const_prof_arr_t temperature, - const_prof_arr_t dewpoint, const_prof_arr_t virtemp, - float cape_thresh, float cinh_thresh, sharp::Parcel* mupcl) { - if ((pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[height.size()]; - float* pcl_buoy = new float[height.size()]; + mod.def( + "convective_temperature", + [](Lft& lifter, const_prof_arr_t pres, const_prof_arr_t hght, + const_prof_arr_t tmpk, const_prof_arr_t vtmpk, const_prof_arr_t mixr, + float cinh_thresh) { + check_equal_sizes(pres, hght, tmpk, vtmpk, mixr); - sharp::PressureLayer eil = sharp::effective_inflow_layer( - lifter, pressure.data(), height.data(), temperature.data(), - dewpoint.data(), virtemp.data(), pcl_vtmp, pcl_buoy, - height.size(), cape_thresh, cinh_thresh, mupcl); + const std::size_t NZ = pres.size(); + auto pcl_vtmp = std::make_unique(NZ); + auto pcl_buoy = std::make_unique(NZ); - delete[] pcl_vtmp; - delete[] pcl_buoy; + float cnvtv_tmpk = sharp::convective_temperature( + lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), + mixr.data(), pcl_vtmp.get(), pcl_buoy.get(), pres.size()); - return eil; + return cnvtv_tmpk; }, nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("virtemp"), - nb::arg("cape_thresh") = 100.0f, nb::arg("cinh_thresh") = -250.0f, - nb::arg("mupcl") = nb::none(), - R"pbdoc( -Computes the Effective Inflow Layer, or the layer of the atmosphere -beliefed to be the primary source of inflow for supercell thunderstorms. -The Effective Inflow Layer, and its use in computing shear and storm -relative helicity, is described by Thompson et al. 2007: -https://www.spc.noaa.gov/publications/thompson/effective.pdf - -Standard/default values for cape_thresh and cinh_thresh have been -experimentally determined to be cape_thresh = 100 J/kg and -cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the -'mupcl' kwarg, the Most Unstable parcel found during the EIL search will -be returned. - -References ----------- -Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + nb::arg("temperature"), nb::arg("virtual_temperature"), + nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, doc.c_str()); +} -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - A 1D NumPy array of dewpoint values (K) -virtemp : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -cape_thresh : float, default = 100.0 - The CAPE threshold used to compute the Effective Inflow Layer -cinh_thresh : float, default = -250.0 - The CINH threshold used to compute the Effective Inflow Layer -mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional +template +void bind_pyrocb_firepower(nb::module_& mod, const char* template_doc, + const char* lifter_name) { + std::string doc = fmt::format(template_doc, lifter_name); -Returns -------- -nwsspc.sharp.calc.layer.PressureLayer - The Effective Inflow Layer - )pbdoc"); + mod.def( + "pyrocumulonimbus_firepower_threshold", + [](Lft& lifter, sharp::PressureLayer& mix_layer, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t mixr, + const_prof_arr_t vtmpk, const_prof_arr_t uwin, const_prof_arr_t vwin, + const_prof_arr_t theta, sharp::Parcel* pcl, float phi, + float beta_incr) { + check_equal_sizes(pres, hght, tmpk, mixr, vtmpk, uwin, vwin, theta); + const std::size_t NZ = pres.size(); + auto pcl_vtmp = std::make_unique(NZ); + auto pcl_buoy = std::make_unique(NZ); - m_params.def( - "effective_inflow_layer", - [](sharp::lifter_lut& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t dewpoint, - const_prof_arr_t virtemp, float cape_thresh, float cinh_thresh, - sharp::Parcel* mupcl) { - if ((pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[height.size()]; - float* pcl_buoy = new float[height.size()]; + float pft = sharp::pyrocumulonimbus_firepower_threshold( + lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), + mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), + theta.data(), pcl_vtmp.get(), pcl_buoy.get(), pres.size(), pcl, + phi, beta_incr); - sharp::PressureLayer eil = sharp::effective_inflow_layer( - lifter, pressure.data(), height.data(), temperature.data(), - dewpoint.data(), virtemp.data(), pcl_vtmp, pcl_buoy, - height.size(), cape_thresh, cinh_thresh, mupcl); + return pft; + }, + nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), + nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), + nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), + nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, doc.c_str()); +} - delete[] pcl_vtmp; - delete[] pcl_buoy; +inline void make_params_bindings(nb::module_ m) { + nb::module_ m_params = + m.def_submodule("params", + "Sounding and Hodograph Analysis and Research Program " + "Library (SHARPlib) :: Derived Parameters"); - return eil; - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("virtemp"), - nb::arg("cape_thresh") = 100.0f, nb::arg("cinh_thresh") = -250.0f, - nb::arg("mupcl") = nb::none(), + const char* eil_template_doc = R"pbdoc( Computes the Effective Inflow Layer, or the layer of the atmosphere beliefed to be the primary source of inflow for supercell thunderstorms. @@ -219,7 +136,7 @@ Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.p Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut +lifter : {} pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -240,80 +157,12 @@ Returns ------- nwsspc.sharp.calc.layer.PressureLayer The Effective Inflow Layer - )pbdoc"); - - m_params.def( - "effective_inflow_layer", - [](sharp::lifter_lut& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t dewpoint, - const_prof_arr_t virtemp, float cape_thresh, float cinh_thresh, - sharp::Parcel* mupcl) { - if ((pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[height.size()]; - float* pcl_buoy = new float[height.size()]; - - sharp::PressureLayer eil = sharp::effective_inflow_layer( - lifter, pressure.data(), height.data(), temperature.data(), - dewpoint.data(), virtemp.data(), pcl_vtmp, pcl_buoy, - height.size(), cape_thresh, cinh_thresh, mupcl); - - delete[] pcl_vtmp; - delete[] pcl_buoy; - - return eil; - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("virtemp"), - nb::arg("cape_thresh") = 100.0f, nb::arg("cinh_thresh") = -250.0f, - nb::arg("mupcl") = nb::none(), - R"pbdoc( -Computes the Effective Inflow Layer, or the layer of the atmosphere -beliefed to be the primary source of inflow for supercell thunderstorms. -The Effective Inflow Layer, and its use in computing shear and storm -relative helicity, is described by Thompson et al. 2007: -https://www.spc.noaa.gov/publications/thompson/effective.pdf - -Standard/default values for cape_thresh and cinh_thresh have been -experimentally determined to be cape_thresh = 100 J/kg and -cinh_thresh = -250.0 J/kg. If an empty parcel object is passed via the -'mupcl' kwarg, the Most Unstable parcel found during the EIL search will -be returned. - -References ----------- -Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + )pbdoc"; -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - A 1D NumPy array of dewpoint values (K) -virtemp : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -cape_thresh : float, default = 100.0 - The CAPE threshold used to compute the Effective Inflow Layer -cinh_thresh : float, default = -250.0 - The CINH threshold used to compute the Effective Inflow Layer -mupcl : None or nwsspc.sharp.calc.parcel.Parcel, optional - -Returns -------- -nwsspc.sharp.calc.layer.PressureLayer - The Effective Inflow Layer - )pbdoc"); + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_effective_inflow(m_params, eil_template_doc, lifter_name); + }); m_params.def( "storm_motion_bunkers", @@ -322,12 +171,7 @@ nwsspc.sharp.calc.layer.PressureLayer sharp::HeightLayer mean_wind_layer_agl, sharp::HeightLayer wind_shear_layer_agl, const bool leftMover, const bool pressureWeighted) { - if ((pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != u_wind.shape(0)) || - (pressure.shape(0) != v_wind.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } + check_equal_sizes(pressure, height, u_wind, v_wind); sharp::WindComponents storm_mtn = sharp::storm_motion_bunkers( pressure.data(), height.data(), u_wind.data(), v_wind.data(), height.size(), mean_wind_layer_agl, wind_shear_layer_agl, @@ -384,12 +228,7 @@ nwsspc.sharp.calc.winds.WindComponents const_prof_arr_t u_wind, const_prof_arr_t v_wind, sharp::PressureLayer eff_infl_lyr, sharp::Parcel& mupcl, const bool leftMover) { - if ((pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != u_wind.shape(0)) || - (pressure.shape(0) != v_wind.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } + check_equal_sizes(pressure, height, u_wind, v_wind); sharp::WindComponents storm_mtn = sharp::storm_motion_bunkers( pressure.data(), height.data(), u_wind.data(), v_wind.data(), height.size(), eff_infl_lyr, mupcl, leftMover); @@ -451,21 +290,10 @@ nwsspc.sharp.calc.winds.WindComponents [](const_prof_arr_t pressure, const_prof_arr_t height, const_prof_arr_t u_wind, const_prof_arr_t v_wind) -> std::pair { - auto pres = pressure.view(); - auto hght = height.view(); - auto uwin = u_wind.view(); - auto vwin = v_wind.view(); - - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0))) { - throw nb::buffer_error( - "pressure, height, u_wind, and v_wind must have the " - "same sizes!"); - } - - return sharp::mcs_motion_corfidi(pres.data(), hght.data(), - uwin.data(), vwin.data(), + check_equal_sizes(pressure, height, u_wind, v_wind); + + return sharp::mcs_motion_corfidi(pressure.data(), height.data(), + u_wind.data(), v_wind.data(), pressure.size()); }, nb::arg("pressure"), nb::arg("height"), nb::arg("u_wind"), @@ -504,16 +332,10 @@ tuple[nwsspc.sharp.calc.winds.WindComponents, nwsspc.sharp.calc.winds.WindCompon [](const_prof_arr_t pres, const_prof_arr_t hght, const_prof_arr_t uwin, const_prof_arr_t vwin, sharp::PressureLayer eil, const float eql_pres) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0))) { - throw nb::buffer_error( - "pressure, height, u_wind, and v_wind must have the " - "same sizes!"); - } + check_equal_sizes(pres, hght, uwin, vwin); return sharp::effective_bulk_wind_difference( - pres.data(), hght.data(), uwin.data(), vwin.data(), - pres.shape(0), eil, eql_pres); + pres.data(), hght.data(), uwin.data(), vwin.data(), pres.size(), + eil, eql_pres); }, nb::arg("pressure"), nb::arg("height"), nb::arg("u_wind"), nb::arg("v_wind"), nb::arg("effective_inflow_layer"), @@ -528,6 +350,25 @@ of 0-6 km wind shear, but allows more flexibility for elevated convection. Returns MISSING if the effective inflow layer or equilibrium level pressure are MISSING. +Parameters +---------- +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of height values (meters) +u_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of u-wind components (m/s) +v_wind : numpy.ndarray[dtype=float32] + 1D NumPy array of v-wind components (m/s) +effective_inflow_layer : nwsspc.sharp.calc.layer.PressureLayer + The PressureLayer that defines the Effective Inflow layer +equilibrium_level_pressure : float + The pressure of the equilibrium level of the most unstable parcel (Pa) + +Returns +------- +nwsspc.sharp.calc.winds.WindComponents + The (u, v) wind components of the effective bulk wind difference (m/s) )pbdoc"); m_params.def("energy_helicity_index", &sharp::energy_helicity_index, @@ -741,13 +582,7 @@ float const sharp::WindComponents storm_motion, const_prof_arr_t pres, const_prof_arr_t hght, const_prof_arr_t uwin, const_prof_arr_t vwin) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0))) { - throw nb::buffer_error( - "pressure, height, u_wind, and v_wind must have the " - "same sizes!"); - } + check_equal_sizes(pres, hght, uwin, vwin); return sharp::large_hail_parameter( mu_pcl, lapse_rate_700_500mb, hail_growth_zone, storm_motion, @@ -787,35 +622,14 @@ u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of u_wind values (m/s) v_wind : numpy.ndarray[dtype=float32] 1D NumPy array of v_wind values (m/s) - )pbdoc"); - - m_params.def( - "convective_temperature", - [](sharp::lifter_wobus& lifter, const_prof_arr_t pres, - const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t vtmpk, - const_prof_arr_t mixr, float cinh_thresh) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float cnvtv_tmpk = sharp::convective_temperature( - lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), - mixr.data(), pcl_vtmp, pcl_buoy, pres.size()); - delete[] pcl_vtmp; - delete[] pcl_buoy; +Returns +------- +float + Large Hail Parameter + )pbdoc"); - return cnvtv_tmpk; - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("virtual_temperature"), - nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, + const char* convective_temp_template_doc = R"pbdoc( Computes the convective temperature by iteratively lifting parcels from the surface using a lowest 100 hPa mean mixing ratio and increasing @@ -824,7 +638,7 @@ threshold. The first guess is the current surface temperature. Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus +lifter : {} pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -842,198 +656,36 @@ Returns ------- float The convective temperature (K) -)pbdoc"); - - m_params.def( - "convective_temperature", - [](sharp::lifter_cm1& lifter, const_prof_arr_t pres, - const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t vtmpk, - const_prof_arr_t mixr, float cinh_thresh) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float cnvtv_tmpk = sharp::convective_temperature( - lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), - mixr.data(), pcl_vtmp, pcl_buoy, pres.size()); +)pbdoc"; - delete[] pcl_vtmp; - delete[] pcl_buoy; + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_convective_temperature(m_params, convective_temp_template_doc, + lifter_name); + }); - return cnvtv_tmpk; + m_params.def( + "precipitable_water", + [](sharp::PressureLayer& layer, const_prof_arr_t pres, + const_prof_arr_t mixr) { + check_equal_sizes(pres, mixr); + return sharp::precipitable_water(layer, pres.data(), mixr.data(), + pres.size()); }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("virtual_temperature"), - nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, + + nb::arg("layer"), nb::arg("pres"), nb::arg("mixr"), R"pbdoc( -Computes the convective temperature by iteratively lifting parcels from -the surface using a lowest 100 hPa mean mixing ratio and increasing -surface temperatures to find the first parcel that reaches the CINH -threshold. The first guess is the current surface temperature. +Given a PressureLayer to integrate over, compute the precipitable water +from the given pressure and mixing ratio arrays. Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -virtemp : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -mixratio : numpy.ndarray[dtype=float32] - A 1D NumPy array of water vapor mixing ratio values (g/g) -cinh_thresh : float, default = -1.0 - The CINH threshold used to compute the convective temperature (J/kg) - -Returns -------- -float - The convective temperature (K) -)pbdoc"); - - m_params.def( - "convective_temperature", - [](sharp::lifter_lut& lifter, - const_prof_arr_t pres, const_prof_arr_t hght, const_prof_arr_t tmpk, - const_prof_arr_t vtmpk, const_prof_arr_t mixr, float cinh_thresh) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float cnvtv_tmpk = sharp::convective_temperature( - lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), - mixr.data(), pcl_vtmp, pcl_buoy, pres.size()); - - delete[] pcl_vtmp; - delete[] pcl_buoy; - - return cnvtv_tmpk; - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("virtual_temperature"), - nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, - R"pbdoc( -Computes the convective temperature by iteratively lifting parcels from -the surface using a lowest 100 hPa mean mixing ratio and increasing -surface temperatures to find the first parcel that reaches the CINH -threshold. The first guess is the current surface temperature. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -virtemp : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -mixratio : numpy.ndarray[dtype=float32] - A 1D NumPy array of water vapor mixing ratio values (g/g) -cinh_thresh : float, default = -1.0 - The CINH threshold used to compute the convective temperature (J/kg) - -Returns -------- -float - The convective temperature (K) -)pbdoc"); - - m_params.def( - "convective_temperature", - [](sharp::lifter_lut& lifter, const_prof_arr_t pres, - const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t vtmpk, - const_prof_arr_t mixr, float cinh_thresh) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float cnvtv_tmpk = sharp::convective_temperature( - lifter, pres.data(), hght.data(), tmpk.data(), vtmpk.data(), - mixr.data(), pcl_vtmp, pcl_buoy, pres.size()); - - delete[] pcl_vtmp; - delete[] pcl_buoy; - - return cnvtv_tmpk; - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("height"), - nb::arg("temperature"), nb::arg("virtual_temperature"), - nb::arg("mixratio"), nb::arg("cinh_thresh") = -1.0, - R"pbdoc( -Computes the convective temperature by iteratively lifting parcels from -the surface using a lowest 100 hPa mean mixing ratio and increasing -surface temperatures to find the first parcel that reaches the CINH -threshold. The first guess is the current surface temperature. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -virtemp : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -mixratio : numpy.ndarray[dtype=float32] - A 1D NumPy array of water vapor mixing ratio values (g/g) -cinh_thresh : float, default = -1.0 - The CINH threshold used to compute the convective temperature (J/kg) - -Returns -------- -float - The convective temperature (K) -)pbdoc"); - - m_params.def( - "precipitable_water", - [](sharp::PressureLayer& layer, const_prof_arr_t pres, - const_prof_arr_t mixr) { - if ((pres.shape(0) != mixr.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - return sharp::precipitable_water(layer, pres.data(), mixr.data(), - pres.size()); - }, - - nb::arg("layer"), nb::arg("pres"), nb::arg("mixr"), - R"pbdoc( -Given a PressureLayer to integrate over, compute the precipitable water -from the given pressure and mixing ratio arrays. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.PressureLayer - a PressureLayer over which to integrate (Pa) -pres : numpy.ndarray[dtype=float32] - 1D NumPy array of presssure values (Pa) -mixr : numpy.ndarray[dtype=float32] - 1D NumPy array of water vapor mixing ratio values (unitless) +layer : nwsspc.sharp.calc.layer.PressureLayer + a PressureLayer over which to integrate (Pa) +pres : numpy.ndarray[dtype=float32] + 1D NumPy array of presssure values (Pa) +mixr : numpy.ndarray[dtype=float32] + 1D NumPy array of water vapor mixing ratio values (unitless) Returns ------- @@ -1044,10 +696,7 @@ float m_params.def( "hail_growth_layer", [](const_prof_arr_t pres, const_prof_arr_t tmpk) { - if ((pres.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } + check_equal_sizes(pres, tmpk); return sharp::hail_growth_layer(pres.data(), tmpk.data(), pres.size()); }, @@ -1074,10 +723,7 @@ nwsspc.sharp.calc.layer.PressureLayer m_params.def( "dendritic_layer", [](const_prof_arr_t pres, const_prof_arr_t tmpk) { - if ((pres.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } + check_equal_sizes(pres, tmpk); return sharp::dendritic_layer(pres.data(), tmpk.data(), pres.size()); }, @@ -1155,22 +801,17 @@ float m_params.def( "equilibrium_moisture_content", [](const_prof_arr_t temperature, const_prof_arr_t rel_humidity) { + check_equal_sizes(temperature, rel_humidity); + const std::size_t NZ = temperature.size(); auto tmpk = temperature.view(); auto relh = rel_humidity.view(); - if ((tmpk.shape(0) != relh.shape(0))) { - throw nb::buffer_error( - "temperature and rel_humidity must have the same size!"); - } - - float* emc_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - emc_arr[k] = - sharp::equilibrium_moisture_content(tmpk(k), relh(k)); - } - - nb::capsule owner(emc_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(emc_arr, {tmpk.shape(0)}, owner); + + return make_output_array(NZ, [&](float* out) { + for (size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::equilibrium_moisture_content(tmpk(k), relh(k)); + } + }); }, nb::arg("temperature"), nb::arg("rel_humidity"), R"pbdoc( @@ -1215,24 +856,18 @@ float "fosberg_fire_index", [](const_prof_arr_t temperature, const_prof_arr_t rel_humidity, const_prof_arr_t wind_speed) { + check_equal_sizes(temperature, rel_humidity, wind_speed); + const std::size_t NZ = temperature.size(); auto tmpk = temperature.view(); auto relh = rel_humidity.view(); auto wspd = wind_speed.view(); - if ((tmpk.shape(0) != relh.shape(0)) || - (tmpk.shape(0) != wspd.shape(0))) { - throw nb::buffer_error( - "temperature, rel_humidity, and wind_speed must have the " - "same sizes!"); - } - - float* fwwi = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - fwwi[k] = sharp::fosberg_fire_index(tmpk(k), relh(k), wspd(k)); - } - - nb::capsule owner(fwwi, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(fwwi, {tmpk.shape(0)}, owner); + + return make_output_array(NZ, [&](float* out) { + for (size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::fosberg_fire_index(tmpk(k), relh(k), wspd(k)); + } + }); }, nb::arg("temperature"), nb::arg("rel_humidity"), nb::arg("wind_speed"), R"pbdoc( @@ -1253,130 +888,7 @@ numpy.ndarray[dtype=float32] Fosberg Fire-Weather Index )pbdoc"); - m_params.def( - "pyrocumulonimbus_firepower_threshold", - [](sharp::lifter_cm1& lifter, sharp::PressureLayer& mix_layer, - const_prof_arr_t pres, const_prof_arr_t hght, const_prof_arr_t tmpk, - const_prof_arr_t mixr, const_prof_arr_t vtmpk, const_prof_arr_t uwin, - const_prof_arr_t vwin, const_prof_arr_t theta, sharp::Parcel* pcl, - float phi, float beta_incr) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0)) || - (pres.shape(0) != theta.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float pft = sharp::pyrocumulonimbus_firepower_threshold( - lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), - mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), - theta.data(), pcl_vtmp, pcl_buoy, pres.size(), pcl, phi, - beta_incr); - - delete[] pcl_vtmp; - delete[] pcl_buoy; - - return pft; - }, - nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), - nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), - nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), - nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, - R"pbdoc( -Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum -amount of firepower required to generate pyrocumulonimbus clouds for a -given atmospheric profile. Requires a PressureLayer to define a mixing -layer used to average values of potential temperature, mixing ratio, -and wind speed. The beta increment determines how to vary the plume -buoyancy factor, with smaller values resulting in more iteration steps. -Phi is the fire moisture to potential temperature increment ratio. - -Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. -If a parcel is passed, the values will be set with the PFT fire parcel. - -References ----------- -Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml -Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 -mix_layer : nwsspc.sharp.calc.layer.PressureLayer - A mixing layer to take average values of the environment from -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -mixratio : numpy.ndarray[dtype=float32] - A 1D NumPy array of water vapor mixing ratio values (g/g) -virtual_temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -u_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of U wind component values (m/s) -v_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of V wind compnent values (m/s) -potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of potential temperature values (K) -pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional - If a parcel is provided, returns the parcel for the PFT -phi : float - The fire moisture to potential temperature increment ratio (kg/(kg*K)) -beta_incr : float - The fire plume buoyancy factor (unitless) - -Returns -------- -float - The PyroCB Firepower Threshold (Watts) - -)pbdoc"); - - m_params.def( - "pyrocumulonimbus_firepower_threshold", - [](sharp::lifter_wobus& lifter, sharp::PressureLayer& mix_layer, - const_prof_arr_t pres, const_prof_arr_t hght, const_prof_arr_t tmpk, - const_prof_arr_t mixr, const_prof_arr_t vtmpk, const_prof_arr_t uwin, - const_prof_arr_t vwin, const_prof_arr_t theta, sharp::Parcel* pcl, - float phi, float beta_incr) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0)) || - (pres.shape(0) != theta.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float pft = sharp::pyrocumulonimbus_firepower_threshold( - lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), - mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), - theta.data(), pcl_vtmp, pcl_buoy, pres.size(), pcl, phi, - beta_incr); - - delete[] pcl_vtmp; - delete[] pcl_buoy; - - return pft; - }, - nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), - nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), - nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), - nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, + const char* pft_template_doc = R"pbdoc( Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum amount of firepower required to generate pyrocumulonimbus clouds for a @@ -1396,7 +908,7 @@ Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20- Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus +lifter : {} mix_layer : nwsspc.sharp.calc.layer.PressureLayer A mixing layer to take average values of the environment from pressure : numpy.ndarray[dtype=float32] @@ -1427,185 +939,12 @@ Returns float The PyroCB Firepower Threshold (Watts) -)pbdoc"); - - m_params.def( - "pyrocumulonimbus_firepower_threshold", - [](sharp::lifter_lut& lifter, - sharp::PressureLayer& mix_layer, const_prof_arr_t pres, - const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t mixr, - const_prof_arr_t vtmpk, const_prof_arr_t uwin, const_prof_arr_t vwin, - const_prof_arr_t theta, sharp::Parcel* pcl, float phi, - float beta_incr) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0)) || - (pres.shape(0) != theta.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float pft = sharp::pyrocumulonimbus_firepower_threshold( - lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), - mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), - theta.data(), pcl_vtmp, pcl_buoy, pres.size(), pcl, phi, - beta_incr); - - delete[] pcl_vtmp; - delete[] pcl_buoy; +)pbdoc"; - return pft; - }, - nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), - nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), - nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), - nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, - R"pbdoc( -Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum -amount of firepower required to generate pyrocumulonimbus clouds for a -given atmospheric profile. Requires a PressureLayer to define a mixing -layer used to average values of potential temperature, mixing ratio, -and wind speed. The beta increment determines how to vary the plume -buoyancy factor, with smaller values resulting in more iteration steps. -Phi is the fire moisture to potential temperature increment ratio. - -Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. -If a parcel is passed, the values will be set with the PFT fire parcel. - -References ----------- -Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml -Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut -mix_layer : nwsspc.sharp.calc.layer.PressureLayer - A mixing layer to take average values of the environment from -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -mixratio : numpy.ndarray[dtype=float32] - A 1D NumPy array of water vapor mixing ratio values (g/g) -virtual_temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -u_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of U wind component values (m/s) -v_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of V wind compnent values (m/s) -potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of potential temperature values (K) -pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional - If a parcel is provided, returns the parcel for the PFT -phi : float - The fire moisture to potential temperature increment ratio (kg/(kg*K)) -beta_incr : float - The fire plume buoyancy factor (unitless) - -Returns -------- -float - The PyroCB Firepower Threshold (Watts) - -)pbdoc"); - - m_params.def( - "pyrocumulonimbus_firepower_threshold", - [](sharp::lifter_lut& lifter, - sharp::PressureLayer& mix_layer, const_prof_arr_t pres, - const_prof_arr_t hght, const_prof_arr_t tmpk, const_prof_arr_t mixr, - const_prof_arr_t vtmpk, const_prof_arr_t uwin, const_prof_arr_t vwin, - const_prof_arr_t theta, sharp::Parcel* pcl, float phi, - float beta_incr) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != mixr.shape(0)) || - (pres.shape(0) != vtmpk.shape(0)) || - (pres.shape(0) != uwin.shape(0)) || - (pres.shape(0) != vwin.shape(0)) || - (pres.shape(0) != theta.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - float* pcl_vtmp = new float[hght.size()]; - float* pcl_buoy = new float[hght.size()]; - - float pft = sharp::pyrocumulonimbus_firepower_threshold( - lifter, mix_layer, pres.data(), hght.data(), tmpk.data(), - mixr.data(), vtmpk.data(), uwin.data(), vwin.data(), - theta.data(), pcl_vtmp, pcl_buoy, pres.size(), pcl, phi, - beta_incr); - - delete[] pcl_vtmp; - delete[] pcl_buoy; - - return pft; - }, - nb::arg("lifter"), nb::arg("mix_layer"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), nb::arg("mixratio"), - nb::arg("virtual_temperature"), nb::arg("u_wind"), nb::arg("v_wind"), - nb::arg("potential_temperature"), nb::arg("pcl") = nb::none(), - nb::arg("phi") = 6.67e-5, nb::arg("beta_incr") = 0.005, - R"pbdoc( -Computes the Pyrocumulonimbus Firepower Threshold (PFT), or the minimum -amount of firepower required to generate pyrocumulonimbus clouds for a -given atmospheric profile. Requires a PressureLayer to define a mixing -layer used to average values of potential temperature, mixing ratio, -and wind speed. The beta increment determines how to vary the plume -buoyancy factor, with smaller values resulting in more iteration steps. -Phi is the fire moisture to potential temperature increment ratio. - -Default values for beta_incr and phi are 0.005 and 6.67e-5, respectively. -If a parcel is passed, the values will be set with the PFT fire parcel. - -References ----------- -Tory et al. 2018: https://journals.ametsoc.org/view/journals/mwre/146/8/mwr-d-17-0377.1.xml -Tory et al. 2021: https://journals.ametsoc.org/view/journals/wefo/36/2/WAF-D-20-0027.1.xml - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut -mix_layer : nwsspc.sharp.calc.layer.PressureLayer - A mixing layer to take average values of the environment from -pressure : numpy.ndarray[dtype=float32] - A 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - A 1D NumPy array of height values (Pa) -temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of temperature values (K) -mixratio : numpy.ndarray[dtype=float32] - A 1D NumPy array of water vapor mixing ratio values (g/g) -virtual_temperature : numpy.ndarray[dtype=float32] - A 1D NumPy array of virtual temperature values (K) -u_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of U wind component values (m/s) -v_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of V wind compnent values (m/s) -potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of potential temperature values (K) -pcl : None or nwsspc.sharp.calc.parcel.Parcel, optional - If a parcel is provided, returns the parcel for the PFT -phi : float - The fire moisture to potential temperature increment ratio (kg/(kg*K)) -beta_incr : float - The fire plume buoyancy factor (unitless) - -Returns -------- -float - The PyroCB Firepower Threshold (Watts) - -)pbdoc"); + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_pyrocb_firepower(m_params, pft_template_doc, lifter_name); + }); } #endif diff --git a/src/nanobind/parcel_bindings.h b/src/nanobind/parcel_bindings.h index 87e49661..86289b87 100644 --- a/src/nanobind/parcel_bindings.h +++ b/src/nanobind/parcel_bindings.h @@ -9,18 +9,401 @@ // clang-format on #include #include +#include +#include + +#include "binding_utils.h" #include "sharplib_types.h" namespace nb = nanobind; +template +void bind_lift_parcel(nb::class_& cls, const char* doc_template, + const char* lifter_name) { + std::string doc = fmt::format(doc_template, lifter_name); + cls.def( + "lift_parcel", + [](sharp::Parcel& pcl, Lft& lifter, const_prof_arr_t pressure) { + return make_output_array(pressure.size(), [&](float* out) { + pcl.lift_parcel(lifter, pressure.data(), out, pressure.size()); + }); + }, + nb::arg("lifter"), nb::arg("pressure"), doc.c_str()); +} + +template +void bind_mu_parcel(nb::class_& cls, const char* doc_template, + const char* layer_name, const char* lifter_name) { + std::string doc = fmt::format(doc_template, layer_name, lifter_name); + cls.def_static( + "most_unstable_parcel", + [](Layer& layer, Lft& lifter, const_prof_arr_t pressure, + const_prof_arr_t height, const_prof_arr_t temperature, + const_prof_arr_t virtemp, const_prof_arr_t dewpoint) { + check_equal_sizes(pressure, temperature, dewpoint, height, virtemp); + + const std::size_t NZ = height.size(); + auto pcl_virtemp_arr = std::make_unique(NZ); + auto pcl_buoy_arr = std::make_unique(NZ); + + sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( + layer, lifter, pressure.data(), height.data(), + temperature.data(), virtemp.data(), dewpoint.data(), + pcl_virtemp_arr.get(), pcl_buoy_arr.get(), NZ); + + return max_pcl; + }, + nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), + nb::arg("height"), nb::arg("temperature"), + nb::arg("virtual_temperature"), nb::arg("dewpoint"), doc.c_str()); +} + +template +void bind_ml_parcel(nb::class_& cls, const char* doc) { + if constexpr (Layer::coord == sharp::LayerCoordinate::height) { + cls.def_static( + "mixed_layer_parcel", + [](Layer& mix_layer, const_prof_arr_t pressure, + const_prof_arr_t height, const_prof_arr_t potential_temperature, + const_prof_arr_t mixing_ratio) { + check_equal_sizes(pressure, potential_temperature, mixing_ratio, + height); + return sharp::Parcel::mixed_layer_parcel( + mix_layer, pressure.data(), height.data(), + potential_temperature.data(), mixing_ratio.data(), + pressure.size()); + }, + nb::arg("mix_layer"), nb::arg("pressure"), nb::arg("height"), + nb::arg("potential_temperature"), nb::arg("mixing_ratio"), doc); + } else { + cls.def_static( + "mixed_layer_parcel", + [](Layer& mix_layer, const_prof_arr_t pressure, + const_prof_arr_t potential_temperature, + const_prof_arr_t mixing_ratio) { + check_equal_sizes(pressure, potential_temperature, + mixing_ratio); + return sharp::Parcel::mixed_layer_parcel( + mix_layer, pressure.data(), nullptr, + potential_temperature.data(), mixing_ratio.data(), + pressure.size()); + }, + nb::arg("mix_layer"), nb::arg("pressure"), + nb::arg("potential_temperature"), nb::arg("mixing_ratio"), doc); + } +} + +template +void bind_lut_data_class(nb::class_>& cls) { + cls.def(nb::init(), + nb::arg("lifter"), nb::arg("pmin") = 5000.0f, + nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, + nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, + nb::arg("n_thetae") = 221) + .def_ro("pres_min", &sharp::lut_data::pres_min) + .def_ro("pres_max", &sharp::lut_data::pres_max) + .def_ro("thte_min", &sharp::lut_data::thetae_min) + .def_ro("thte_max", &sharp::lut_data::thetae_max) + .def_ro("num_logp", &sharp::lut_data::num_logp) + .def_ro("num_thetae", &sharp::lut_data::num_thetae); +} + +template +void bind_lut_data(nb::module_& mod, const char* doc_template, + const char* lifter_name, const char* return_name) { + std::string doc = fmt::format(doc_template, lifter_name, return_name); + mod.def( + "lut_data", + [](Lft& lifter, float pmin, float pmax, float thte_min, float thte_max, + std::size_t n_logp, std::size_t n_thetae) { + return sharp::lut_data(lifter, pmin, pmax, thte_min, thte_max, + n_logp, n_thetae); + }, + nb::arg("lifter"), nb::arg("pmin") = 5000.0f, + nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, + nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, + nb::arg("n_thetae") = 221, doc.c_str()); +} + +template +void bind_lifter_lut_class(nb::class_>& cls) { + cls.def(nb::init>>(), + nb::arg("data")) + .def_ro_static("lift_from_lcl", &sharp::lifter_lut::lift_from_lcl, + R"pbdoc( +A static flag that helps the parcel lifting functions know where to lift from. +)pbdoc") + .def("setup", &sharp::lifter_lut::setup, nb::arg("lcl_pres"), + nb::arg("lcl_tmpk"), R"pbdoc( +Performs a setup step based on the LCL attributes. + +Computes the fractional index needed to select the correct pseudoadiabat +for lookup. If the LCL is outside the table bounds, it falls back to the +direct solver. + +Parameters +---------- +lcl_pres : float + The LCL pressure (Pa) +lcl_tmpk : float + The LCL temperature (K) + +Returns +------- +None +)pbdoc") + .def("__call__", &sharp::lifter_lut::operator(), nb::is_operator(), + nb::arg("pres"), nb::arg("tmpk"), nb::arg("new_pres"), R"pbdoc( +Performs LUT interpolation to lift a parcel moist adiabatically. + +Parameters +---------- +pres : float + Parcel pressure (Pa) +tmpk : float + Parcel temperature (K) +new_pres : float + Final level of parcel after lift (Pa) + +Returns +------- +float + The temperature of the lifted parcel (K) +)pbdoc") + .def("parcel_virtual_temperature", + &sharp::lifter_lut::parcel_virtual_temperature, + nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( +Computes the virtual temperature of the parcel (after saturation). + +Parameters +---------- +pres : float + Parcel pressure (Pa) +tmpk : float + Parcel temperature (K) + +Returns +------- +float + The virtual temperature of the parcel (K) +)pbdoc"); +} + +template +void bind_lifter_lut(nb::module_& mod, const char* doc_template, + const char* lut_name, const char* return_name) { + std::string doc = fmt::format(doc_template, lut_name, return_name); + mod.def( + "lifter_lut", + [](std::shared_ptr>& lut) { + return sharp::lifter_lut(std::move(lut)); + }, + nb::arg("lut"), doc.c_str()); +} + +template +void bind_lower_parcel(nb::class_& cls, + const char* doc_template, const char* lifter_name) { + std::string doc = fmt::format(doc_template, lifter_name); + cls.def( + "lower_parcel", + [](sharp::DowndraftParcel& pcl, Lft& lifter, + const_prof_arr_t pressure) { + return make_output_array(pressure.size(), [&](float* out) { + pcl.lower_parcel(lifter, pressure.data(), out, pressure.size()); + }); + }, + nb::arg("lifter"), nb::arg("pressure"), doc.c_str()); +} + inline void make_parcel_bindings(nb::module_ m) { nb::module_ m_parcel = m.def_submodule( "parcel", "Sounding and Hodograph Analysis and Research Program Library " "(SHARPlib) :: Parcel Lifting Routines"); - // Bind the constructors, named fields, and default arguments + const char* lift_parcel_template_doc = + R"pbdoc( +Lifts a Parcel dry adiabatically from its LPL to its LCL dry +adiabatically, and then moist adiabatically from the LCL to +the top of the profile. The moist adiabat used is determined +by the type of lifting functor passed to the function (i.e. +lifter_wobus or lifter_cm1). + +Parameters +---------- +lifter : {} + An instantiated lifter functor +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + +Returns +------- +numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel virtual temperature values (K) + )pbdoc"; + + const char* mu_pcl_template_doc = + R"pbdoc( +Given input arrays of pressure, height, temperature, virtual temperature, +and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and +parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + +Parameters +---------- +layer : {} + Layer for which to search for the Most Unstable Parcel +lifter : {} + Parcel lifting routine to use for moist ascent +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile temperature values (K) +virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile virtual temperature values (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of profile dewpoint values (K) + +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with most-unstable values + )pbdoc"; + + const char* ml_pcl_pres_doc = + R"pbdoc( +Given input arrays of pressure, potential temperature, +and water vapor mixing ratio, as well as a defined PressureLayer, +compute and return a mixed-layer Parcel. + +Parameters +---------- +mix_layer : nwsspc.sharp.calc.layer.PressureLayer + PressureLayer over which to compute a mixed-layer parcel +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile potential temperature (K) +mixing_ratio : numpy.ndarray[dtype=float32] + 1D NumPy array of profile water vapor mixing ratio (unitless) + +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with mixed layer values + +)pbdoc"; + + const char* ml_pcl_hght_doc = + R"pbdoc( +Given input arrays of pressure, potential temperature, and water +vapor mixing ratio, as well as a defined PressureLayer, compute +and return a mixed-layer Parcel. + +Parameters +---------- +mix_layer : nwsspc.sharp.calc.layer.HeightLayer + HeightLayer over which to compute a mixed-layer parcel +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of profile pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of profile height values (meters) +potential_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of profile potential temperature (K) +mixing_ratio : numpy.ndarray[dtype=float32] + 1D NumPy array of profile water vapor mixing ratio (unitless) + +Returns +------- +nwsspc.sharp.calc.parcel.Parcel + Parcel with mixed layer values + )pbdoc"; + + const char* lut_data_template_doc = + R"pbdoc( +Constructs the lookup table (LUT) data for a parcel lifter. + +Parameters +---------- +lifter : {} +pmin : float + The minimum pressure of the lookup table (Pa) +pmax : float + The maximum pressure of the lookup table (Pa) +thte_min : float + The minimum thetae of the lookup table (K) +thte_max : float + The maximum thetae of the lookup table (K) +n_logp : uint + The number of logp levels for the lookup table +n_thetae : uint + The number of thetae levels for the lookup table + +Returns +------- +{} + A lookup table for the given lifter +)pbdoc"; + + const char* lifter_lut_class_template_doc = + R"pbdoc( +A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) +for fast moist adiabatic ascent calculations. + +Instead of directly solving the moist ascent ODEs, this lifter uses +bilinear interpolation of a precomputed lookup table to determine the +parcel temperature. If the parcel's LCL falls outside the table bounds, +it falls back to the direct {} solver automatically. + +LUT based parcel ascent only works for pseudoadiabats. Constructing the +LUT with a reversible adiabat type will result in an error being thrown. + +Parameters +---------- +data : {} + A shared lookup table constructed with a lifter_{} instance. +)pbdoc"; + + const char* lifter_lut_template_doc = + R"pbdoc( +Constructs the parcel lifter from a LUT. + +Parameters +---------- +lut : {} + +Returns +------- +{} +)pbdoc"; + + const char* lower_parcel_template_doc = + R"pbdoc( +Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist +adiabatically from its LPL to the surface. The moist adiabat used +is determined by the type of lifting functor passed to the function +(i.e. lifter_wobus or lifter_cm1). + +Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual +temperature correction is not used for downdraft parcels. + +Parameters +---------- +lifter : {} + An instantiated lifter functor +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of Pressure levels for lifting (Pa) + +Returns +------- +numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) + )pbdoc"; + nb::class_(m_parcel, "lifter_wobus", R"pbdoc( A functor that calls the Wobus Wetlift for computation of moist adiabats. @@ -173,311 +556,30 @@ float The virtual temperature of the parcel (K) )pbdoc"); - nb::class_>(m_parcel, "lut_data_wobus") - .def(nb::init(), - nb::arg("lifter"), nb::arg("pmin") = 5000.0f, - nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, - nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, - nb::arg("n_thetae") = 221) - .def_ro("pres_min", &sharp::lut_data::pres_min) - .def_ro("pres_max", &sharp::lut_data::pres_max) - .def_ro("thte_min", &sharp::lut_data::thetae_min) - .def_ro("thte_max", &sharp::lut_data::thetae_max) - .def_ro("num_logp", &sharp::lut_data::num_logp) - .def_ro("num_thetae", - &sharp::lut_data::num_thetae); - - nb::class_>(m_parcel, "lut_data_cm1") - .def(nb::init(), - nb::arg("lifter"), nb::arg("pmin") = 5000.0f, - nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, - nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, - nb::arg("n_thetae") = 221) - .def_ro("pres_min", &sharp::lut_data::pres_min) - .def_ro("pres_max", &sharp::lut_data::pres_max) - .def_ro("thte_min", &sharp::lut_data::thetae_min) - .def_ro("thte_max", &sharp::lut_data::thetae_max) - .def_ro("num_logp", &sharp::lut_data::num_logp) - .def_ro("num_thetae", &sharp::lut_data::num_thetae); - - m_parcel.def( - "lut_data", - [](sharp::lifter_wobus& lifter, float pmin, float pmax, float thte_min, - float thte_max, std::size_t n_logp, std::size_t n_thetae) { - return sharp::lut_data(lifter, pmin, pmax, thte_min, thte_max, - n_logp, n_thetae); - }, - nb::arg("lifter"), nb::arg("pmin") = 5000.0f, - nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, - nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, - nb::arg("n_thetae") = 221, - R"pbdoc( -Constructs the lookup table (LUT) data for a parcel lifter. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus -pmin : float - The minimum pressure of the lookup table (Pa) -pmax : float - The maximum pressure of the lookup table (Pa) -thte_min : float - The minimum thetae of the lookup table (K) -thte_max : float - The maximum thetae of the lookup table (K) -n_logp : uint - The number of logp levels for the lookup table -n_thetae : uint - The number of thetae levels for the lookup table - -Returns -------- -nwsspc.sharp.calc.parcel.lut_data_wobus - A lookup table for nwsspc.sharp.calc.parcel.lifter_wobus -)pbdoc"); - - m_parcel.def( - "lut_data", - [](sharp::lifter_cm1& lifter, float pmin, float pmax, float thte_min, - float thte_max, std::size_t n_logp, std::size_t n_thetae) { - return sharp::lut_data(lifter, pmin, pmax, thte_min, thte_max, - n_logp, n_thetae); - }, - nb::arg("lifter"), nb::arg("pmin") = 5000.0f, - nb::arg("pmax") = 110000.0f, nb::arg("thte_min") = 210.0f, - nb::arg("thte_max") = 430.0f, nb::arg("n_logp") = 201, - nb::arg("n_thetae") = 221, - R"pbdoc( -Constructs the lookup table (LUT) data for a parcel lifter. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 -pmin : float - The minimum pressure of the lookup table (Pa) -pmax : float - The maximum pressure of the lookup table (Pa) -thte_min : float - The minimum thetae of the lookup table (K) -thte_max : float - The maximum thetae of the lookup table (K) -n_logp : uint - The number of logp levels for the lookup table -n_thetae : uint - The number of thetae levels for the lookup table - -Returns -------- -nwsspc.sharp.calc.parcel.lut_data_wobus - A lookup table for nwsspc.sharp.calc.parcel.lifter_cm1 -)pbdoc"); - - nb::class_>(m_parcel, - "lifter_lut_wobus", - R"pbdoc( -A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) -for fast moist adiabatic ascent calculations. - -Instead of directly solving the moist ascent ODEs, this lifter uses -bilinear interpolation of a precomputed lookup table to determine the -parcel temperature. If the parcel's LCL falls outside the table bounds, -it falls back to the direct Wobus solver automatically. - -LUT based parcel ascent only works for pseudoadiabats. Constructing the -LUT with a reversible adiabat type will result in an error being thrown. - -Parameters ----------- -data : nwsspc.sharp.calc.parcel.lut_data - A shared lookup table constructed with a lifter_wobus instance. -)pbdoc") - .def(nb::init< - std::shared_ptr>>(), - nb::arg("data")) - .def_ro_static("lift_from_lcl", - &sharp::lifter_lut::lift_from_lcl, - R"pbdoc( -A static flag that helps the parcel lifting functions know where to lift from. -)pbdoc") - .def("setup", &sharp::lifter_lut::setup, - nb::arg("lcl_pres"), nb::arg("lcl_tmpk"), R"pbdoc( -Performs a setup step based on the LCL attributes. - -Computes the fractional index needed to select the correct pseudoadiabat -for lookup. If the LCL is outside the table bounds, it falls back to the -direct solver. - -Parameters ----------- -lcl_pres : float - The LCL pressure (Pa) -lcl_tmpk : float - The LCL temperature (K) - -Returns -------- -None -)pbdoc") - .def("__call__", &sharp::lifter_lut::operator(), - nb::is_operator(), nb::arg("pres"), nb::arg("tmpk"), - nb::arg("new_pres"), R"pbdoc( -Performs LUT interpolation to lift a parcel moist adiabatically. - -Parameters ----------- -pres : float - Parcel pressure (Pa) -tmpk : float - Parcel temperature (K) -new_pres : float - Final level of parcel after lift (Pa) - -Returns -------- -float - The temperature of the lifted parcel (K) -)pbdoc") - .def( - "parcel_virtual_temperature", - &sharp::lifter_lut::parcel_virtual_temperature, - nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( -Computes the virtual temperature of the parcel (after saturation). - -Parameters ----------- -pres : float - Parcel pressure (Pa) -tmpk : float - Parcel temperature (K) - -Returns -------- -float - The virtual temperature of the parcel (K) -)pbdoc"); - - nb::class_>(m_parcel, "lifter_lut_cm1", - R"pbdoc( -A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) -for fast moist adiabatic ascent calculations. - -Instead of directly solving the moist ascent ODEs, this lifter uses -bilinear interpolation of a precomputed lookup table to determine the -parcel temperature. If the parcel's LCL falls outside the table bounds, -it falls back to the direct CM1 solver automatically. - -LUT based parcel ascent only works for pseudoadiabats. Constructing the -LUT with a reversible adiabat type will result in an error being thrown. - -Parameters ----------- -data : lut_data - A shared lookup table constructed with a lifter_cm1 instance. -)pbdoc") - .def(nb::init< - std::shared_ptr>>(), - nb::arg("data")) - .def_ro_static("lift_from_lcl", - &sharp::lifter_lut::lift_from_lcl, - R"pbdoc( -A static flag that helps the parcel lifting functions know where to lift from. -The lifter_cm1 lifts from the last lifted level, rather than the LCL, because -it is an iterative solver. This results in major performance improvements while -maintaining accuracy. -)pbdoc") - .def("setup", &sharp::lifter_lut::setup, - nb::arg("lcl_pres"), nb::arg("lcl_tmpk"), R"pbdoc( -Performs a setup step based on the LCL attributes. - -Computes the fractional index needed to select the correct pseudoadiabat -for lookup. If the LCL is outside the table bounds, it falls back to the -direct solver. - -Parameters ----------- -lcl_pres : float - The LCL pressure (Pa) -lcl_tmpk : float - The LCL temperature (K) - -Returns -------- -None -)pbdoc") - .def("__call__", &sharp::lifter_lut::operator(), - nb::is_operator(), nb::arg("pres"), nb::arg("tmpk"), - nb::arg("new_pres"), R"pbdoc( -Performs LUT interpolation to lift a parcel moist adiabatically. - -Parameters ----------- -pres : float - Parcel pressure (Pa) -tmpk : float - Parcel temperature (K) -new_pres : float - Final level of parcel after lift (Pa) + for_each_base_lifter([&](auto tag, const char* suffix, + const char* display_name, const char* lifter_fqn, + const char* lut_data_fqn, + const char* lifter_lut_fqn) { + using Lft = typename decltype(tag)::type; -Returns -------- -float - The temperature of the lifted parcel (K) -)pbdoc") - .def("parcel_virtual_temperature", - &sharp::lifter_lut::parcel_virtual_temperature, - nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( -Computes the virtual temperature of the parcel (after saturation). - -Parameters ----------- -pres : float - Parcel pressure (Pa) -tmpk : float - Parcel temperature (K) - -Returns -------- -float - The virtual temperature of the parcel (K) -)pbdoc"); - - m_parcel.def( - "lifter_lut", - [](std::shared_ptr>& lut) { - return sharp::lifter_lut(std::move(lut)); - }, - nb::arg("lut"), - R"pbdoc( -Constructs the parcel lifter from a LUT. - -Parameters ----------- -lut : nwsspc.sharp.calc.parcel.lut_data_wobus - -Returns -------- -nwsspc.sharp.calc.parcel.lifter_lut_wobus -)pbdoc"); + std::string lut_data_cls_name = fmt::format("lut_data_{}", suffix); + auto lut_data_cls = nb::class_>( + m_parcel, lut_data_cls_name.c_str()); + bind_lut_data_class(lut_data_cls); - m_parcel.def( - "lifter_lut", - [](std::shared_ptr>& lut) { - return sharp::lifter_lut(std::move(lut)); - }, - nb::arg("lut"), - R"pbdoc( -Constructs the parcel lifter from a LUT. + bind_lut_data(m_parcel, lut_data_template_doc, lifter_fqn, + lut_data_fqn); -Parameters ----------- -lut : nwsspc.sharp.calc.parcel.lut_data_cm1 + std::string lifter_lut_cls_name = fmt::format("lifter_lut_{}", suffix); + std::string lifter_lut_cls_doc = fmt::format( + lifter_lut_class_template_doc, display_name, lut_data_fqn, suffix); + auto lifter_lut_cls = nb::class_>( + m_parcel, lifter_lut_cls_name.c_str(), lifter_lut_cls_doc.c_str()); + bind_lifter_lut_class(lifter_lut_cls); -Returns -------- -nwsspc.sharp.calc.parcel.lifter_lut_cm1 -)pbdoc"); + bind_lifter_lut(m_parcel, lifter_lut_template_doc, lut_data_fqn, + lifter_lut_fqn); + }); nb::enum_(m_parcel, "LPL") .value("SFC", sharp::LPL::SFC, "A Surface Based Parcel") @@ -486,43 +588,44 @@ nwsspc.sharp.calc.parcel.lifter_lut_cm1 .value("ML", sharp::LPL::ML, "A Mixed-Layer Parcel") .value("USR", sharp::LPL::USR, "A User Defined Parcel"); - nb::class_(m_parcel, "Parcel", R"pbdoc( + auto parcel_class = + nb::class_(m_parcel, "Parcel", R"pbdoc( Contains information about a Parcel's starting level and thermodynamic attributes, as well as derived computations, methods for constructing a parcel, and parcel ascent routines. )pbdoc") - .def_rw("pres", &sharp::Parcel::pres, R"pbdoc( + .def_rw("pres", &sharp::Parcel::pres, R"pbdoc( Parcel starting pressure (Pa) )pbdoc") - .def_rw("tmpk", &sharp::Parcel::tmpk, R"pbdoc( + .def_rw("tmpk", &sharp::Parcel::tmpk, R"pbdoc( Parcel starting temperature (K) )pbdoc") - .def_rw("dwpk", &sharp::Parcel::dwpk, R"pbdoc( + .def_rw("dwpk", &sharp::Parcel::dwpk, R"pbdoc( Parcel starting dewpoint (K) )pbdoc") - .def_rw("lcl_pressure", &sharp::Parcel::lcl_pressure, R"pbdoc( + .def_rw("lcl_pressure", &sharp::Parcel::lcl_pressure, R"pbdoc( Pressure at the Lifted Condensation Level (Pa) )pbdoc") - .def_rw("lfc_pressure", &sharp::Parcel::lfc_pressure, R"pbdoc( + .def_rw("lfc_pressure", &sharp::Parcel::lfc_pressure, R"pbdoc( Pressure at the Level of Free Convection (Pa) )pbdoc") - .def_rw("eql_pressure", &sharp::Parcel::eql_pressure, R"pbdoc( + .def_rw("eql_pressure", &sharp::Parcel::eql_pressure, R"pbdoc( Pressure at the parcel Equilibrium Level )pbdoc") - .def_rw("mpl_pressure", &sharp::Parcel::mpl_pressure, R"pbdoc( + .def_rw("mpl_pressure", &sharp::Parcel::mpl_pressure, R"pbdoc( Pressure at the Maximum Parcel Level )pbdoc") - .def_rw("cape", &sharp::Parcel::cape, R"pbdoc( + .def_rw("cape", &sharp::Parcel::cape, R"pbdoc( Parcel Convective Available Potential Energy (J/kg) between the LFC and EL )pbdoc") - .def_rw("cinh", &sharp::Parcel::cinh, R"pbdoc( + .def_rw("cinh", &sharp::Parcel::cinh, R"pbdoc( Parcel Convective Inhibition (J/kg) between the LFC and EL )pbdoc") - .def(nb::init<>()) - .def( - nb::init(), - nb::arg("pressure"), nb::arg("temperature"), nb::arg("dewpoint"), - nb::arg("lpl"), R"pbdoc( + .def(nb::init<>()) + .def(nb::init(), + nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), nb::arg("lpl"), R"pbdoc( Constructor for a Parcel Parameters @@ -536,904 +639,229 @@ dewpoint : float lpl : nwsspc.sharp.calc.parcel.LPL Parcel Lifted Parcel Level (LPL) definition )pbdoc") - .def( - "lift_parcel", - [](sharp::Parcel& pcl, sharp::lifter_wobus& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* virtemp_arr = new float[NZ]; - - pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); - - nb::capsule owner(virtemp_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lifts a Parcel dry adiabatically from its LPL to its LCL dry -adiabatically, and then moist adiabatically from the LCL to -the top of the profile. The moist adiabat used is determined -by the type of lifting functor passed to the function (i.e. -lifter_wobus or lifter_cm1). - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - An instantiated lifter_wobus functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - )pbdoc") - .def( - "lift_parcel", - [](sharp::Parcel& pcl, sharp::lifter_cm1& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* virtemp_arr = new float[NZ]; - - pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); - - nb::capsule owner(virtemp_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lifts a Parcel dry adiabatically from its LPL to its LCL dry -adiabatically, and then moist adiabatically from the LCL to -the top of the profile. The moist adiabat used is determined -by the type of lifting functor passed to the function (i.e. -lifter_wobus or lifter_cm1). - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - An instantiated lifter_cm1 functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - )pbdoc") - .def( - "lift_parcel", - [](sharp::Parcel& pcl, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* virtemp_arr = new float[NZ]; - - pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); - - nb::capsule owner(virtemp_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lifts a Parcel dry adiabatically from its LPL to its LCL dry -adiabatically, and then moist adiabatically from the LCL to -the top of the profile. The moist adiabat used is determined -by the type of lifting functor passed to the function (i.e. -lifter_wobus or lifter_cm1). - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - )pbdoc") - .def( - "lift_parcel", - [](sharp::Parcel& pcl, sharp::lifter_lut& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* virtemp_arr = new float[NZ]; - - pcl.lift_parcel(lifter, pressure.data(), virtemp_arr, NZ); - - nb::capsule owner(virtemp_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(virtemp_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lifts a Parcel dry adiabatically from its LPL to its LCL dry -adiabatically, and then moist adiabatically from the LCL to -the top of the profile. The moist adiabat used is determined -by the type of lifting functor passed to the function (i.e. -lifter_wobus or lifter_cm1). - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel virtual temperature values (K) - )pbdoc") - .def( - "find_lfc_el", - [](sharp::Parcel& pcl, const_prof_arr_t pres, const_prof_arr_t hght, - const_prof_arr_t buoy) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != buoy.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - pcl.find_lfc_el(pres.data(), hght.data(), buoy.data(), - buoy.size()); - return std::make_tuple(pcl.lfc_pressure, pcl.eql_pressure); - }, - nb::arg("pressure"), nb::arg("height"), nb::arg("buoyancy"), - R"pbdoc( + .def( + "find_lfc_el", + [](sharp::Parcel& pcl, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t buoy) { + check_equal_sizes(pres, hght, buoy); + pcl.find_lfc_el(pres.data(), hght.data(), buoy.data(), + buoy.size()); + return std::make_tuple(pcl.lfc_pressure, pcl.eql_pressure); + }, + nb::arg("pressure"), nb::arg("height"), nb::arg("buoyancy"), + R"pbdoc( Searches the buoyancy array for the LFC and EL combination that results in the maximum amount of CAPE in the given profile. The buoyancy array -is typically computed by calling nwsspc.sharp.calc.parcel.Parcel.lift_parcel. -Once the LFC and EL are found, the value are set in -nwsspc.sharp.calc.parcel.Parcel.lfc_pres and -nwsspc.sharp.calc.parcel.Parcel.eql_pres via the provided parcel. - -The value of eql_pres is MISSING if there is no qualifying level -found within the data bounds (e.g. incomplete data, or EL above -the available data). Any calls to nwsspc.sharp.calc.parcel.Parcel.cape_cinh -will still compute CAPE without the presence of an EL, using the best-available -data. - -Parameters ----------- -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) -buoyancy : numpy.ndarray[dtype=float32] - 1D NumPy array of buoyancy values (m/s^2) - -Returns -------- -tuple[float, float] - (LFC_PRES, EL_PRES) - )pbdoc") - .def( - "maximum_parcel_level", - [](sharp::Parcel& pcl, const_prof_arr_t pres, const_prof_arr_t hght, - const_prof_arr_t buoy) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != buoy.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - - return pcl.maximum_parcel_level(pres.data(), hght.data(), - buoy.data(), pres.shape(0)); - }, - nb::arg("pressure"), nb::arg("height"), nb::arg("buoyancy"), - R"pbdoc( -Find the pressure of the Maximum Parcel Level (MPL). - -The Maximum Parcel Level (MPL) is the level a parcel would reach -if it expended all of its integrated positive buoyancy past the -Equilibrium Level. It is found by integrating negatively buoyant -area above the Equilibrium Level until the integrated negative -buoyancy is equal in magnitude to the Convective Available -Potential Energy between the Level of Free Convection and the -Equilibrium Level. - -For valid calculations, nwsspc.sharp.calc.parcel.Parcel.cape_cinh -must be called first, or nwsspc.sharp.calc.parcel.Parcel.cape and -nwsspc.sharp.calc.parcel.Parcel.eql_pressure must be set. - -A value of nwsspc.sharp.calc.constants.MISSING is returned if: - * CAPE is 0 - * nwsspc.sharp.calc.parcel.Parcel.eql_pressure is MISSING - * No valid MPL candidate is found within the profile - In this scenario, it likely exceeds the top of the available data - -In addition to being returned, the result is stored inside of -nwsspc.sharp.calc.parcel.Parcel.mpl_pressure. - -Parameters ----------- -pres : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values (Pa) -hght : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) -buoyancy : numpy.ndarray[dtype=float32] - 1D NumPy array of buoyancy values (m/s^2) - -Returns -------- -float - The pressure of the Maximum Parcel Level (Pa) - )pbdoc") - .def( - "cape_cinh", - [](sharp::Parcel& pcl, const_prof_arr_t pres, const_prof_arr_t hght, - const_prof_arr_t buoy) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != buoy.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - pcl.cape_cinh(pres.data(), hght.data(), buoy.data(), - buoy.size()); - return std::make_tuple(pcl.cape, pcl.cinh); - }, - nb::arg("pres"), nb::arg("hght"), nb::arg("buoy"), - R"pbdoc( -Assuming that nwsspc.sharp.calc.parcel.Parcel.lift_parcel has -been called, cape_cinh will integrate the area between the LFC -and EL to compute CAPE, and integrate the area between the LPL -and LCL to compute CINH. - -If eql_pressure is MISSING, but lfc_pressure is defined, the -routine will compute CAPE with the available data despite the -lack of a defined equilibrium level. This is useful for -incomplete profile data, or pressure-level data where the EL -is above the top pressure value. - -The results are stored in nwsspc.sharp.calc.parcel.Parcel.cape -and nwsspc.sharp.calc.parcel.Parcel.cinh via the provided parcel. - -Parameters ----------- -pres : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values (Pa) -hght : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) -buoyancy : numpy.ndarray[dtype=float32] - 1D NumPy array of buoyancy values (m/s^2) - -Returns -------- -tuple[float, float] - (CAPE, CINH) - )pbdoc") - .def( - "lifted_index", - [](sharp::Parcel& pcl, const float plev, const_prof_arr_t pres, - const_prof_arr_t vtmpk, const_prof_arr_t pcl_vtmpk) { - if ((pres.shape(0) != vtmpk.shape(0)) || - (pres.shape(0) != pcl_vtmpk.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - return pcl.lifted_index(plev, pres.data(), vtmpk.data(), - pcl_vtmpk.data(), pres.size()); - }, - nb::arg("pres_lev"), nb::arg("pressure"), - nb::arg("virtual_temperature"), - nb::arg("parcel_virtual_temperature"), - R"pbdoc( -Using the parcel and environment virtual temperature, compute the lifted index -at the given level. The pressure level typically used is 500 hPa. The lifted index -is the difference between the environment and parcel virtual temperatures at the -requested level. - -Parameters ----------- -pres_lev : float - The pressure level (Pa) to compute the lifted index at -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure values (Pa) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of environment virtual temperature values (K) -parcel_virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of parcel virtual temperature values (K) - -Returns -------- -float - The lifted index - )pbdoc") - .def_static("surface_parcel", &sharp::Parcel::surface_parcel, - nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), R"pbdoc( -Given input values of surface pressure, temperature, and dewpoint -temperature, construct and return a Surface Based Parcel. - -Parameters ----------- -pressure : float - Surface pressure (Pa) -temperature : float - Surface temperature (K) -dewpoint : float - Surface dewpoint (K) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with surface values - )pbdoc") - .def_static( - "mixed_layer_parcel", - [](sharp::PressureLayer& mix_layer, const_prof_arr_t pressure, - const_prof_arr_t potential_temperature, - const_prof_arr_t mixing_ratio) { - if ((pressure.shape(0) != potential_temperature.shape(0)) || - (pressure.shape(0) != mixing_ratio.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - return sharp::Parcel::mixed_layer_parcel( - mix_layer, pressure.data(), nullptr, - potential_temperature.data(), mixing_ratio.data(), - pressure.size()); - }, - nb::arg("mix_layer"), nb::arg("pressure"), - nb::arg("potential_temperature"), nb::arg("mixing_ratio"), - R"pbdoc( -Given input arrays of pressure, potential temperature, -and water vapor mixing ratio, as well as a defined PressureLayer, -compute and return a mixed-layer Parcel. - -Parameters ----------- -mix_layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer over which to compute a mixed-layer parcel -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile potential temperature (K) -mixing_ratio : numpy.ndarray[dtype=float32] - 1D NumPy array of profile water vapor mixing ratio (unitless) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with mixed layer values - -)pbdoc") - .def_static( - "mixed_layer_parcel", - [](sharp::HeightLayer& mix_layer, const_prof_arr_t pressure, - const_prof_arr_t height, const_prof_arr_t potential_temperature, - const_prof_arr_t mixing_ratio) { - if ((pressure.shape(0) != potential_temperature.shape(0)) || - (pressure.shape(0) != mixing_ratio.shape(0)) || - (pressure.shape(0) != height.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - return sharp::Parcel::mixed_layer_parcel( - mix_layer, pressure.data(), height.data(), - potential_temperature.data(), mixing_ratio.data(), - height.size()); - }, - nb::arg("mix_layer"), nb::arg("pressure"), nb::arg("height"), - nb::arg("potential_temperature"), nb::arg("mixing_ratio"), - R"pbdoc( -Given input arrays of pressure, potential temperature, and water -vapor mixing ratio, as well as a defined PressureLayer, compute -and return a mixed-layer Parcel. - -Parameters ----------- -mix_layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer over which to compute a mixed-layer parcel -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -potential_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile potential temperature (K) -mixing_ratio : numpy.ndarray[dtype=float32] - 1D NumPy array of profile water vapor mixing ratio (unitless) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with mixed layer values - )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::PressureLayer& layer, sharp::lifter_cm1& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values - )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::HeightLayer& layer, sharp::lifter_cm1& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values - )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::PressureLayer& layer, sharp::lifter_wobus& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values - )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::HeightLayer& layer, sharp::lifter_wobus& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. +is typically computed by calling nwsspc.sharp.calc.parcel.Parcel.lift_parcel. +Once the LFC and EL are found, the value are set in +nwsspc.sharp.calc.parcel.Parcel.lfc_pres and +nwsspc.sharp.calc.parcel.Parcel.eql_pres via the provided parcel. + +The value of eql_pres is MISSING if there is no qualifying level +found within the data bounds (e.g. incomplete data, or EL above +the available data). Any calls to nwsspc.sharp.calc.parcel.Parcel.cape_cinh +will still compute CAPE without the presence of an EL, using the best-available +data. Parameters ---------- -layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) + 1D NumPy array of pressure values (Pa) +height : numpy.ndarray[dtype=float32] + 1D NumPy array of height values (meters) +buoyancy : numpy.ndarray[dtype=float32] + 1D NumPy array of buoyancy values (m/s^2) Returns ------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values +tuple[float, float] + (LFC_PRES, EL_PRES) )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::PressureLayer& layer, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + .def( + "maximum_parcel_level", + [](sharp::Parcel& pcl, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t buoy) { + check_equal_sizes(pres, hght, buoy); + return pcl.maximum_parcel_level(pres.data(), hght.data(), + buoy.data(), pres.size()); + }, + nb::arg("pressure"), nb::arg("height"), nb::arg("buoyancy"), + R"pbdoc( +Find the pressure of the Maximum Parcel Level (MPL). + +The Maximum Parcel Level (MPL) is the level a parcel would reach +if it expended all of its integrated positive buoyancy past the +Equilibrium Level. It is found by integrating negatively buoyant +area above the Equilibrium Level until the integrated negative +buoyancy is equal in magnitude to the Convective Available +Potential Energy between the Level of Free Convection and the +Equilibrium Level. + +For valid calculations, nwsspc.sharp.calc.parcel.Parcel.cape_cinh +must be called first, or nwsspc.sharp.calc.parcel.Parcel.cape and +nwsspc.sharp.calc.parcel.Parcel.eql_pressure must be set. + +A value of nwsspc.sharp.calc.constants.MISSING is returned if: + * CAPE is 0 + * nwsspc.sharp.calc.parcel.Parcel.eql_pressure is MISSING + * No valid MPL candidate is found within the profile + In this scenario, it likely exceeds the top of the available data + +In addition to being returned, the result is stored inside of +nwsspc.sharp.calc.parcel.Parcel.mpl_pressure. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_lut - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) +pres : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) +hght : numpy.ndarray[dtype=float32] + 1D NumPy array of height values (meters) +buoyancy : numpy.ndarray[dtype=float32] + 1D NumPy array of buoyancy values (m/s^2) -Returns +Returns ------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values +float + The pressure of the Maximum Parcel Level (Pa) )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::HeightLayer& layer, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + .def( + "cape_cinh", + [](sharp::Parcel& pcl, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t buoy) { + check_equal_sizes(pres, hght, buoy); + pcl.cape_cinh(pres.data(), hght.data(), buoy.data(), + buoy.size()); + return std::make_tuple(pcl.cape, pcl.cinh); + }, + nb::arg("pres"), nb::arg("hght"), nb::arg("buoy"), + R"pbdoc( +Assuming that nwsspc.sharp.calc.parcel.Parcel.lift_parcel has +been called, cape_cinh will integrate the area between the LFC +and EL to compute CAPE, and integrate the area between the LPL +and LCL to compute CINH. + +If eql_pressure is MISSING, but lfc_pressure is defined, the +routine will compute CAPE with the available data despite the +lack of a defined equilibrium level. This is useful for +incomplete profile data, or pressure-level data where the EL +is above the top pressure value. + +The results are stored in nwsspc.sharp.calc.parcel.Parcel.cape +and nwsspc.sharp.calc.parcel.Parcel.cinh via the provided parcel. Parameters ---------- -layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_lut - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) +pres : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) +hght : numpy.ndarray[dtype=float32] + 1D NumPy array of height values (meters) +buoyancy : numpy.ndarray[dtype=float32] + 1D NumPy array of buoyancy values (m/s^2) Returns ------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values +tuple[float, float] + (CAPE, CINH) )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::PressureLayer& layer, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + .def( + "lifted_index", + [](sharp::Parcel& pcl, const float plev, const_prof_arr_t pres, + const_prof_arr_t vtmpk, const_prof_arr_t pcl_vtmpk) { + check_equal_sizes(pres, vtmpk, pcl_vtmpk); + return pcl.lifted_index(plev, pres.data(), vtmpk.data(), + pcl_vtmpk.data(), pres.size()); + }, + nb::arg("pres_lev"), nb::arg("pressure"), + nb::arg("virtual_temperature"), + nb::arg("parcel_virtual_temperature"), + R"pbdoc( +Using the parcel and environment virtual temperature, compute the lifted index +at the given level. The pressure level typically used is 500 hPa. The lifted index +is the difference between the environment and parcel virtual temperatures at the +requested level. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_lut - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) +pres_lev : float + The pressure level (Pa) to compute the lifted index at +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of pressure values (Pa) +virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of environment virtual temperature values (K) +parcel_virtual_temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of parcel virtual temperature values (K) -Returns +Returns ------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values +float + The lifted index )pbdoc") - .def_static( - "most_unstable_parcel", - [](sharp::HeightLayer& layer, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure, const_prof_arr_t height, - const_prof_arr_t temperature, const_prof_arr_t virtemp, - const_prof_arr_t dewpoint) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != height.shape(0)) || - (pressure.shape(0) != virtemp.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - const std::ptrdiff_t NZ = height.size(); - - float* pcl_virtemp_arr = new float[NZ]; - float* pcl_buoy_arr = new float[NZ]; - - sharp::Parcel max_pcl = sharp::Parcel::most_unstable_parcel( - layer, lifter, pressure.data(), height.data(), - temperature.data(), virtemp.data(), dewpoint.data(), - pcl_virtemp_arr, pcl_buoy_arr, NZ); - - delete[] pcl_virtemp_arr; - delete[] pcl_buoy_arr; - - return max_pcl; - }, - nb::arg("layer"), nb::arg("lifter"), nb::arg("pressure"), - nb::arg("height"), nb::arg("temperature"), - nb::arg("virtual_temperature"), nb::arg("dewpoint"), - R"pbdoc( -Given input arrays of pressure, height, temperature, virtual temperature, -and dewpoint temperature, as well as a defined PressureLayer/HeightLayer and -parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + .def_static("surface_parcel", &sharp::Parcel::surface_parcel, + nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), R"pbdoc( +Given input values of surface pressure, temperature, and dewpoint +temperature, construct and return a Surface Based Parcel. Parameters ---------- -layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to search for the Most Unstable Parcel -lifter : nwsspc.sharp.calc.parcel.lifter_lut - Parcel lifting routine to use for moist ascent -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of profile pressure values (Pa) -height : numpy.ndarray[dtype=float32] - 1D NumPy array of profile height values (meters) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile temperature values (K) -virtual_temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of profile virtual temperature values (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of profile dewpoint values (K) +pressure : float + Surface pressure (Pa) +temperature : float + Surface temperature (K) +dewpoint : float + Surface dewpoint (K) Returns ------- nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values - )pbdoc"); - - nb::class_(m_parcel, "DowndraftParcel", R"pbdoc( + Parcel with surface values + )pbdoc"); + + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_lift_parcel(parcel_class, lift_parcel_template_doc, + lifter_name); + }); + + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_mu_parcel( + parcel_class, mu_pcl_template_doc, + "nwsspc.sharp.calc.layer.PressureLayer", lifter_name); + bind_mu_parcel( + parcel_class, mu_pcl_template_doc, + "nwsspc.sharp.calc.layer.HeightLayer", lifter_name); + }); + + bind_ml_parcel(parcel_class, ml_pcl_pres_doc); + bind_ml_parcel(parcel_class, ml_pcl_hght_doc); + + auto dpcl_class = + nb::class_(m_parcel, "DowndraftParcel", R"pbdoc( Contains information about a DowndraftParcel's starting level and thermodynamic attributes, as well as derived computations, methods for constructing a parcel, and parcel descent routines. )pbdoc") - .def_rw("pres", &sharp::DowndraftParcel::pres, R"pbdoc( + .def_rw("pres", &sharp::DowndraftParcel::pres, R"pbdoc( DowndraftParcel starting pressure (Pa) )pbdoc") - .def_rw("tmpk", &sharp::DowndraftParcel::tmpk, R"pbdoc( + .def_rw("tmpk", &sharp::DowndraftParcel::tmpk, R"pbdoc( DowndraftParcel starting temperature (K) )pbdoc") - .def_rw("dwpk", &sharp::DowndraftParcel::dwpk, R"pbdoc( + .def_rw("dwpk", &sharp::DowndraftParcel::dwpk, R"pbdoc( DowndraftParcel starting dewpoint (K) )pbdoc") - .def_rw("cape", &sharp::DowndraftParcel::cape, R"pbdoc( + .def_rw("cape", &sharp::DowndraftParcel::cape, R"pbdoc( DowndraftParcel Convective Available Potential Energy (J/kg) )pbdoc") - .def_rw("cinh", &sharp::DowndraftParcel::cinh, R"pbdoc( + .def_rw("cinh", &sharp::DowndraftParcel::cinh, R"pbdoc( DowndraftParcel Convective Inhibition (J/kg) )pbdoc") - .def(nb::init<>()) - .def(nb::init(), - nb::arg("pressure"), nb::arg("temperature"), nb::arg("dewpoint"), - R"pbdoc( + .def(nb::init<>()) + .def(nb::init(), + nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), + R"pbdoc( Constructor for a DowndraftParcel Parameters @@ -1445,163 +873,17 @@ temperature : float dewpoint : float DowndraftParcel initial dewpoint (K) )pbdoc") - .def( - "lower_parcel", - [](sharp::DowndraftParcel& pcl, sharp::lifter_wobus& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* tmpk_arr = new float[NZ]; - - pcl.lower_parcel(lifter, pressure.data(), tmpk_arr, NZ); - - nb::capsule owner(tmpk_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(tmpk_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist -adiabatically from its LPL to the surface. The moist adiabat used -is determined by the type of lifting functor passed to the function -(i.e. lifter_wobus or lifter_cm1). - -Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual -temperature correction is not used for downdraft parcels. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - An instantiated lifter_wobus functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel temperature values (K) - )pbdoc") - .def( - "lower_parcel", - [](sharp::DowndraftParcel& pcl, sharp::lifter_cm1& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* tmpk_arr = new float[NZ]; - - pcl.lower_parcel(lifter, pressure.data(), tmpk_arr, NZ); - - nb::capsule owner(tmpk_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(tmpk_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist -adiabatically from its LPL to the surface. The moist adiabat used -is determined by the type of lifting functor passed to the function -(i.e. lifter_wobus or lifter_cm1). - -Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual -temperature correction is not used for downdraft parcels. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - An instantiated lifter_cm1 functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel temperature values (K) - )pbdoc") - .def( - "lower_parcel", - [](sharp::DowndraftParcel& pcl, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* tmpk_arr = new float[NZ]; - - pcl.lower_parcel(lifter, pressure.data(), tmpk_arr, NZ); - - nb::capsule owner(tmpk_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(tmpk_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist -adiabatically from its LPL to the surface. The moist adiabat used -is determined by the type of lifting functor passed to the function -(i.e. lifter_wobus or lifter_cm1). - -Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual -temperature correction is not used for downdraft parcels. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel temperature values (K) - )pbdoc") - .def( - "lower_parcel", - [](sharp::DowndraftParcel& pcl, - sharp::lifter_lut& lifter, - const_prof_arr_t pressure) { - const std::ptrdiff_t NZ = pressure.size(); - float* tmpk_arr = new float[NZ]; - - pcl.lower_parcel(lifter, pressure.data(), tmpk_arr, NZ); - - nb::capsule owner(tmpk_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(tmpk_arr, {pressure.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), - R"pbdoc( -Lowers a saturated nwsspc.sharp.calc.parcel.DowndraftParcel moist -adiabatically from its LPL to the surface. The moist adiabat used -is determined by the type of lifting functor passed to the function -(i.e. lifter_wobus or lifter_cm1). - -Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual -temperature correction is not used for downdraft parcels. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - An instantiated lifter_lut functor -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of Pressure levels for lifting (Pa) - -Returns -------- -numpy.ndarray[dtype=float32] - A 1D NumPy array of parcel temperature values (K) - )pbdoc") - .def( - "cape_cinh", - [](sharp::DowndraftParcel& pcl, const_prof_arr_t pres, - const_prof_arr_t hght, const_prof_arr_t buoy) { - if ((pres.shape(0) != hght.shape(0)) || - (pres.shape(0) != buoy.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - pcl.cape_cinh(pres.data(), hght.data(), buoy.data(), - buoy.size()); - return std::make_tuple(pcl.cape, pcl.cinh); - }, - nb::arg("pres"), nb::arg("hght"), nb::arg("buoy"), - R"pbdoc( + .def( + "cape_cinh", + [](sharp::DowndraftParcel& pcl, const_prof_arr_t pres, + const_prof_arr_t hght, const_prof_arr_t buoy) { + check_equal_sizes(pres, hght, buoy); + pcl.cape_cinh(pres.data(), hght.data(), buoy.data(), + buoy.size()); + return std::make_tuple(pcl.cape, pcl.cinh); + }, + nb::arg("pres"), nb::arg("hght"), nb::arg("buoy"), + R"pbdoc( Assuming that nwsspc.sharp.calc.parcel.DowndraftParcel.lower_parcel has been called, cape_cinh will integrate the area between the LPL and the surface to compute downdraft CAPE and downdraft CINH. @@ -1623,26 +905,22 @@ Returns tuple[float, float] (DCAPE, DCINH) )pbdoc") - .def_static( - "min_thetae", - [](sharp::PressureLayer& search_layer, const_prof_arr_t pressure, - const_prof_arr_t temperature, const_prof_arr_t dewpoint, - const_prof_arr_t thetae, const float mean_depth) { - if ((pressure.shape(0) != temperature.shape(0)) || - (pressure.shape(0) != dewpoint.shape(0)) || - (pressure.shape(0) != thetae.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - return sharp::DowndraftParcel::min_thetae( - search_layer, pressure.data(), temperature.data(), - dewpoint.data(), thetae.data(), pressure.shape(0), - mean_depth); - }, - nb::arg("search_layer"), nb::arg("pressure"), - nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("thetae"), - nb::arg("mean_depth") = 10000.0f, - R"pbdoc( + .def_static( + "min_thetae", + [](sharp::PressureLayer& search_layer, + const_prof_arr_t pressure, const_prof_arr_t temperature, + const_prof_arr_t dewpoint, const_prof_arr_t thetae, + const float mean_depth) { + check_equal_sizes(pressure, temperature, dewpoint, thetae); + return sharp::DowndraftParcel::min_thetae( + search_layer, pressure.data(), temperature.data(), + dewpoint.data(), thetae.data(), pressure.size(), + mean_depth); + }, + nb::arg("search_layer"), nb::arg("pressure"), + nb::arg("temperature"), nb::arg("dewpoint"), nb::arg("thetae"), + nb::arg("mean_depth") = 10000.0f, + R"pbdoc( Define a downdraft parcel. Defines a downdraft parcel within a given search layer. @@ -1671,6 +949,12 @@ Returns nwsspc.sharp.calc.parcel.DowndraftParcel Downdraft Parcel )pbdoc"); + + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_lower_parcel(dpcl_class, lower_parcel_template_doc, + lifter_name); + }); } #endif diff --git a/src/nanobind/thermo_bindings.h b/src/nanobind/thermo_bindings.h index 76363bbd..9e4608d3 100644 --- a/src/nanobind/thermo_bindings.h +++ b/src/nanobind/thermo_bindings.h @@ -9,14 +9,85 @@ #include #include #include +#include #include #include +#include "binding_utils.h" #include "sharplib_types.h" namespace nb = nanobind; +template +void bind_wetbulb(nb::module_& mod, const char* scalar_doc_template, + const char* array_doc_template, const char* lifter_name) { + std::string scalar_doc = fmt::format(scalar_doc_template, lifter_name); + mod.def( + "wetbulb", + [](Lft& lifter, float pressure, float temperature, float dewpoint) { + return sharp::wetbulb(lifter, pressure, temperature, dewpoint); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), scalar_doc.c_str()); + + std::string array_doc = fmt::format(array_doc_template, lifter_name); + mod.def( + "wetbulb", + [](Lft& lifter, const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, + const_prof_arr_t dwpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr, dwpk_arr); + const std::size_t NZ = pres_arr.size(); + auto pres = pres_arr.view(); + auto tmpk = tmpk_arr.view(); + auto dwpk = dwpk_arr.view(); + + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); + } + }); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), array_doc.c_str()); +} + +template +void bind_theta_wetbulb(nb::module_& mod, const char* scalar_doc_template, + const char* array_doc_template, + const char* lifter_name) { + std::string scalar_doc = fmt::format(scalar_doc_template, lifter_name); + mod.def( + "theta_wetbulb", + [](Lft& lifter, float pressure, float temperature, float dewpoint) { + return sharp::theta_wetbulb(lifter, pressure, temperature, + dewpoint); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), scalar_doc.c_str()); + + std::string array_doc = fmt::format(array_doc_template, lifter_name); + mod.def( + "theta_wetbulb", + [](Lft& lifter, const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, + const_prof_arr_t dwpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr, dwpk_arr); + const std::size_t NZ = pres_arr.size(); + auto pres = pres_arr.view(); + auto tmpk = tmpk_arr.view(); + auto dwpk = dwpk_arr.view(); + + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); + } + }); + }, + nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), + nb::arg("dewpoint"), array_doc.c_str()); +} + inline void make_thermo_bindings(nb::module_ m) { nb::module_ m_therm = m.def_submodule( "thermo", @@ -33,6 +104,130 @@ inline void make_thermo_bindings(nb::module_ m) { .value("adiab_ice", sharp::adiabat::adiab_ice, "Use an adiabatic parcel with liquid and ice processes"); + const char* wetbulb_scalar_template_doc = + R"pbdoc( +Compute the wet bulb temperature (K) given the ambient pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, and +dewpoint temperature to its Lifted Condensation Level (LCL). To compute the +temperature and pressure of the LCL, an approximation is used. See the +lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to its initial pressure level along a moist adiabat or +pseudoadiabat. + +Parameters +---------- +lifter : {} + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : float + The ambient pressure (Pa) +temperature : float + The ambient temperature (K) +dewpoint : float + The ambient dewpoint temperature (K) + +Returns +------- +float + The wetbulb temperature (K) + )pbdoc"; + + const char* wetbulb_array_template_doc = + R"pbdoc( +Compute the wet bulb temperature (K) given the ambient pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, and +dewpoint temperature to its Lifted Condensation Level (LCL). To compute the +temperature and pressure of the LCL, an approximation is used. See the +lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to its initial pressure level along a moist adiabat or +pseudoadiabat. + +Parameters +---------- +lifter : {} + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient pressures (Pa) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient temperatures (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperatures (K) + +Returns +------- +numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) + )pbdoc"; + + const char* theta_wb_scalar_template_doc = + R"pbdoc( +Compute the wet-bulb potential temperature (K) given the pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, +and dewpoint temperature to its Lifted Condensation Level (LCL). +To compute the temperature and pressure of the LCL, an approximation +is used. See the lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to the standard parcel reference pressure level +(1000 hPa) along a moist adiabat. + +Parameters +---------- +lifter : {} + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : float + the ambient air pressure (Pa) +temperature : float + the ambient air temperature (K) +dewpoint : float + the ambient dewpoint temperature (K) + +Returns +------- +float + The wet-bulb potential temperature (K) + )pbdoc"; + + const char* theta_wb_array_template_doc = + R"pbdoc( +Compute the wet-bulb potential temperature (K) given the pressure (Pa), +temperature (K), and dewpoint temperature (K). + +First, it lifts a parcel with the given pressure, temperature, +and dewpoint temperature to its Lifted Condensation Level (LCL). +To compute the temperature and pressure of the LCL, an approximation +is used. See the lcl_temperature function for further detail. + +After the parcel has reached the LCL, the lifter passed to the function +lowers the parcel to the standard parcel reference pressure level +(1000 hPa) along a moist adiabat. + +Parameters +---------- +lifter : {} + a parcel lifter (e.g. lifter_cm1 or lifter_wobus) +pressure : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air pressure (Pa) +temperature : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient air temperature (K) +dewpoint : numpy.ndarray[dtype=float32] + 1D NumPy array of ambient dewpoint temperature (K) + +Returns +------- +numpy.ndarray[dtype=float32] + 1D NumPy array of wet-bulb potential temperatures (K) + )pbdoc"; + m_therm.def("wobf", &sharp::wobf, nb::arg("temperature"), R"pbdoc( Computes the difference between the wet-bulb potential temperatures @@ -77,19 +272,14 @@ float m_therm.def( "wobf", [](const_prof_arr_t tmpk_arr) { - // Get a high performance view - // into the array for access + const std::size_t NZ = tmpk_arr.size(); auto tmpk = tmpk_arr.view(); - float *wobf_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - wobf_arr[k] = sharp::wobf(tmpk(k)); - } - - nb::capsule owner(wobf_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(wobf_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::wobf(tmpk(k)); + } + }); }, nb::arg("tmpk_arr"), R"pbdoc( @@ -159,24 +349,16 @@ float m_therm.def( "lcl_temperature", [](const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { - // Get a high performance view - // into these arrays for access + check_equal_sizes(tmpk_arr, dwpk_arr); + const std::size_t NZ = tmpk_arr.size(); auto tmpk = tmpk_arr.view(); auto dwpk = dwpk_arr.view(); - if ((tmpk.shape(0) != dwpk.shape(0))) { - throw nb::buffer_error( - "tmpk_arr and dwpk_arr must have the same size!"); - } - - float *lcl_tmpk_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - lcl_tmpk_arr[k] = sharp::lcl_temperature(tmpk(k), dwpk(k)); - } - - nb::capsule owner(lcl_tmpk_arr, - [](void *p) noexcept { delete[] (float *)p; }); - return out_arr_t(lcl_tmpk_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::lcl_temperature(tmpk(k), dwpk(k)); + } + }); }, nb::arg("tmpk_arr"), nb::arg("dwpk_arr"), R"pbdoc( @@ -233,22 +415,16 @@ float m_therm.def( "vapor_pressure", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr); + const std::size_t NZ = pres_arr.size(); auto tmpk = tmpk_arr.view(); auto pres = pres_arr.view(); - if ((tmpk.shape(0) != pres.shape(0))) { - throw nb::buffer_error( - "tmpk_arr and pres_arr must have the same size!"); - } - float *vappres_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - vappres_arr[k] = sharp::vapor_pressure(pres(k), tmpk(k)); - } - - nb::capsule owner(vappres_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(vappres_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::vapor_pressure(pres(k), tmpk(k)); + } + }); }, nb::arg("pres_arr"), nb::arg("tmpk_arr"), R"pbdoc( @@ -308,22 +484,16 @@ float m_therm.def( "vapor_pressure_ice", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr); + const std::size_t NZ = pres_arr.size(); auto tmpk = tmpk_arr.view(); auto pres = pres_arr.view(); - if ((tmpk.shape(0) != pres.shape(0))) { - throw nb::buffer_error( - "tmpk_arr and pres_arr must have the same size!"); - } - - float *vappres_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - vappres_arr[k] = sharp::vapor_pressure_ice(pres(k), tmpk(k)); - } - - nb::capsule owner(vappres_arr, - [](void *p) noexcept { delete[] (float *)p; }); - return out_arr_t(vappres_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::vapor_pressure_ice(pres(k), tmpk(k)); + } + }); }, nb::arg("pres_arr"), nb::arg("tmpk_arr"), R"pbdoc( @@ -378,22 +548,16 @@ float m_therm.def( "temperature_at_mixratio", [](const_prof_arr_t mixr_arr, const_prof_arr_t pres_arr) { + check_equal_sizes(mixr_arr, pres_arr); + const std::size_t NZ = pres_arr.size(); auto mixr = mixr_arr.view(); auto pres = pres_arr.view(); - if ((mixr.shape(0) != pres.shape(0))) { - throw nb::buffer_error( - "mixr_arr and pres_arr must have the same size!"); - } - - float *dwpk_arr = new float[mixr.shape(0)]; - for (size_t k = 0; k < mixr.shape(0); ++k) { - dwpk_arr[k] = sharp::temperature_at_mixratio(mixr(k), pres(k)); - } - nb::capsule owner(dwpk_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(dwpk_arr, {mixr.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::temperature_at_mixratio(mixr(k), pres(k)); + } + }); }, nb::arg("mixr_arr"), nb::arg("pres_arr"), R"pbdoc( @@ -440,21 +604,16 @@ float m_therm.def( "theta_level", [](const_prof_arr_t theta_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(theta_arr, tmpk_arr); + const std::size_t NZ = tmpk_arr.size(); auto theta = theta_arr.view(); auto tmpk = tmpk_arr.view(); - if ((theta.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "theta_arr and tmpk_arr must have the same size!"); - } - float *pres_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - pres_arr[k] = sharp::theta_level(theta(k), tmpk(k)); - } - nb::capsule owner(pres_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(pres_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::theta_level(theta(k), tmpk(k)); + } + }); }, nb::arg("theta_arr"), nb::arg("tmpk_arr"), R"pbdoc( @@ -501,21 +660,16 @@ float "theta", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, const float ref_pres) { + check_equal_sizes(pres_arr, tmpk_arr); + const std::size_t NZ = pres_arr.size(); auto pres = pres_arr.view(); auto tmpk = tmpk_arr.view(); - if ((pres.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "pres_arr and tmpk_arr must have the same size!"); - } - float *theta_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - theta_arr[k] = sharp::theta(pres(k), tmpk(k), ref_pres); - } - nb::capsule owner(theta_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(theta_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::theta(pres(k), tmpk(k), ref_pres); + } + }); }, nb::arg("pres_arr"), nb::arg("tmpk_arr"), nb::arg("ref_pressure") = sharp::THETA_REF_PRESSURE, @@ -578,17 +732,14 @@ float m_therm.def( "mixratio", [](const_prof_arr_t spfh_arr) { + const std::size_t NZ = spfh_arr.size(); auto spfh = spfh_arr.view(); - float *mixr_arr = new float[spfh.shape(0)]; - for (size_t k = 0; k < spfh.shape(0); ++k) { - mixr_arr[k] = sharp::mixratio(spfh(k)); - } - - nb::capsule owner(mixr_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(mixr_arr, {spfh.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::mixratio(spfh(k)); + } + }); }, nb::arg("spfh_arr"), R"pbdoc( @@ -609,21 +760,16 @@ numpy.ndarray[dtype=float32] m_therm.def( "mixratio", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr); + const std::size_t NZ = pres_arr.size(); auto pres = pres_arr.view(); auto tmpk = tmpk_arr.view(); - if ((pres.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "pres_arr and tmpk_arr must have the same size!"); - } - float *mixr_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - mixr_arr[k] = sharp::mixratio(pres(k), tmpk(k)); - } - nb::capsule owner(mixr_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(mixr_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::mixratio(pres(k), tmpk(k)); + } + }); }, nb::arg("pres_arr"), nb::arg("tmpk_arr"), R"pbdoc( @@ -668,21 +814,16 @@ float m_therm.def( "mixratio_ice", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr); + const std::size_t NZ = pres_arr.size(); auto pres = pres_arr.view(); auto tmpk = tmpk_arr.view(); - if ((pres.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "pres_arr and tmpk_arr must have the same size!"); - } - float *mixr_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - mixr_arr[k] = sharp::mixratio_ice(pres(k), tmpk(k)); - } - nb::capsule owner(mixr_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(mixr_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::mixratio_ice(pres(k), tmpk(k)); + } + }); }, nb::arg("pres_arr"), nb::arg("tmpk_arr"), R"pbdoc( @@ -722,16 +863,14 @@ float m_therm.def( "specific_humidity", [](const_prof_arr_t mixr_arr) { + const std::size_t NZ = mixr_arr.size(); auto mixr = mixr_arr.view(); - float *spfh_arr = new float[mixr.shape(0)]; - for (size_t k = 0; k < mixr.shape(0); ++k) { - spfh_arr[k] = sharp::specific_humidity(mixr(k)); - } - - nb::capsule owner(spfh_arr, - [](void *p) noexcept { delete[] (float *)p; }); - return out_arr_t(spfh_arr, {mixr.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::specific_humidity(mixr(k)); + } + }); }, nb::arg("mixr_arr"), R"pbdoc( @@ -777,20 +916,18 @@ float "relative_humidity", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr, dwpk_arr); + const std::size_t NZ = pres_arr.size(); auto pres = pres_arr.view(); auto tmpk = tmpk_arr.view(); auto dwpk = dwpk_arr.view(); - float *relh = new float[pres.shape(0)]; - - for (std::ptrdiff_t k = 0; k < pres.shape(0); ++k) { - relh[k] = sharp::relative_humidity(pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(relh, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(relh, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::relative_humidity(pres(k), tmpk(k), dwpk(k)); + } + }); }, nb::arg("pressure"), nb::arg("temperature"), nb::arg("dewpoint"), R"pbdoc( @@ -813,9 +950,7 @@ Returns ------- numpy.ndarray[dtype=float32] 1D NumPy array of relative humidity values (fraction, unitless) - )pbdoc" - - ); + )pbdoc"); m_therm.def("virtual_temperature", &sharp::virtual_temperature, nb::arg("temperature"), nb::arg("rv"), nb::arg("rl") = 0.0f, @@ -848,68 +983,46 @@ float "virtual_temperature", [](const_prof_arr_t tmpk_arr, const_prof_arr_t rv_arr, const_prof_arr_t rl_arr, const_prof_arr_t ri_arr) { + check_equal_sizes(tmpk_arr, rv_arr); + const std::size_t NZ = tmpk_arr.size(); auto tmpk = tmpk_arr.view(); auto rv = rv_arr.view(); - float *vtmp_arr; - // tmpk and rv are always defined, check their - // sizes and ensure they're equal - if (tmpk.shape(0) != rv.shape(0)) { - throw nb::buffer_error( - "tmpk_arr and rv_arr must have the same size!"); - } - - // check if rl_arr and ri_arr are not none if (rl_arr.is_valid() && ri_arr.is_valid()) { + check_equal_sizes(tmpk_arr, rv_arr, rl_arr, ri_arr); auto rl = rl_arr.view(); auto ri = ri_arr.view(); - // ensure they're the same shape/size - if ((tmpk.shape(0) != rl.shape(0)) || - (tmpk.shape(0) != ri.shape(0))) { - throw nb::buffer_error( - "tmpk_arr and rv_arr must have the same size!"); - } - vtmp_arr = new float[tmpk.shape(0)]; - - for (size_t k = 0; k < tmpk.shape(0); ++k) { - vtmp_arr[k] = sharp::virtual_temperature(tmpk(k), rv(k), - rl(k), ri(k)); - } + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::virtual_temperature(tmpk(k), rv(k), + rl(k), ri(k)); + } + }); } else if (rl_arr.is_valid()) { + check_equal_sizes(tmpk_arr, rv_arr, rl_arr); auto rl = rl_arr.view(); - if (tmpk.shape(0) != rl.shape(0)) { - throw nb::buffer_error( - "tmpk_arr, rv_arr, and rl_arr must have the same " - "size!"); - } - vtmp_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - vtmp_arr[k] = - sharp::virtual_temperature(tmpk(k), rv(k), rl(k)); - } + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::virtual_temperature(tmpk(k), rv(k), rl(k)); + } + }); } else if (ri_arr.is_valid()) { + check_equal_sizes(tmpk_arr, rv_arr, ri_arr); auto ri = ri_arr.view(); - if (tmpk.shape(0) != ri.shape(0)) { - throw nb::buffer_error( - "tmpk_arr, rv_arr, and ri_arr must have the same " - "size!"); - } - vtmp_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - vtmp_arr[k] = - sharp::virtual_temperature(tmpk(k), rv(k), 0.0, ri(k)); - } + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::virtual_temperature(tmpk(k), rv(k), 0.0, + ri(k)); + } + }); } else { - vtmp_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - vtmp_arr[k] = sharp::virtual_temperature(tmpk(k), rv(k)); - } + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::virtual_temperature(tmpk(k), rv(k)); + } + }); } - - nb::capsule owner(vtmp_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(vtmp_arr, {tmpk.shape(0)}, owner); }, nb::arg("tmpk_arr"), nb::arg("rv_arr"), nb::arg("rl_arr") = nb::none(), nb::arg("ri_arr") = nb::none(), @@ -977,26 +1090,17 @@ float "wetlift", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, const_prof_arr_t lifted_pres_arr) { + check_equal_sizes(pres_arr, tmpk_arr, lifted_pres_arr); + const std::size_t NZ = pres_arr.size(); auto pres = pres_arr.view(); auto tmpk = tmpk_arr.view(); auto lifted_pres = lifted_pres_arr.view(); - if ((pres.shape(0) != tmpk.shape(0)) || - (pres.shape(0) != lifted_pres.shape(0))) { - throw nb::buffer_error( - "pres_arr, tmpk_arr, and lifted_pres_arr must have the " - "same sizes!"); - } - float *tmpk_out_arr = new float[tmpk.shape(0)]; - for (size_t k = 0; k < tmpk.shape(0); ++k) { - tmpk_out_arr[k] = - sharp::wetlift(pres(k), tmpk(k), lifted_pres(k)); - } - - nb::capsule owner(tmpk_out_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(tmpk_out_arr, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::wetlift(pres(k), tmpk(k), lifted_pres(k)); + } + }); }, R"pbdoc( Compute the temperature of a parcel lifted moist adiabatically to a new level. @@ -1062,778 +1166,63 @@ tuple[float, float] A tuple of (lcl_pres, lcl_temperature) )pbdoc"); - m_therm.def( - "wetbulb", - [](sharp::lifter_wobus &lifter, float pressure, float temperature, - float dewpoint) { - return sharp::wetbulb(lifter, pressure, temperature, dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_wetbulb(m_therm, wetbulb_scalar_template_doc, + wetbulb_array_template_doc, lifter_name); + }); -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. + for_each_lifter([&](auto tag, const char* lifter_name) { + using Lft = typename decltype(tag)::type; + bind_theta_wetbulb(m_therm, theta_wb_scalar_template_doc, + theta_wb_array_template_doc, lifter_name); + }); -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. + m_therm.def("thetae", &sharp::thetae, nb::arg("pressure"), + nb::arg("temperature"), nb::arg("dewpoint"), + R"pbdoc( +Compute the equivalent potential temperature (K) given the +air pressure (Pa), air temperature (K), and dewpoint temperature (K). +The equivalent potential temperature is computed as in Bolton 1980. +It was found in Davies-Jones 2009 to be the most accurate non-iterative +formulation of theta-e. + +References +---------- +Bolton 1980: https://doi.org/10.1175/1520-0493(1980)108%3C1046:TCOEPT%3E2.0.CO;2 + +Davies-Jones 2009: https://doi.org/10.1175/2009MWR2774.1 Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : float - The ambient pressure (Pa) + the ambient air pressure (Pa) temperature : float - The ambient temperature (K) + the ambient air temperature (K) dewpoint : float - The ambient dewpoint temperature (K) + the dewpoint temperature (K) Returns ------- float - The wetbulb temperature (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_cm1 &lifter, float pressure, float temperature, - float dewpoint) { - return sharp::wetbulb(lifter, pressure, temperature, dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - The ambient pressure (Pa) -temperature : float - The ambient temperature (K) -dewpoint : float - The ambient dewpoint temperature (K) - -Returns -------- -float - The wetbulb temperature (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_lut &lifter, float pressure, - float temperature, float dewpoint) { - return sharp::wetbulb(lifter, pressure, temperature, dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - The ambient pressure (Pa) -temperature : float - The ambient temperature (K) -dewpoint : float - The ambient dewpoint temperature (K) - -Returns -------- -float - The wetbulb temperature (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_lut &lifter, float pressure, - float temperature, float dewpoint) { - return sharp::wetbulb(lifter, pressure, temperature, dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - The ambient pressure (Pa) -temperature : float - The ambient temperature (K) -dewpoint : float - The ambient dewpoint temperature (K) - -Returns -------- -float - The wetbulb temperature (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_wobus &lifter, const_prof_arr_t pres_arr, - const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *wb_out = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - wb_out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(wb_out, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(wb_out, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient pressures (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperatures (K) - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wetbulb temperatures (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_cm1 &lifter, const_prof_arr_t pres_arr, - const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *wb_out = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - wb_out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(wb_out, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(wb_out, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient pressures (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperatures (K) - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wetbulb temperatures (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_lut &lifter, - const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, - const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *wb_out = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - wb_out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(wb_out, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(wb_out, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient pressures (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperatures (K) - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wetbulb temperatures (K) - )pbdoc"); - - m_therm.def( - "wetbulb", - [](sharp::lifter_lut &lifter, - const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, - const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *wb_out = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - wb_out[k] = sharp::wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(wb_out, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(wb_out, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet bulb temperature (K) given the ambient pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, and -dewpoint temperature to its Lifted Condensation Level (LCL). To compute the -temperature and pressure of the LCL, an approximation is used. See the -lcl_temperature function for further detail. - -After the parcel has reached the LCL, the lifter passed to the function -lowers the parcel to its initial pressure level along a moist adiabat or -pseudoadiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient pressures (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient temperatureds (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperatures (K) - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wetbulb temperatures (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_wobus &lifter, float pressure, float temperature, - float dewpoint) { - return sharp::theta_wetbulb(lifter, pressure, temperature, - dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - the ambient air pressure (Pa) -temperature : float - the ambient air temperature (K) -dewpoint : float - the ambient dewpoint temperature - -Returns -------- -float - The wet-bulb potential temperature (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_cm1 &lifter, float pressure, float temperature, - float dewpoint) { - return sharp::theta_wetbulb(lifter, pressure, temperature, - dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - the ambient air pressure (Pa) -temperature : float - the ambient air temperature (K) -dewpoint : float - the ambient dewpoint temperature - -Returns -------- -float - The wet-bulb potential temperature (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_lut &lifter, float pressure, - float temperature, float dewpoint) { - return sharp::theta_wetbulb(lifter, pressure, temperature, - dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - the ambient air pressure (Pa) -temperature : float - the ambient air temperature (K) -dewpoint : float - the ambient dewpoint temperature - -Returns -------- -float - The wet-bulb potential temperature (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_lut &lifter, float pressure, - float temperature, float dewpoint) { - return sharp::theta_wetbulb(lifter, pressure, temperature, - dewpoint); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : float - the ambient air pressure (Pa) -temperature : float - the ambient air temperature (K) -dewpoint : float - the ambient dewpoint temperature - -Returns -------- -float - The wet-bulb potential temperature (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_wobus &lifter, const_prof_arr_t pres_arr, - const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *theta_wb = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - theta_wb[k] = - sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(theta_wb, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(theta_wb, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_wobus - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air pressure (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air temperature (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wet-bulb potential temperatures (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_cm1 &lifter, const_prof_arr_t pres_arr, - const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *theta_wb = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - theta_wb[k] = - sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(theta_wb, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(theta_wb, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air pressure (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air temperature (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wet-bulb potential temperatures (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_lut &lifter, - const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, - const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *theta_wb = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - theta_wb[k] = - sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(theta_wb, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(theta_wb, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air pressure (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air temperature (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wet-bulb potential temperatures (K) - )pbdoc"); - - m_therm.def( - "theta_wetbulb", - [](sharp::lifter_lut &lifter, - const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, - const_prof_arr_t dwpk_arr) { - auto pres = pres_arr.view(); - auto tmpk = tmpk_arr.view(); - auto dwpk = dwpk_arr.view(); - - float *theta_wb = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - theta_wb[k] = - sharp::theta_wetbulb(lifter, pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(theta_wb, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(theta_wb, {tmpk.shape(0)}, owner); - }, - nb::arg("lifter"), nb::arg("pressure"), nb::arg("temperature"), - nb::arg("dewpoint"), - R"pbdoc( -Compute the wet-bulb potential temperature (K) given the pressure (Pa), -temperature (K), and dewpoint temperature (K). - -First, it lifts a parcel with the given pressure, temperature, -and dewpoint temperature to its Lifted Condensation Level (LCL). -To compute the temperature and pressure of the LCL, an approximation -is used. See the lcl_temperature fuction for further detail. - -After the parcel has reached the LCL, the lifted passed to the function -lowers the parcel to the standard parcel reference pressure level -(1000 hPa) along a moist adiabat. - -Parameters ----------- -lifter : nwsspc.sharp.calc.parcel.lifter_lut - a parcel lifter (e.g. lifter_cm1 or lifter_wobus) -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air pressure (Pa) -temperature : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient air temperature (K) -dewpoint : numpy.ndarray[dtype=float32] - 1D NumPy array of ambient dewpoint temperature - -Returns -------- -numpy.ndarray[dtype=float32] - 1D NumPy array of wet-bulb potential temperatures (K) - )pbdoc"); - - m_therm.def("thetae", &sharp::thetae, nb::arg("pressure"), - nb::arg("temperature"), nb::arg("dewpoint"), - R"pbdoc( -Compute the equivalent potential temperature (K) given the -air pressure (Pa), air temperature (K), and dewpoint temperature (K). -The equivalent potential temperature is computed as in Bolton 1980. -It was found in Davies-Jones 2009 to be the most accurate non-iterative -formulation of theta-e. - -References ----------- -Bolton 1980: https://doi.org/10.1175/1520-0493(1980)108%3C1046:TCOEPT%3E2.0.CO;2 - -Davies-Jones 2009: https://doi.org/10.1175/2009MWR2774.1 - -Parameters ----------- -pressure : float - the ambient air pressure (Pa) -temperature : float - the ambient air temperature (K) -dewpoint : float - the dewpoint temperature (K) - -Returns -------- -float - The equivalent potential temperature (K) + The equivalent potential temperature (K) )pbdoc"); m_therm.def( "thetae", [](const_prof_arr_t pres_arr, const_prof_arr_t tmpk_arr, const_prof_arr_t dwpk_arr) { + check_equal_sizes(pres_arr, tmpk_arr, dwpk_arr); + const std::size_t NZ = pres_arr.size(); auto pres = pres_arr.view(); auto tmpk = tmpk_arr.view(); auto dwpk = dwpk_arr.view(); - float *thetae = new float[pres.shape(0)]; - - for (size_t k = 0; k < pres.shape(0); ++k) { - thetae[k] = sharp::thetae(pres(k), tmpk(k), dwpk(k)); - } - - nb::capsule owner(thetae, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(thetae, {tmpk.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t k = 0; k < NZ; ++k) { + out[k] = sharp::thetae(pres(k), tmpk(k), dwpk(k)); + } + }); }, nb::arg("pressure"), nb::arg("temperature"), nb::arg("dewpoint"), R"pbdoc( @@ -1868,6 +1257,7 @@ numpy.ndarray[dtype=float32] "lapse_rate", [](sharp::HeightLayer layer, const_prof_arr_t hght_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(hght_arr, tmpk_arr); return sharp::lapse_rate(layer, hght_arr.data(), tmpk_arr.data(), tmpk_arr.size()); }, @@ -1896,6 +1286,7 @@ float "lapse_rate", [](sharp::PressureLayer layer, const_prof_arr_t pres_arr, const_prof_arr_t hght_arr, const_prof_arr_t tmpk_arr) { + check_equal_sizes(pres_arr, hght_arr, tmpk_arr); return sharp::lapse_rate(layer, pres_arr.data(), hght_arr.data(), tmpk_arr.data(), tmpk_arr.size()); }, @@ -1925,6 +1316,7 @@ float "lapse_rate_max", [](sharp::HeightLayer layer, float depth, const_prof_arr_t hght_arr, const_prof_arr_t tmpk_arr) -> std::tuple { + check_equal_sizes(hght_arr, tmpk_arr); sharp::HeightLayer out_lyr = {0, 0}; float max_lr = sharp::lapse_rate_max(layer, depth, hght_arr.data(), tmpk_arr.data(), @@ -1961,6 +1353,7 @@ tuple[float, nwsspc.sharp.calc.layer.HeightLayer] [](sharp::PressureLayer layer, float depth, const_prof_arr_t pres_arr, const_prof_arr_t hght_arr, const_prof_arr_t tmpk_arr) -> std::tuple { + check_equal_sizes(pres_arr, hght_arr, tmpk_arr); sharp::PressureLayer out_lyr = {0, 0}; float max_lr = sharp::lapse_rate_max( layer, depth, pres_arr.data(), hght_arr.data(), tmpk_arr.data(), @@ -2017,15 +1410,12 @@ float m_therm.def( "buoyancy", [](const_prof_arr_t pcl_tmpk, const_prof_arr_t env_tmpk) { - float *buoy_arr = new float[pcl_tmpk.size()]; - - sharp::buoyancy(pcl_tmpk.data(), env_tmpk.data(), buoy_arr, - pcl_tmpk.size()); + check_equal_sizes(pcl_tmpk, env_tmpk); + const std::size_t NZ = pcl_tmpk.size(); - nb::capsule owner(buoy_arr, - [](void *p) noexcept { delete[] (float *)p; }); - - return out_arr_t(buoy_arr, {pcl_tmpk.size()}, owner); + return make_output_array(NZ, [&](float* out) { + sharp::buoyancy(pcl_tmpk.data(), env_tmpk.data(), out, NZ); + }); }, nb::arg("parcel_temperature"), nb::arg("environment_temperature"), R"pbdoc( @@ -2048,16 +1438,11 @@ numpy.ndarray[dtype=float32] "pbl_top", [](const_prof_arr_t pres_arr, const_prof_arr_t thetav_arr, float offset) { - auto pres = pres_arr.view(); - auto thetav = thetav_arr.view(); - if ((pres.shape(0) != thetav.shape(0))) { - throw nb::buffer_error( - "pressure and virtual potential temperature must have the " - "same size!"); - } + check_equal_sizes(pres_arr, thetav_arr); + const std::size_t NZ = pres_arr.size(); - std::size_t pbl_idx = sharp::pbl_top(pres.data(), thetav.data(), - pres.shape(0), offset); + std::size_t pbl_idx = + sharp::pbl_top(pres_arr.data(), thetav_arr.data(), NZ, offset); return pbl_idx; }, @@ -2087,16 +1472,13 @@ float "temperature_layer", [](const_prof_arr_t pressure, const_prof_arr_t temperature, const float tmpk_1, const float tmpk_2, const float pres_min) { + check_equal_sizes(pressure, temperature); + const std::size_t NZ = pressure.size(); auto pres = pressure.view(); auto tmpk = temperature.view(); - std::size_t len = pressure.size(); - if ((pres.shape(0) != tmpk.shape(0))) { - throw nb::buffer_error( - "Pressure and temperature must have the same size!"); - } return sharp::temperature_layer(pres.data(), tmpk.data(), tmpk_1, - tmpk_2, len, pres_min); + tmpk_2, NZ, pres_min); }, nb::arg("pressure"), nb::arg("temperature"), nb::arg("tmpk_lo"), nb::arg("tmpk_hi"), nb::arg("pres_min") = 10000.0, diff --git a/src/nanobind/winds_bindings.h b/src/nanobind/winds_bindings.h index f506a7ee..eb934d82 100644 --- a/src/nanobind/winds_bindings.h +++ b/src/nanobind/winds_bindings.h @@ -5,12 +5,106 @@ #include #include -// clang-format on +// clang-format on #include #include +#include + +#include +#include "binding_utils.h" #include "sharplib_types.h" +namespace nb = nanobind; + +template +void bind_helicity(nb::module_& mod, const char* doc_template, + const char* layer_name) { + const char *coord_name, *coord_unit, *layer_unit; + if constexpr (Layer::coord == sharp::LayerCoordinate::pressure) { + coord_name = "pressure"; + coord_unit = "Pa"; + layer_unit = "Pa"; + } else { + coord_name = "height"; + coord_unit = "meters"; + layer_unit = "meters AGL"; + } + + std::string doc = fmt::format(doc_template, layer_name, coord_name, + coord_unit, layer_unit); + mod.def( + "helicity", + [](Layer layer, sharp::WindComponents storm_motion, + const_prof_arr_t coord, const_prof_arr_t u_wind, + const_prof_arr_t v_wind) { + check_equal_sizes(coord, u_wind, v_wind); + const std::size_t NZ = coord.size(); + return sharp::helicity(layer, storm_motion, coord.data(), + u_wind.data(), v_wind.data(), NZ); + }, + nb::arg("layer"), nb::arg("storm_motion"), nb::arg(coord_name), + nb::arg("u_wind"), nb::arg("v_wind"), doc.c_str()); +} + +template +void bind_wind_shear(nb::module_& mod, const char* doc_template, + const char* layer_name) { + const char *coord_name, *coord_unit, *layer_unit; + if constexpr (Layer::coord == sharp::LayerCoordinate::pressure) { + coord_name = "pressure"; + coord_unit = "Pa"; + layer_unit = "Pa"; + } else { + coord_name = "height"; + coord_unit = "meters"; + layer_unit = "meters AGL"; + } + + std::string doc = fmt::format(doc_template, layer_name, coord_name, + coord_unit, layer_unit); + mod.def( + "wind_shear", + [](Layer layer, const_prof_arr_t height, const_prof_arr_t u_wind, + const_prof_arr_t v_wind) { + check_equal_sizes(height, u_wind, v_wind); + return sharp::wind_shear(layer, height.data(), u_wind.data(), + v_wind.data(), height.size()); + }, + nb::arg("layer"), nb::arg(coord_name), nb::arg("u_wind"), + nb::arg("v_wind"), doc.c_str()); +} + +template +void bind_max_wind(nb::module_& mod, const char* doc_template, + const char* layer_name) { + const char *coord_name, *coord_unit, *layer_unit; + if constexpr (Layer::coord == sharp::LayerCoordinate::pressure) { + coord_name = "pressure"; + coord_unit = "Pa"; + layer_unit = "Pa"; + } else { + coord_name = "height"; + coord_unit = "meters"; + layer_unit = "meters AGL"; + } + + std::string doc = fmt::format(doc_template, layer_name, coord_name, + coord_unit, layer_unit); + mod.def( + "max_wind", + [](Layer layer, const_prof_arr_t coord, const_prof_arr_t u_wind, + const_prof_arr_t v_wind) { + check_equal_sizes(coord, u_wind, v_wind); + const std::size_t NZ = u_wind.size(); + + return sharp::max_wind(layer, coord.data(), u_wind.data(), + v_wind.data(), NZ); + }, + nb::arg("layer"), nb::arg(coord_name), nb::arg("u_wind"), + nb::arg("v_wind"), doc.c_str()); +} + // clang-format on inline void make_winds_bindings(nb::module_ m) { nb::module_ m_wind = @@ -31,54 +125,7 @@ inline void make_winds_bindings(nb::module_ m) { .def_rw("u", &sharp::WindComponents::u, "U wind component (m/s)") .def_rw("v", &sharp::WindComponents::v, "V wind component (m/s)"); - m_wind.def( - "helicity", - [](sharp::HeightLayer& layer, sharp::WindComponents storm_motion, - const_prof_arr_t height, const_prof_arr_t u_wind, - const_prof_arr_t v_wind) { - return sharp::helicity(layer, storm_motion, height.data(), - u_wind.data(), v_wind.data(), height.size()); - }, - nb::arg("layer"), nb::arg("storm_motion"), nb::arg("height"), - nb::arg("u_wind"), nb::arg("v_wind"), - R"pbdoc( -Computes the Storm Relative Helicity (SRH) over a given layer using -storm motion vector components stored in a WindComponents object. - -This integration occurs over the given arrays, using interpolation -for the top and bottom of the integration layer, and native data levels -in between. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer - A HeightLayer to integrate over (meters AGL) -storm_motion : nwsspc.sharp.calc.winds.WindComponents - A WindComponents object with the storm motion in m/s -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) -u_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of environment U-wind components (m/s) -v_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of environment V-wind components (m/s) - -Returns -------- -float - Storm Relative Helicity (m^2/s^2) - )pbdoc"); - - m_wind.def( - "helicity", - [](sharp::PressureLayer& layer, sharp::WindComponents storm_motion, - const_prof_arr_t pressure, const_prof_arr_t u_wind, - const_prof_arr_t v_wind) { - return sharp::helicity(layer, storm_motion, pressure.data(), - u_wind.data(), v_wind.data(), - pressure.size()); - }, - nb::arg("layer"), nb::arg("storm_motion"), nb::arg("pressure"), - nb::arg("u_wind"), nb::arg("v_wind"), + const char* helicity_template_doc = R"pbdoc( Computes the Storm Relative Helicity (SRH) over a given layer using storm motion vector components stored in a WindComponents object. @@ -89,12 +136,12 @@ in between. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer - A PressureLayer to integrate over (Pa) +layer : {0} + A layer to integrate over ({3}) storm_motion : nwsspc.sharp.calc.winds.WindComponents A WindComponents object with the storm motion in m/s -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {1} values ({2}) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of environment U-wind components (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -104,48 +151,13 @@ Returns ------- float Storm Relative Helicity (m^2/s^2) - )pbdoc"); - - m_wind.def( - "wind_shear", - [](sharp::HeightLayer layer, const_prof_arr_t height, - const_prof_arr_t u_wind, const_prof_arr_t v_wind) { - return sharp::wind_shear(layer, height.data(), u_wind.data(), - v_wind.data(), height.size()); - }, - nb::arg("layer"), nb::arg("height"), nb::arg("u_wind"), - nb::arg("v_wind"), - R"pbdoc( -Computes the U and V components of the wind shear over a -layer given the vertical sounding arrays of height, u_wind, -and v_wind. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer - HeightLayer for which to compute wind shear -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) -u_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of U-wind components (m/s) -v_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of V-wind components (m/s) - -Returns -------- -nwsspc.sharp.calc.winds.WindComponents - WindComponents of U and V wind shear components (m/s) - )pbdoc"); + )pbdoc"; + bind_helicity(m_wind, helicity_template_doc, + "nwsspc.sharp.calc.layer.HeightLayer"); + bind_helicity( + m_wind, helicity_template_doc, "nwsspc.sharp.calc.layer.PressureLayer"); - m_wind.def( - "wind_shear", - [](sharp::PressureLayer layer, const_prof_arr_t pressure, - const_prof_arr_t u_wind, const_prof_arr_t v_wind) { - return sharp::wind_shear(layer, pressure.data(), u_wind.data(), - v_wind.data(), pressure.size()); - }, - nb::arg("layer"), nb::arg("pressure"), nb::arg("u_wind"), - nb::arg("v_wind"), + const char* wind_shear_template_doc = R"pbdoc( Computes the U and V components of the wind shear over a layer given the vertical sounding arrays of height, u_wind, @@ -153,10 +165,10 @@ and v_wind. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer for which to compute wind shear -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height values (meters) +layer : {0} + Layer for which to compute wind shear ({3}) +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {1} values ({2}) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of U-wind components (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -166,13 +178,19 @@ Returns ------- nwsspc.sharp.calc.winds.WindComponents WindComponents of U and V wind shear components (m/s) - )pbdoc"); + )pbdoc"; + bind_wind_shear(m_wind, wind_shear_template_doc, + "nwsspc.sharp.calc.layer.HeightLayer"); + bind_wind_shear( + m_wind, wind_shear_template_doc, + "nwsspc.sharp.calc.layer.PressureLayer"); m_wind.def( "mean_wind", [](sharp::PressureLayer& layer, const_prof_arr_t pressure, - const_prof_arr_t u_wind, const prof_arr_t v_wind, + const_prof_arr_t u_wind, const_prof_arr_t v_wind, const bool weighted) { + check_equal_sizes(pressure, u_wind, v_wind); return sharp::mean_wind(layer, pressure.data(), u_wind.data(), v_wind.data(), pressure.size(), weighted); }, @@ -198,38 +216,20 @@ weighted : bool Returns ------- nwsspc.sharp.calc.winds.WindComponents - WindComponents of U anf V mean wind components (m/s) + WindComponents of U and V mean wind components (m/s) )pbdoc"); - m_wind.def( - "max_wind", - [](sharp::PressureLayer layer, const_prof_arr_t pressure, - const_prof_arr_t u_wind, const_prof_arr_t v_wind) { - if ((pressure.shape(0) != u_wind.shape(0)) || - (pressure.shape(0) != v_wind.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - auto u_comp = u_wind.view(); - auto v_comp = v_wind.view(); - auto pres = pressure.view(); - auto len = u_comp.shape(0); - - return sharp::max_wind(layer, pres.data(), u_comp.data(), - v_comp.data(), len); - }, - nb::arg("layer"), nb::arg("pressure"), nb::arg("u_wind"), - nb::arg("v_wind"), + const char* max_wind_template_doc = R"pbdoc( -Finds the maximum wind speed over a given nwsspc.sharp.calc.layer.PressureLayer, +Finds the maximum wind speed over a given layer, returning the vector components. Parameters ---------- -layer : nwsspc.sharp.calc.layer.PressureLayer - PressureLayer over which to compute mean -pressure : numpy.ndarray[dtype=float32] - 1D NumPy array of pressure coordinate values (Pa) +layer : {0} + Layer over which to compute the max ({3}) +{1} : numpy.ndarray[dtype=float32] + 1D NumPy array of {1} coordinate values ({2}) u_wind : numpy.ndarray[dtype=float32] 1D NumPy array of U-wind component values (m/s) v_wind : numpy.ndarray[dtype=float32] @@ -238,46 +238,12 @@ v_wind : numpy.ndarray[dtype=float32] Returns ------- nwsspc.sharp.calc.winds.WindComponents -)pbdoc"); - - m_wind.def( - "max_wind", - [](sharp::HeightLayer layer, const_prof_arr_t height, - const_prof_arr_t u_wind, const_prof_arr_t v_wind) { - if ((height.shape(0) != u_wind.shape(0)) || - (height.shape(0) != v_wind.shape(0))) { - throw nb::buffer_error( - "All input arrays must have the same size!"); - } - auto u_comp = u_wind.view(); - auto v_comp = v_wind.view(); - auto hght = height.view(); - auto len = u_comp.shape(0); +)pbdoc"; - return sharp::max_wind(layer, hght.data(), u_comp.data(), - v_comp.data(), len); - }, - nb::arg("layer"), nb::arg("height"), nb::arg("u_wind"), - nb::arg("v_wind"), - R"pbdoc( -Finds the maximum wind speed over a given nwsspc.sharp.calc.layer.HeightLayer, -returning the vector components. - -Parameters ----------- -layer : nwsspc.sharp.calc.layer.HeightLayer - PressureLayer over which to compute mean -height : numpy.ndarray[dtype=float32] - 1D NumPy array of height coordinate values (m) -u_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of U-wind component values (m/s) -v_wind : numpy.ndarray[dtype=float32] - 1D NumPy array of V-wind component values (m/s) - -Returns -------- -nwsspc.sharp.calc.winds.WindComponents -)pbdoc"); + bind_max_wind(m_wind, max_wind_template_doc, + "nwsspc.sharp.calc.layer.HeightLayer"); + bind_max_wind( + m_wind, max_wind_template_doc, "nwsspc.sharp.calc.layer.PressureLayer"); m_wind.def("vector_magnitude", &sharp::vector_magnitude, nb::arg("u_comp"), nb::arg("v_comp"), @@ -302,21 +268,17 @@ float m_wind.def( "vector_magnitude", [](const_prof_arr_t u_comp_arr, const_prof_arr_t v_comp_arr) { + check_equal_sizes(u_comp_arr, v_comp_arr); + const std::size_t NZ = u_comp_arr.size(); auto u_comp = u_comp_arr.view(); auto v_comp = v_comp_arr.view(); - auto len = u_comp.shape(0); - - float* wspd_arr = new float[u_comp.shape(0)]; - for (int idx = 0; idx < len; ++idx) { - wspd_arr[idx] = - sharp::vector_magnitude(u_comp(idx), v_comp(idx)); - } - - nb::capsule owner(wspd_arr, - [](void* p) noexcept { delete[] (float*)p; }); - - return out_arr_t(wspd_arr, {u_comp.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t idx = 0; idx < NZ; ++idx) { + out[idx] = + sharp::vector_magnitude(u_comp(idx), v_comp(idx)); + } + }); }, nb::arg("u_comp"), nb::arg("v_comp"), R"pbdoc( @@ -358,20 +320,16 @@ float m_wind.def( "vector_angle", [](const_prof_arr_t u_comp_arr, const_prof_arr_t v_comp_arr) { + check_equal_sizes(u_comp_arr, v_comp_arr); + const std::size_t NZ = u_comp_arr.size(); auto u_comp = u_comp_arr.view(); auto v_comp = v_comp_arr.view(); - auto len = u_comp.shape(0); - - float* wdir_arr = new float[u_comp.shape(0)]; - for (int idx = 0; idx < len; ++idx) { - wdir_arr[idx] = sharp::vector_angle(u_comp(idx), v_comp(idx)); - } - - nb::capsule owner(wdir_arr, - [](void* p) noexcept { delete[] (float*)p; }); - - return out_arr_t(wdir_arr, {u_comp.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (std::size_t idx = 0; idx < NZ; ++idx) { + out[idx] = sharp::vector_angle(u_comp(idx), v_comp(idx)); + } + }); }, nb::arg("u_comp"), nb::arg("v_comp"), R"pbdoc( @@ -413,19 +371,17 @@ float "u_component", [](const_prof_arr_t wind_speed_arr, const_prof_arr_t wind_direction_arr) { + check_equal_sizes(wind_speed_arr, wind_direction_arr); + const std::size_t NZ = wind_speed_arr.size(); auto wind_speed = wind_speed_arr.view(); auto wind_direction = wind_direction_arr.view(); - float* uwin_arr = new float[wind_speed.shape(0)]; - - for (size_t k = 0; k < wind_speed.shape(0); ++k) { - uwin_arr[k] = - sharp::u_component(wind_speed(k), wind_direction(k)); - } - - nb::capsule owner(uwin_arr, - [](void* p) noexcept { delete[] (float*)p; }); - return out_arr_t(uwin_arr, {wind_speed.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::u_component(wind_speed(k), wind_direction(k)); + } + }); }, nb::arg("wind_speed"), nb::arg("wind_direction"), R"pbdoc( @@ -466,19 +422,17 @@ float "v_component", [](const_prof_arr_t wind_speed_arr, const_prof_arr_t wind_direction_arr) { + check_equal_sizes(wind_speed_arr, wind_direction_arr); + const std::size_t NZ = wind_speed_arr.size(); auto wind_speed = wind_speed_arr.view(); auto wind_direction = wind_direction_arr.view(); - float* vwin_arr = new float[wind_speed.shape(0)]; - - for (size_t k = 0; k < wind_speed.shape(0); ++k) { - vwin_arr[k] = - sharp::v_component(wind_speed(k), wind_direction(k)); - } - nb::capsule owner(vwin_arr, - [](void* p) noexcept { delete[] (float*)p; }); - - return out_arr_t(vwin_arr, {wind_speed.shape(0)}, owner); + return make_output_array(NZ, [&](float* out) { + for (size_t k = 0; k < NZ; ++k) { + out[k] = + sharp::v_component(wind_speed(k), wind_direction(k)); + } + }); }, nb::arg("wind_speed"), nb::arg("wind_direction"), R"pbdoc( @@ -547,24 +501,27 @@ nwsspc.sharp.calc.winds.WindVector "components_to_vector", [](const_prof_arr_t u_comp_arr, const_prof_arr_t v_comp_arr) -> std::tuple { + check_equal_sizes(u_comp_arr, v_comp_arr); + const std::size_t NZ = u_comp_arr.size(); auto u_comp = u_comp_arr.view(); auto v_comp = v_comp_arr.view(); + auto wspd_buf = std::make_unique(NZ); + auto wdir_buf = std::make_unique(NZ); - float* wspd = new float[u_comp.shape(0)]; - float* wdir = new float[v_comp.shape(0)]; - - for (size_t k = 0; k < u_comp.shape(0); ++k) { - wspd[k] = sharp::vector_magnitude(u_comp(k), v_comp(k)); - wdir[k] = sharp::vector_angle(u_comp(k), v_comp(k)); + for (size_t k = 0; k < NZ; ++k) { + wspd_buf[k] = sharp::vector_magnitude(u_comp(k), v_comp(k)); + wdir_buf[k] = sharp::vector_angle(u_comp(k), v_comp(k)); } + float* wspd = wspd_buf.release(); + float* wdir = wdir_buf.release(); nb::capsule owner_wspd( wspd, [](void* p) noexcept { delete[] (float*)p; }); nb::capsule owner_wdir( wdir, [](void* p) noexcept { delete[] (float*)p; }); - auto wspd_arr = out_arr_t(wspd, {u_comp.shape(0)}, owner_wspd); - auto wdir_arr = out_arr_t(wdir, {u_comp.shape(0)}, owner_wdir); + auto wspd_arr = out_arr_t(wspd, {NZ}, owner_wspd); + auto wdir_arr = out_arr_t(wdir, {NZ}, owner_wdir); return std::make_tuple(wspd_arr, wdir_arr); }, @@ -585,7 +542,7 @@ Returns tuple[numpy.ndarray[dtype=float32], numpy.ndarray[dtype=float32]] wspd: 1D NumPy array of wind speeds (m/s) - wdir: 1D NumPy array of wind directiond (degrees from North) + wdir: 1D NumPy array of wind directions (degrees from North) )pbdoc"); m_wind.def( @@ -623,7 +580,7 @@ components as WindComponents. Parameters ---------- wind_vector : nwsspc.sharp.calc.winds.WindVector - WindVector containing wind wpeed (m/s) and direction (degrees from North) + WindVector containing wind speed (m/s) and direction (degrees from North) Returns ------- @@ -635,32 +592,35 @@ nwsspc.sharp.calc.winds.WindComponents "vector_to_components", [](const_prof_arr_t wspd_arr, const_prof_arr_t wdir_arr) -> std::tuple { + check_equal_sizes(wspd_arr, wdir_arr); + const std::size_t NZ = wspd_arr.size(); auto wspd = wspd_arr.view(); auto wdir = wdir_arr.view(); + auto u_comp = std::make_unique(NZ); + auto v_comp = std::make_unique(NZ); - float* u_comp = new float[wspd.shape(0)]; - float* v_comp = new float[wspd.shape(0)]; - - for (size_t k = 0; k < wspd.shape(0); ++k) { + for (std::size_t k = 0; k < NZ; ++k) { u_comp[k] = sharp::u_component(wspd(k), wdir(k)); v_comp[k] = sharp::v_component(wspd(k), wdir(k)); } + float* uwin = u_comp.release(); + float* vwin = v_comp.release(); nb::capsule owner_uwin( - u_comp, [](void* p) noexcept { delete[] (float*)p; }); + uwin, [](void* p) noexcept { delete[] (float*)p; }); nb::capsule owner_vwin( - v_comp, [](void* p) noexcept { delete[] (float*)p; }); + vwin, [](void* p) noexcept { delete[] (float*)p; }); - auto uwin_arr = out_arr_t(u_comp, {wspd.shape(0)}, owner_uwin); - auto vwin_arr = out_arr_t(v_comp, {wdir.shape(0)}, owner_vwin); + auto uwin_arr = out_arr_t(uwin, {NZ}, owner_uwin); + auto vwin_arr = out_arr_t(vwin, {NZ}, owner_vwin); return std::make_tuple(uwin_arr, vwin_arr); }, nb::arg("wspd"), nb::arg("wdir"), R"pbdoc( Given 1D NumPy arrays of the wind speed (m/s) and direction (degrees from North), -compute and return 1D NumPy arays of the zonal (U) and meridional (V) vector -componenWindComponents. +compute and return 1D NumPy arrays of the zonal (U) and meridional (V) vector +components. Parameters ---------- diff --git a/tests/python/test_parcel.py b/tests/python/test_parcel.py index d33a985c..54e3406e 100644 --- a/tests/python/test_parcel.py +++ b/tests/python/test_parcel.py @@ -232,6 +232,27 @@ def test_mixed_layer_parcel(): assert (pcl.eql_pressure == pytest.approx(20968, abs=1e-0)) assert (pcl.mpl_pressure == pytest.approx(11649, abs=1e-0)) + mix_lyr_hght = layer.pressure_layer_to_height( + mix_lyr, + snd_data["pres"], + snd_data["hght"], + False) + + pcl = parcel.Parcel.mixed_layer_parcel( + mix_lyr_hght, snd_data["pres"], snd_data["hght"], theta, snd_data["mixr"]) + vtmpk = pcl.lift_parcel(lifter, snd_data["pres"]) + buoy = thermo.buoyancy(vtmpk, snd_data["vtmp"]) + cape, cinh = pcl.cape_cinh(snd_data["pres"], snd_data["hght"], buoy) + mpl = pcl.maximum_parcel_level(snd_data["pres"], snd_data["hght"], buoy) + li = pcl.lifted_index(50000.0, snd_data["pres"], snd_data["vtmp"], vtmpk) + + assert (li == pytest.approx(-6.428, abs=1e-2)) + assert (cape == pytest.approx(1929.36, abs=5e-1)) + assert (cinh == pytest.approx(-133.71, abs=5e-1)) + assert (pcl.lfc_pressure == pytest.approx(67100, abs=1e-0)) + assert (pcl.eql_pressure == pytest.approx(20968, abs=1e-0)) + assert (pcl.mpl_pressure == pytest.approx(11649, abs=1e-0)) + def test_most_unstable_parcel(): search_layer = layer.PressureLayer( From 643e6baba1c974b105a04d786a7a9b6e69ff49bb Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 31 Mar 2026 15:28:33 -0500 Subject: [PATCH 50/55] make the FMT_HEADER_ONLY flag a CMake thing --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c55b10e7..98c9d3b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,10 @@ file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS add_library(SHARPlib ${SOURCES}) +target_compile_definitions(SHARPlib PUBLIC + $ +) + target_include_directories(SHARPlib PUBLIC $ $ From 673afa3f96cd9527a7405303674061d692a9d26d Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 31 Mar 2026 15:31:32 -0500 Subject: [PATCH 51/55] remove FMT defines from cpp files --- src/SHARPlib/layer.cpp | 4 +--- src/SHARPlib/params/fire.cpp | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/SHARPlib/layer.cpp b/src/SHARPlib/layer.cpp index c06d7639..b161d241 100644 --- a/src/SHARPlib/layer.cpp +++ b/src/SHARPlib/layer.cpp @@ -16,14 +16,12 @@ #include #include #include +#include #include #include #include -#define FMT_HEADER_ONLY -#include - namespace sharp { HeightLayer::HeightLayer(float bottom, float top, float delta) { diff --git a/src/SHARPlib/params/fire.cpp b/src/SHARPlib/params/fire.cpp index 83f94363..ad5f3a08 100644 --- a/src/SHARPlib/params/fire.cpp +++ b/src/SHARPlib/params/fire.cpp @@ -18,7 +18,6 @@ #include #include #include -#define FMT_HEADER_ONLY #include #include From 7197ca14fb253f1b6fbe78a1aa2fde8d3dc0bd18 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 31 Mar 2026 17:31:22 -0500 Subject: [PATCH 52/55] Change mean_wind to use native levels (#109, #110) This PR updates `mean_wind` to integrate on native-level data, rather than iterating on interpolated pressure levels. This should result in more accurate/correct computations, and potentially more performant as well. Tests were updated to account for changes in known-values. --- src/SHARPlib/winds.cpp | 82 +++++++++++++++++++++++++++++++------ tests/python/test_params.py | 24 +++++------ tests/python/test_winds.py | 8 ++-- tests/unit/test_winds.cpp | 24 +++++------ 4 files changed, 97 insertions(+), 41 deletions(-) diff --git a/src/SHARPlib/winds.cpp b/src/SHARPlib/winds.cpp index f405a961..630338b5 100644 --- a/src/SHARPlib/winds.cpp +++ b/src/SHARPlib/winds.cpp @@ -19,6 +19,8 @@ #include #include +#include "SHARPlib/algorithms.h" + namespace sharp { float u_component(float wind_speed, float wind_direction) { @@ -113,21 +115,75 @@ WindComponents mean_wind(PressureLayer layer, const float pressure[], if (layer.bottom > pressure[0]) layer.bottom = pressure[0]; if (layer.top < pressure[N - 1]) layer.top = pressure[N - 1]; - float pr_lvl = layer.bottom; - float u_sum = 0; - float v_sum = 0; - float weight = 0; - while (pr_lvl >= layer.top) { - float w = (weighted) ? pr_lvl : 1.0f; - u_sum += interp_pressure(pr_lvl, pressure, u_wind, N) * w; - v_sum += interp_pressure(pr_lvl, pressure, v_wind, N) * w; - weight += w; - pr_lvl += layer.delta; + LayerIndex idx = get_layer_index(layer, pressure, N); + + float u_lyr_bot = interp_pressure(layer.bottom, pressure, u_wind, N); + float v_lyr_bot = interp_pressure(layer.bottom, pressure, v_wind, N); + float u_lyr_top = interp_pressure(layer.top, pressure, u_wind, N); + float v_lyr_top = interp_pressure(layer.top, pressure, v_wind, N); + +#ifndef NO_QC + if ((u_lyr_bot == MISSING) || (v_lyr_bot == MISSING) || + (u_lyr_top == MISSING) || (v_lyr_top == MISSING)) { + return {MISSING, MISSING}; + } +#endif + + float u_integ = 0.0f; + float v_integ = 0.0f; + float w_integ = 0.0f; + float dummy_w = 0.0f; + + float p_prev = layer.bottom; + float u_prev = u_lyr_bot; + float v_prev = v_lyr_bot; + + for (std::ptrdiff_t k = idx.kbot; k <= idx.ktop; ++k) { +#ifndef NO_QC + if ((u_wind[k] == MISSING) || (v_wind[k] == MISSING)) { + continue; + } +#endif + const float p_curr = pressure[k]; + const float u_curr = u_wind[k]; + const float v_curr = v_wind[k]; + + if (weighted) { + u_integ += _integ_trapz(u_curr * p_curr, u_prev * p_prev, p_curr, + p_prev, dummy_w, false); + v_integ += _integ_trapz(v_curr * p_curr, v_prev * p_prev, p_curr, + p_prev, dummy_w, false); + w_integ += + _integ_trapz(p_curr, p_prev, p_curr, p_prev, dummy_w, false); + } else { + u_integ += + _integ_trapz(u_curr, u_prev, p_curr, p_prev, w_integ, true); + v_integ += + _integ_trapz(v_curr, v_prev, p_curr, p_prev, dummy_w, false); + } + + p_prev = p_curr; + u_prev = u_curr; + v_prev = v_curr; + } + + { + if (weighted) { + u_integ += _integ_trapz(u_lyr_top * layer.top, u_prev * p_prev, + layer.top, p_prev, dummy_w, false); + v_integ += _integ_trapz(v_lyr_top * layer.top, v_prev * p_prev, + layer.top, p_prev, dummy_w, false); + w_integ += _integ_trapz(layer.top, p_prev, layer.top, p_prev, + dummy_w, false); + } else { + u_integ += _integ_trapz(u_lyr_top, u_prev, layer.top, p_prev, + w_integ, true); + v_integ += _integ_trapz(v_lyr_top, v_prev, layer.top, p_prev, + dummy_w, false); + } } - const float mean_u = u_sum / weight; - const float mean_v = v_sum / weight; - return {mean_u, mean_v}; + return {u_integ / w_integ, v_integ / w_integ}; } /// @cond DOXYGEN_IGNORE diff --git a/tests/python/test_params.py b/tests/python/test_params.py index f1c8981a..31c859ba 100644 --- a/tests/python/test_params.py +++ b/tests/python/test_params.py @@ -123,8 +123,8 @@ def test_bunkers_motion_nonparcel(): mw_lyr, shr_lyr, ) - assert (storm_mtn.u == pytest.approx(10.232333)) - assert (storm_mtn.v == pytest.approx(5.7385511)) + assert (storm_mtn.u == pytest.approx(10.197119)) + assert (storm_mtn.v == pytest.approx(5.7385821)) def test_bunkers_motion(): @@ -150,8 +150,8 @@ def test_bunkers_motion(): eil, mupcl ) - assert (storm_mtn.u == pytest.approx(9.65811)) - assert (storm_mtn.v == pytest.approx(5.558156)) + assert (storm_mtn.u == pytest.approx(9.701575)) + assert (storm_mtn.v == pytest.approx(5.622299)) def test_corfidi_vectors(): @@ -162,10 +162,10 @@ def test_corfidi_vectors(): snd_data["vwin"] ) - assert (upshear.u == pytest.approx(12.7017, abs=1e-3)) - assert (upshear.v == pytest.approx(2.99329, abs=1e-3)) - assert (downshear.u == pytest.approx(23.2054, abs=1e-3)) - assert (downshear.v == pytest.approx(16.10519, abs=1e-3)) + assert (upshear.u == pytest.approx(12.5269, abs=1e-3)) + assert (upshear.v == pytest.approx(2.7875, abs=1e-3)) + assert (downshear.u == pytest.approx(23.1313, abs=1e-3)) + assert (downshear.v == pytest.approx(15.99528, abs=1e-3)) def test_effective_bulk_wind(): @@ -287,7 +287,7 @@ def test_stp_scp_ship_dcp_lhp(): esrh, ebwd ) - assert (stp == pytest.approx(0.48329, abs=1e-4)) + assert (stp == pytest.approx(0.48378, abs=1e-4)) scp = params.supercell_composite_parameter(mupcl.cape, esrh, ebwd) assert (scp == pytest.approx(7.9699, abs=1e-1)) @@ -326,7 +326,7 @@ def test_stp_scp_ship_dcp_lhp(): snd_data["uwin"], snd_data["vwin"] ) - assert (lhp == pytest.approx(14.2394, abs=1e-2)) + assert (lhp == pytest.approx(14.1323, abs=1e-2)) def test_ehi(): @@ -371,7 +371,7 @@ def test_ehi(): ) ehi = params.energy_helicity_index(pcl.cape, srh) - assert (ehi == pytest.approx(4.41228, abs=1e-5)) + assert (ehi == pytest.approx(4.38889, abs=1e-5)) def test_convective_temperature(): @@ -440,4 +440,4 @@ def test_pft(): snd_data["vwin"], snd_data["theta"] ) - assert (pft == pytest.approx(153856081920.0, abs=1e6)) + assert (pft == pytest.approx(158187356160.0, abs=1e6)) diff --git a/tests/python/test_winds.py b/tests/python/test_winds.py index fc1ead95..f4a35419 100644 --- a/tests/python/test_winds.py +++ b/tests/python/test_winds.py @@ -78,14 +78,14 @@ def test_mean_wind(): mw = winds.mean_wind( plyr, snd_data["pres"], snd_data["uwin"], snd_data["vwin"]) - assert (mw.u == pytest.approx(4.39905, abs=1e-5)) - assert (mw.v == pytest.approx(11.97378, abs=1e-5)) + assert (mw.u == pytest.approx(4.474552, abs=1e-5)) + assert (mw.v == pytest.approx(12.094197, abs=1e-5)) mw = winds.mean_wind( plyr, snd_data["pres"], snd_data["uwin"], snd_data["vwin"], True) - assert (mw.u == pytest.approx(3.683177, abs=1e-5)) - assert (mw.v == pytest.approx(11.69769, abs=1e-5)) + assert (mw.u == pytest.approx(3.787980, abs=1e-5)) + assert (mw.v == pytest.approx(11.847604, abs=1e-5)) def test_max_wind(): diff --git a/tests/unit/test_winds.cpp b/tests/unit/test_winds.cpp index ce2da08b..97798074 100644 --- a/tests/unit/test_winds.cpp +++ b/tests/unit/test_winds.cpp @@ -160,23 +160,23 @@ TEST_CASE("Testing mean wind calculations") { const auto mean_layer6 = sharp::mean_wind(layer3, pres, u_wind, v_wind, NZ, false); - CHECK(mean_layer1.u == doctest::Approx(14.7669)); - CHECK(mean_layer1.v == doctest::Approx(14.7669)); + CHECK(mean_layer1.u == doctest::Approx(14.7222)); + CHECK(mean_layer1.v == doctest::Approx(14.7222)); - CHECK(mean_layer2.u == doctest::Approx(21.0501)); - CHECK(mean_layer2.v == doctest::Approx(21.0501)); + CHECK(mean_layer2.u == doctest::Approx(21)); + CHECK(mean_layer2.v == doctest::Approx(21)); - CHECK(mean_layer3.u == doctest::Approx(26.2741)); - CHECK(mean_layer3.v == doctest::Approx(26.2741)); + CHECK(mean_layer3.u == doctest::Approx(26.2121)); + CHECK(mean_layer3.v == doctest::Approx(26.2121)); - CHECK(mean_layer4.u == doctest::Approx(14.9538)); - CHECK(mean_layer4.v == doctest::Approx(14.9538)); + CHECK(mean_layer4.u == doctest::Approx(15)); + CHECK(mean_layer4.v == doctest::Approx(15)); - CHECK(mean_layer5.u == doctest::Approx(22.4424)); - CHECK(mean_layer5.v == doctest::Approx(22.4424)); + CHECK(mean_layer5.u == doctest::Approx(22.5)); + CHECK(mean_layer5.v == doctest::Approx(22.5)); - CHECK(mean_layer6.u == doctest::Approx(32.3939)); - CHECK(mean_layer6.v == doctest::Approx(32.3939)); + CHECK(mean_layer6.u == doctest::Approx(32.5)); + CHECK(mean_layer6.v == doctest::Approx(32.5)); } TEST_CASE("Testing max wind calculations") { From ebf6e655983a84b1656a294ed7686eb4017305f8 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Tue, 31 Mar 2026 17:44:50 -0500 Subject: [PATCH 53/55] Address integrate_layer_trapz inconsistencies (#111, #112) This PR addresses some inconsistencies with how `integrate_layer_trapz` handles missing data. It also avoids some edge cases where a divide by zero can occur, and correctly returns MISSING if all data are missing. --- include/SHARPlib/layer.h | 88 ++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/include/SHARPlib/layer.h b/include/SHARPlib/layer.h index b7e9acc6..6021f71e 100644 --- a/include/SHARPlib/layer.h +++ b/include/SHARPlib/layer.h @@ -479,20 +479,20 @@ template const std::ptrdiff_t N, const int integ_sign = 0, const bool weighted = false) { - float var_lyr_bottom; - float coord_lyr_bottom = layer.bottom; - - float var_lyr_top; - float coord_lyr_top = layer.top; - float integrated = 0.0; float weights = 0.0; const bool isign = std::signbit(integ_sign); - // using constexpr means that this if statement optimizes - // away at compile time since the layer coordinate is known LayerIndex idx = get_layer_index(layer, coord_array, N); + + float var_lyr_bottom; + float var_lyr_top; + + auto include = [integ_sign, isign](float layer_avg) -> float { + return (float)((!integ_sign) | (isign == std::signbit(layer_avg))); + }; + if constexpr (layer.coord == LayerCoordinate::height) { var_lyr_bottom = interp_height(layer.bottom, coord_array, var_array, N); var_lyr_top = interp_height(layer.top, coord_array, var_array, N); @@ -502,6 +502,12 @@ template var_lyr_top = interp_pressure(layer.top, coord_array, var_array, N); } + // When the layer falls entirely between two native levels + // with no interior points, handle as a single trapezoid. + // NOTE: weights is accumulated by _integ_trapz through its + // reference parameter whenever weighted=true. The same weights + // variable must be passed to every _integ_trapz call in this + // function for the final division to be correct. if (idx.ktop < idx.kbot) { if ((var_lyr_bottom == MISSING) || (var_lyr_top == MISSING)) { return MISSING; @@ -509,49 +515,48 @@ template float layer_avg = _integ_trapz(var_lyr_top, var_lyr_bottom, layer.top, layer.bottom, weights, weighted); - float cond = - (float)((!integ_sign) | (isign == std::signbit(layer_avg))); - integrated += cond * layer_avg; + integrated += include(layer_avg) * layer_avg; } else { - for (std::ptrdiff_t k = idx.kbot; k < idx.ktop; ++k) { + // Start from the interpolated layer bottom and walk + // through native levels to the interpolated top. + // When a level is MISSING, the next valid level forms a + // wider trapezoid from the last valid point, bridging + // the gap via linear interpolation. + float coord_prev = layer.bottom; + float var_prev = var_lyr_bottom; + bool have_prev = (var_lyr_bottom != MISSING); + bool any_segment = false; + + for (std::ptrdiff_t k = idx.kbot; k <= idx.ktop; ++k) { #ifndef NO_QC if (var_array[k] == MISSING) { continue; } #endif + if (have_prev) { + float layer_avg = + _integ_trapz(var_array[k], var_prev, coord_array[k], + coord_prev, weights, weighted); - float coord_bottom = coord_array[k]; - float var_bottom = var_array[k]; - - float coord_top = coord_array[k + 1]; - float var_top = var_array[k + 1]; - - float layer_avg = _integ_trapz(var_top, var_bottom, coord_top, - coord_bottom, weights, weighted); + integrated += include(layer_avg) * layer_avg; + any_segment = true; + } - float cond = - (float)((!integ_sign) | (isign == std::signbit(layer_avg))); - integrated += cond * layer_avg; + coord_prev = coord_array[k]; + var_prev = var_array[k]; + have_prev = true; } - // interpolated bottom of layer + // Final segment: last valid native level to the + // interpolated layer top + if (have_prev && (var_lyr_top != MISSING)) { + float layer_avg = _integ_trapz(var_lyr_top, var_prev, layer.top, + coord_prev, weights, weighted); - if (var_lyr_bottom != sharp::MISSING) { - float layer_avg = _integ_trapz(var_array[idx.kbot], var_lyr_bottom, - coord_array[idx.kbot], - coord_lyr_bottom, weights, weighted); - float cond = ((!integ_sign) | (isign == std::signbit(layer_avg))); - integrated += cond * layer_avg; - } - - // interpolated top of layer - if (var_lyr_top != sharp::MISSING) { - float layer_avg = - _integ_trapz(var_lyr_top, var_array[idx.ktop], coord_lyr_top, - coord_array[idx.ktop], weights, weighted); - float cond = ((!integ_sign) | (isign == std::signbit(layer_avg))); - integrated += cond * layer_avg; + integrated += include(layer_avg) * layer_avg; + any_segment = true; } + if (!any_segment) return MISSING; } if constexpr (layer.coord == LayerCoordinate::pressure) { @@ -559,7 +564,10 @@ template weights *= -1.0f; } - if (weighted) integrated /= weights; + if (weighted) { + if (weights == 0.0f) return MISSING; + integrated /= weights; + } return integrated; } From 05593c4a610b4d5dc13a5889efdf96b7daf3db50 Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 1 Apr 2026 14:15:49 -0500 Subject: [PATCH 54/55] Added serialization functionality to lifters and LUT data (#115, #116) --- include/SHARPlib/parcel.h | 52 +++++++++++++++++ src/nanobind/nwsspc/sharp/calc/parcel.pyi | 16 ++++++ src/nanobind/parcel_bindings.h | 70 ++++++++++++++++++++++- 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index a1a3e50a..fe9dbdfc 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -278,6 +278,16 @@ struct lut_data { */ std::vector m_LUT_pcl_tmpk; + /** + * \brief Dispatch tag for constructing from serialized data + */ + struct from_serialized_t {}; + + /** + * \brief Dispatch tag for constructing from serialized data + */ + static constexpr from_serialized_t from_serialized{}; + /** * \author Kelton Halbert - NWS Storm Prediction Center * @@ -323,6 +333,48 @@ struct lut_data { generate_table(); } + /** + * \author Kelton Halbert - NWS Storm Prediction Center + * + * \brief Create a pseudoadiabatic lookup table (LUT) + * + * Construct the pseudoadiabatic lookup table (LUT) using + * the provided lifter and LUT configuration options. This + * lookup data is safe to share with many threads, but each + * thread must use its own sharp::lifter_lut. This constructor + * is intended only for loading serialized data. + * + * \param lifter Parcel lifting function/functor + * \param pmin Minimum Pressure (Pa) + * \param pmax Maximum pressure (Pa) + * \param thte_min Minimum theta-e (K) + * \param thte_max Maximum theta-e (K) + * \param n_logp Number of logarithmic pressure levels + * \param n_thetae Number of theta-e levels + * \param table Previously constructed LUT data. + */ + lut_data(from_serialized_t, Lft lifter, float pmin, float pmax, + float thte_min, float thte_max, std::size_t n_logp, + std::size_t n_thetae, std::vector table) + : m_lifter(std::move(lifter)), + pres_min(pmin), + pres_max(pmax), + thetae_min(thte_min), + thetae_max(thte_max), + num_logp(n_logp), + num_thetae(n_thetae), + m_logp_max(std::log(pmax)), + m_delta_thetae_inv(static_cast(n_thetae - 1) / + (thte_max - thte_min)), + m_delta_logp_inv(static_cast(n_logp - 1) / + (std::log(pmin) - std::log(pmax))), + m_LUT_pcl_tmpk(std::move(table)) { + if (m_LUT_pcl_tmpk.size() != num_logp * num_thetae) { + throw std::invalid_argument( + "lut_data: table size does not match dimensions"); + } + } + /** * \author Kelton Halbert - NWS Storm Prediction Center * diff --git a/src/nanobind/nwsspc/sharp/calc/parcel.pyi b/src/nanobind/nwsspc/sharp/calc/parcel.pyi index 844d2ba0..d7bfae9e 100644 --- a/src/nanobind/nwsspc/sharp/calc/parcel.pyi +++ b/src/nanobind/nwsspc/sharp/calc/parcel.pyi @@ -70,6 +70,10 @@ class lifter_wobus: The temperature of the lifted parcel (K) """ + def __getstate__(self) -> tuple: ... + + def __setstate__(self, arg: tuple, /) -> None: ... + def parcel_virtual_temperature(self, pres: float, tmpk: float) -> float: """ Computes the virtual temperature of the parcel (after saturation). @@ -165,6 +169,10 @@ class lifter_cm1: The temperature of the lifted parcel (K) """ + def __getstate__(self) -> tuple: ... + + def __setstate__(self, arg: tuple, /) -> None: ... + def parcel_virtual_temperature(self, pres: float, tmpk: float) -> float: """ Computes the virtual temperature of the parcel (after saturation). @@ -203,6 +211,10 @@ class lut_data_wobus: @property def num_thetae(self) -> int: ... + def __getstate__(self) -> tuple: ... + + def __setstate__(self, arg: tuple, /) -> None: ... + @overload def lut_data(lifter: lifter_wobus, pmin: float = 5000.0, pmax: float = 110000.0, thte_min: float = 210.0, thte_max: float = 430.0, n_logp: int = 201, n_thetae: int = 221) -> lut_data_wobus: """ @@ -388,6 +400,10 @@ class lut_data_cm1: @property def num_thetae(self) -> int: ... + def __getstate__(self) -> tuple: ... + + def __setstate__(self, arg: tuple, /) -> None: ... + class lifter_lut_cm1: """ A parcel lifter functor that uses a pseudoadiabatic lookup table (LUT) diff --git a/src/nanobind/parcel_bindings.h b/src/nanobind/parcel_bindings.h index 86289b87..9026dae7 100644 --- a/src/nanobind/parcel_bindings.h +++ b/src/nanobind/parcel_bindings.h @@ -107,7 +107,52 @@ void bind_lut_data_class(nb::class_>& cls) { .def_ro("thte_min", &sharp::lut_data::thetae_min) .def_ro("thte_max", &sharp::lut_data::thetae_max) .def_ro("num_logp", &sharp::lut_data::num_logp) - .def_ro("num_thetae", &sharp::lut_data::num_thetae); + .def_ro("num_thetae", &sharp::lut_data::num_thetae) + .def("__getstate__", + [](const sharp::lut_data& self) { + constexpr int version = 1; + // Serialize the LUT as raw bytes to avoid per-element overhead + const auto& tbl = self.m_LUT_pcl_tmpk; + nb::bytes table_bytes( + reinterpret_cast(tbl.data()), + tbl.size() * sizeof(float)); + return nb::make_tuple(version, self.m_lifter, self.pres_min, + self.pres_max, self.thetae_min, + self.thetae_max, self.num_logp, + self.num_thetae, table_bytes); + }) + .def("__setstate__", [](sharp::lut_data& self, nb::tuple state) { + auto ver = nb::cast(state[0]); + if (ver != 1) { + throw std::runtime_error( + "lut_data: unsupported pickled object version"); + } + auto lifter = nb::cast(state[1]); + auto pmin = nb::cast(state[2]); + auto pmax = nb::cast(state[3]); + auto thte_min = nb::cast(state[4]); + auto thte_max = nb::cast(state[5]); + auto n_logp = nb::cast(state[6]); + auto n_thetae = nb::cast(state[7]); + auto tbl_bytes = nb::cast(state[8]); + + // Reconstruct the vector from the raw bytes + const std::size_t n_floats = n_logp * n_thetae; + const std::size_t expected_bytes = n_floats * sizeof(float); + if (static_cast(tbl_bytes.size()) != expected_bytes) { + throw std::runtime_error( + "lut_data: serialized table size mismatch"); + } + const float* src = + reinterpret_cast(tbl_bytes.c_str()); + std::vector table(src, src + n_floats); + + // Placement-new with the deserialization constructor — + // skips generate_table() + new (&self) sharp::lut_data( + sharp::lut_data::from_serialized, std::move(lifter), pmin, + pmax, thte_min, thte_max, n_logp, n_thetae, std::move(table)); + }); } template @@ -460,6 +505,15 @@ Returns float The temperature of the lifted parcel (K) )pbdoc") + .def("__getstate__", + [](const sharp::lifter_wobus& self) { + return nb::make_tuple(self.converge); + }) + .def("__setstate__", + [](sharp::lifter_wobus& self, nb::tuple state) { + new (&self) sharp::lifter_wobus{}; + self.converge = nb::cast(state[0]); + }) .def("parcel_virtual_temperature", &sharp::lifter_wobus::parcel_virtual_temperature, nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( @@ -537,7 +591,19 @@ Returns float The temperature of the lifted parcel (K) )pbdoc") - + .def("__getstate__", + [](const sharp::lifter_cm1& self) { + return nb::make_tuple(static_cast(self.ma_type), + self.pressure_incr, self.converge); + }) + .def("__setstate__", + [](sharp::lifter_cm1& self, nb::tuple state) { + new (&self) sharp::lifter_cm1{}; + self.ma_type = + static_cast(nb::cast(state[0])); + self.pressure_incr = nb::cast(state[1]); + self.converge = nb::cast(state[2]); + }) .def("parcel_virtual_temperature", &sharp::lifter_cm1::parcel_virtual_temperature, nb::arg("pres"), nb::arg("tmpk"), R"pbdoc( From 7a6b1ef752196998a4b7f350f1077f47d9c8581e Mon Sep 17 00:00:00 2001 From: Kelton Halbert Date: Wed, 1 Apr 2026 16:15:36 -0500 Subject: [PATCH 55/55] updated ipynb example (#114) --- examples/Python/hrrr-example.ipynb | 135 +++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 28 deletions(-) diff --git a/examples/Python/hrrr-example.ipynb b/examples/Python/hrrr-example.ipynb index 0ed15f8b..5b4a573f 100644 --- a/examples/Python/hrrr-example.ipynb +++ b/examples/Python/hrrr-example.ipynb @@ -54,6 +54,7 @@ "\n", "client = Client(\n", " n_workers=8, \n", + " threads_per_worker=1,\n", " memory_limit='8GB', \n", " silence_logs=logging.ERROR\n", ")\n", @@ -195,6 +196,7 @@ " pres, hght, \n", " tmpk, spfh, \n", " uwin, vwin, \n", + " lut_data=None\n", "):\n", " def qc(val):\n", " if (val == constants.MISSING): return np.nan\n", @@ -220,12 +222,17 @@ " theta,\n", " mixr\n", " )\n", - "\n", - " lifter = parcel.lifter_cm1()\n", - " lifter.ma_type = thermo.adiabat.pseudo_liq\n", - " lifter.converge = 0.15 # K\n", - " lifter.pressure_incr = 500.0 # 5 hPa\n", - " \n", + " ## If a lookup table is not provided,\n", + " ## construct a parcel lifter for use\n", + " lifter = None\n", + " if lut_data is None:\n", + " lifter = parcel.lifter_cm1()\n", + " lifter.ma_type = thermo.adiabat.pseudo_liq\n", + " lifter.converge = 0.15 # K\n", + " lifter.pressure_incr = 500.0 # 5 hPa\n", + " else:\n", + " lifter = parcel.lifter_lut(lut_data)\n", + " \n", " # lift the parcel and get CAPE/CINH\n", " pcl_vtmp = mlpcl.lift_parcel(lifter, pres)\n", " pcl_buoy = thermo.buoyancy(pcl_vtmp, vtmp)\n", @@ -273,28 +280,16 @@ " vwin\n", " )\n", "\n", - " if mupcl.eql_pressure != constants.MISSING:\n", - " # Get the effective bulk wind difference\n", - " eql_hght = interp.interp_pressure(\n", - " mupcl.eql_pressure,\n", - " pres,\n", - " hght\n", - " )\n", - " depth = (eql_hght - eil_hght.bottom)*0.5\n", - " ebwd_lyr = layer.HeightLayer(\n", - " eil_hght.bottom, \n", - " float(eil_hght.bottom + depth)\n", - " )\n", - " ebwd_cmp = winds.wind_shear(\n", - " ebwd_lyr,\n", - " hght,\n", - " uwin,\n", - " vwin\n", - " )\n", - " ebwd = winds.vector_magnitude(ebwd_cmp.u, ebwd_cmp.v)\n", - " else:\n", - " ebwd_cmp = winds.WindComponents(constants.MISSING, constants.MISSING)\n", - " ebwd = constants.MISSING\n", + " ebwd_cmp = params.effective_bulk_wind_difference(\n", + " pres, \n", + " hght,\n", + " uwin,\n", + " vwin,\n", + " eil,\n", + " mupcl.eql_pressure\n", + " )\n", + "\n", + " ebwd = winds.vector_magnitude(ebwd_cmp.u, ebwd_cmp.v)\n", " \n", " stp = params.significant_tornado_parameter(\n", " mlpcl,\n", @@ -396,6 +391,90 @@ "print(result_ds)" ] }, + { + "cell_type": "markdown", + "id": "cc3224ba-5fc3-42f2-a94e-5e9aa52306e7", + "metadata": {}, + "source": [ + "## **NEW in SHARPlib v1.4.0 - Lookup Table (LUT) Based Parcel Ascent!**\n", + "It is a fairly common performance optimization (especially when processing large gridded datasets) to precompute a lookup table for parcel ascent. Instead of solving the ODEs directly for every grid point and every parcel lift operation, you create a sufficiently dense lookup table that you can perform interpolation operations on. This can dramatically improve performance, and as of version 1.4.0, SHARPlib supports generating lookup table data with any of the configurable parcel lifters. The catch: this only works for ***pseudoadiabatic*** ascent, and does not work for reversible (adiabatic) ascent. These next cells demonstrate the construction of the LUT data and the usage of ```lifter_lut```.\n", + "\n", + "The LUT data stored in the ```lut_data``` struct can be shared across multiple threads without making copies (it will make copies if using multiple nodes) and is thread safe. Each thread should have its own ```lifter_lut```, however." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2ec4b2b-6935-4b18-be29-2f4a59d0d842", + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "lifter_cm1 = parcel.lifter_cm1()\n", + "lifter_cm1.ma_type = thermo.adiabat.pseudo_liq\n", + "lifter_cm1.converge = 0.001 # K\n", + "lifter_cm1.pressure_incr = 50.0 # every 0.5 hPa\n", + "\n", + "## This will construct the LUT with the defaults. The LUT \n", + "## bounds and discretization can be specified. These \n", + "## defaults are in the documentation, but provided for \n", + "## clarity below:\n", + "## min_pressure: 5000.0 Pa\n", + "## max_pressure: 110000.0 Pa\n", + "## min_thetae: 210.0 K\n", + "## max_thetae: 430.0f\n", + "## num_logp: 201\n", + "## num_thetae: 221\n", + "lut_cm1 = parcel.lut_data(lifter_cm1)\n", + "print(help(parcel.lut_data))" + ] + }, + { + "cell_type": "markdown", + "id": "69e01d81-babe-40d1-b79d-dcba6db54b55", + "metadata": {}, + "source": [ + "Now we can re-run our computations, this time passing the lookup table data to the computation function and compare the execution time! A note on the comparison, however: when no LUT is provided, the function configures the ```lifter_cm1``` function with an integration step of `5 hPa` and a convergence accuracy of `0.15 K`. The lookup table, however, was constructed with an integration step of `0.5 hPa` and a convergence accuracy of `0.001 K`. So, the lookup table should not only be faster than directly calling `lifter_cm1`, but also more accurate!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b41dd91-93a9-4118-b7b6-b1505c91da1a", + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "result_func = xr.apply_ufunc(\n", + " compute_everything, \n", + " ds_merged[\"pres\"], ds_merged[\"gh\"],\n", + " ds_merged[\"t\"], ds_merged[\"q\"],\n", + " ds_merged[\"u\"], ds_merged[\"v\"],\n", + " input_core_dims=input_core_dims,\n", + " output_core_dims=output_core_dims,\n", + " vectorize=True,\n", + " dask=\"parallelized\",\n", + " output_dtypes=output_dtypes,\n", + " ## pass our LUT as a keyword \n", + " ## argument to the compute \n", + " ## function\n", + " kwargs={\"lut_data\": lut_cm1},\n", + ")\n", + "result_dict = {name: data for name, data in zip(output_names, result_func)}\n", + "result_ds = xr.Dataset(result_dict)\n", + "result_ds = result_ds.compute()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fe39f48f-8909-47ad-a16c-adc81ac661c4", + "metadata": {}, + "outputs": [], + "source": [ + "print(result_ds)" + ] + }, { "cell_type": "markdown", "id": "6252d8f4-c279-444b-a310-75c57f415e16",