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 $ $ 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/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 48baae86..bb43c91e 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,10 +18,27 @@ 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 +.. 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_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 ------- Parcel creation, parcel ascent, and integrated CAPE/CINH. 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", 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; } diff --git a/include/SHARPlib/params/convective.h b/include/SHARPlib/params/convective.h index 7de41b93..e8191f81 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_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_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 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_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_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); + /// @endcond } // end namespace sharp diff --git a/include/SHARPlib/params/fire.h b/include/SHARPlib/params/fire.h index 40a51c14..67a09c30 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_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); + +extern 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); + /// @endcond } // namespace sharp diff --git a/include/SHARPlib/parcel.h b/include/SHARPlib/parcel.h index e483dfa3..fe9dbdfc 100644 --- a/include/SHARPlib/parcel.h +++ b/include/SHARPlib/parcel.h @@ -18,7 +18,13 @@ #include #include +#include +#include #include +#include +#include +#include +#include namespace sharp { @@ -44,8 +50,16 @@ 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; + /** * \brief The iterative convergence criteria (K) */ @@ -107,6 +121,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; /** @@ -188,30 +205,551 @@ 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; + + /** + * \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 + * + * \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) + : 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::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(); + } + + /** + * \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 + * + * \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); + + 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}; + } + + /** + * \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, + 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; + } + + /** + * \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 { + 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: + /** + * \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); + + const float delta_thetae = (thetae_max - thetae_min) / (num_thetae - 1); + for (std::size_t i = 0; i < num_thetae; ++i) { + thetae_coord[i] = thetae_min + i * delta_thetae; + } + + 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) { + 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 = thetae_coord[i]; + const float tmpk = solve_tmpk_for_thetae(pres_max, thetae_target); + + 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(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; + + pres_curr = pres; + } + } + } + + /** + * \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; + float tmpk_hi = 330.0f; + float tmpk_mid = ZEROCNK; + + 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 = + sharp::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; + } + } + return tmpk_mid; + } +}; + +/** + * \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 { + /** + * \brief Whether the parcel should be lifted from the LCL or last level + */ + 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; + + /** + * \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) {} + + /** + * \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; + + 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); + + 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); + + if (lcl_tmpk < t_min_bound || lcl_tmpk > t_max_bound || + 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); + } + + /** + * \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 ((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); + } + + 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); + const std::size_t i1 = i0 + 1; + 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(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) { + return m_lifter.parcel_virtual_temperature(pres, tmpk); + } else { + return sharp::virtual_temperature(tmpk, + sharp::mixratio(pres, tmpk)); + } + } +}; + // //////////// END FUNCTORS /////////// /** * \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 { /** @@ -244,7 +782,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 @@ -287,8 +826,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; @@ -344,7 +883,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); @@ -384,19 +924,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) @@ -427,7 +968,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. @@ -523,11 +1065,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 */ @@ -572,11 +1116,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 @@ -643,8 +1190,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(""); @@ -795,6 +1342,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_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_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_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_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::mixed_layer_parcel( PressureLayer& mix_layer, const float pressure[], const float height[], const float pot_temperature[], const float wv_mixratio[], @@ -814,6 +1389,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_lut& liftpcl, const float pressure_arr[], + float pcl_vtmpk_arr[], const std::ptrdiff_t N); + +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( lifter_wobus& liftpcl, const float pressure_arr[], float pcl_tmpk_arr[], const std::ptrdiff_t N); @@ -822,6 +1405,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_lut& liftpcl, const float pressure_arr[], + float pcl_tmpk_arr[], const std::ptrdiff_t N); + +extern template void DowndraftParcel::lower_parcel>( + lifter_lut& 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 ccfaa6c3..80130ea7 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)) { @@ -731,22 +731,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_lut; + +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_lut& lifter, float pressure, float temperature, + float dewpoint); + +extern template float wetbulb>( + lifter_lut& 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_lut& lifter, float pressure, float temperature, + float dewpoint); + +extern template float theta_wetbulb>( + lifter_lut& lifter, float pressure, float temperature, + float dewpoint); + /// @endcond } // end namespace sharp 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/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/convective.cpp b/src/SHARPlib/params/convective.cpp index 919a4370..33208f11 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_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_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 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_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_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); + /// @endcond } // end namespace sharp diff --git a/src/SHARPlib/params/fire.cpp b/src/SHARPlib/params/fire.cpp index 1e0aba04..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 @@ -114,6 +113,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_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_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); + /// @endcond } // namespace sharp diff --git a/src/SHARPlib/parcel.cpp b/src/SHARPlib/parcel.cpp index a08756f6..946d1fe1 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_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_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_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_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::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_lut& liftpcl, const float pressure_arr[], + float pcl_vtmpk_arr[], const std::ptrdiff_t N); + +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( 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_lut& liftpcl, const float pressure_arr[], + float pcl_tmpk_arr[], const std::ptrdiff_t N); + +template void DowndraftParcel::lower_parcel>( + lifter_lut& 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 108a3e81..a95afd9e 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; } @@ -318,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 std::nanf(""); + } #endif // set up solver variables @@ -642,18 +647,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_lut& lifter, float pressure, float temperature, + float dewpoint); + +template float wetbulb>(lifter_lut& 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_lut& lifter, float pressure, float temperature, + float dewpoint); + +template float theta_wetbulb>( + lifter_lut& lifter, float pressure, float temperature, + float dewpoint); + /// @endcond } // end namespace sharp 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/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/__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) 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 1c3aeb3b..622af650 100644 --- a/src/nanobind/nwsspc/sharp/calc/params.pyi +++ b/src/nanobind/nwsspc/sharp/calc/params.pyi @@ -29,7 +29,51 @@ 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] + 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_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: + 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_wobus pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -58,7 +102,7 @@ def effective_inflow_layer(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure 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 +117,51 @@ 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_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 + + Returns + ------- + nwsspc.sharp.calc.layer.PressureLayer + The Effective Inflow Layer + """ + +@overload +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. + 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_cm1 pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -226,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: @@ -450,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 @@ -462,7 +575,37 @@ 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] + 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_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 + 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_wobus pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -492,7 +635,37 @@ 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_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) + """ + +@overload +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 + 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_cm1 pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -679,6 +852,112 @@ def fosberg_fire_index(temperature: Annotated[NDArray[numpy.float32], dict(shape Fosberg Fire-Weather Index """ +@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: + """ + 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_wobus + 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_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 + 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_wobus + 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_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: """ @@ -733,7 +1012,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_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 @@ -753,7 +1032,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_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 f769fada..d7bfae9e 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)""" @@ -67,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). @@ -162,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). @@ -179,6 +190,302 @@ class lifter_cm1: The virtual temperature of the parcel (K) """ +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: ... + + 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: + """ + 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 the given lifter + """ + +@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_cm1 + A lookup table for the given lifter + """ + +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_wobus + 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) + """ + +@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: ... + + 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) + 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 : nwsspc.sharp.calc.parcel.lut_data_cm1 + A shared lookup table constructed with a lifter_cm1 instance. + """ + + def __init__(self, data: lut_data_cm1) -> 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 LPL(enum.Enum): SFC = 1 """A Surface Based Parcel""" @@ -287,50 +594,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) - """ - 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 +628,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 +640,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 @@ -478,70 +741,230 @@ 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 - @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 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_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 + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + 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) + 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 """ - 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. + + @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: + """ + 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 + 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) + 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_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 + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + 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) + 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_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 + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + 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) + 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_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 + parcel lifter (lifter_wobus or lifter_cm1), find and return the most unstable parcel. + + Parameters + ---------- + 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) @@ -570,9 +993,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_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -593,7 +1016,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_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 @@ -601,9 +1024,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_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -619,11 +1042,12 @@ class Parcel: 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_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 @@ -631,9 +1055,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_cm1 Parcel lifting routine to use for moist ascent pressure : numpy.ndarray[dtype=float32] 1D NumPy array of profile pressure values (Pa) @@ -652,6 +1076,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 @@ -712,6 +1188,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')]: """ @@ -725,8 +1257,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) @@ -737,7 +1269,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 @@ -749,8 +1281,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) @@ -760,58 +1292,50 @@ class DowndraftParcel: 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]: + @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')]: """ - 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. + 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). - The results are stored in nwsspc.sharp.calc.parcel.DowndraftParcel.cape - and nwsspc.sharp.calc.parcel.DowndraftParcel.cinh via the provided parcel. + Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual + temperature correction is not used for downdraft parcels. 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) + 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 ------- - tuple[float, float] - (DCAPE, DCINH) + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) """ - @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: + @overload + 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')]: """ - Define a downdraft parcel. + 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). - 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. + Unlike nwsspc.sharp.calc.parcel.Parcel.lift_parcel, the virtual + temperature correction is not used for downdraft parcels. - Parameters + 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. + 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 + Returns ------- - nwsspc.sharp.calc.parcel.DowndraftParcel - Downdraft Parcel + numpy.ndarray[dtype=float32] + A 1D NumPy array of parcel temperature values (K) """ diff --git a/src/nanobind/nwsspc/sharp/calc/thermo.pyi b/src/nanobind/nwsspc/sharp/calc/thermo.pyi index 4676d1d5..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,39 @@ 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). + + 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 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) + """ + +@overload +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). @@ -766,7 +798,7 @@ 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_lut_wobus a parcel lifter (e.g. lifter_cm1 or lifter_wobus) pressure : float The ambient pressure (Pa) @@ -782,7 +814,7 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, temper """ @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_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). @@ -798,12 +830,12 @@ 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_lut_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) + 1D NumPy array of ambient temperatures (K) dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of ambient dewpoint temperatures (K) @@ -813,6 +845,38 @@ def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annotated[N 1D NumPy array of wetbulb temperatures (K) """ +@overload +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). + + 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) + """ + @overload def wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_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')]: """ @@ -830,12 +894,76 @@ 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) + + Returns + ------- + numpy.ndarray[dtype=float32] + 1D NumPy array of wetbulb temperatures (K) + """ + +@overload +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). + + 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_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) + """ + +@overload +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). + + 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_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 temperatures (K) dewpoint : numpy.ndarray[dtype=float32] 1D NumPy array of ambient dewpoint temperatures (K) @@ -854,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 ------- @@ -878,7 +1006,39 @@ 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). + + 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 : 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 (K) + + 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_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). @@ -886,22 +1046,22 @@ 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_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 ------- @@ -910,7 +1070,7 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_cm1, pressure: float, """ @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_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). @@ -918,22 +1078,22 @@ 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_lut_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 + 1D NumPy array of ambient dewpoint temperature (K) Returns ------- @@ -941,6 +1101,38 @@ def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_wobus, pressure: Annot 1D NumPy array of wet-bulb potential temperatures (K) """ +@overload +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). + + 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 : 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 (K) + + Returns + ------- + float + The wet-bulb potential temperature (K) + """ + @overload def theta_wetbulb(lifter: nwsspc.sharp.calc.parcel.lifter_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')]: """ @@ -950,22 +1142,86 @@ 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 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 + 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) + """ + +@overload +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). + + 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 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_lut_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 (K) + + Returns + ------- + float + The wet-bulb potential temperature (K) + """ + +@overload +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). + + 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 : 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 8c4576cd..36086976 100644 --- a/src/nanobind/params_bindings.h +++ b/src/nanobind/params_bindings.h @@ -12,126 +12,116 @@ #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 + nb::arg("mupcl") = nb::none(), doc.c_str()); +} -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. +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); -References ----------- -Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.pdf + 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); -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 + const std::size_t NZ = pres.size(); + auto pcl_vtmp = std::make_unique(NZ); + auto pcl_buoy = std::make_unique(NZ); -Returns -------- -nwsspc.sharp.calc.layer.PressureLayer - The Effective Inflow Layer - )pbdoc"); + 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()); - 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()]; + 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, doc.c_str()); +} - 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); +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); - delete[] pcl_vtmp; - delete[] pcl_buoy; + 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); - return eil; + 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); + + return pft; }, - 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(), + 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()); +} + +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"); + + 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. 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 @@ -146,7 +136,7 @@ Thompson et al. 2007: https://www.spc.noaa.gov/publications/thompson/effective.p Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 +lifter : {} pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -167,7 +157,12 @@ Returns ------- nwsspc.sharp.calc.layer.PressureLayer The Effective Inflow Layer - )pbdoc"); + )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", @@ -176,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, @@ -238,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); @@ -305,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"), @@ -358,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"), @@ -382,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, @@ -595,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, @@ -641,90 +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; - - 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_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) -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 +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()); - - delete[] pcl_vtmp; - delete[] pcl_buoy; +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 @@ -733,7 +638,7 @@ threshold. The first guess is the current surface temperature. Parameters ---------- -lifter : nwsspc.sharp.calc.parcel.lifter_cm1 +lifter : {} pressure : numpy.ndarray[dtype=float32] A 1D NumPy array of pressure values (Pa) height : numpy.ndarray[dtype=float32] @@ -751,16 +656,19 @@ Returns ------- float The convective temperature (K) -)pbdoc"); +)pbdoc"; + + 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); + }); 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!"); - } + check_equal_sizes(pres, mixr); return sharp::precipitable_water(layer, pres.data(), mixr.data(), pres.size()); }, @@ -788,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()); }, @@ -818,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()); }, @@ -899,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( @@ -959,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( @@ -997,42 +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, + 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 @@ -1052,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_cm1 +lifter : {} mix_layer : nwsspc.sharp.calc.layer.PressureLayer A mixing layer to take average values of the environment from pressure : numpy.ndarray[dtype=float32] @@ -1083,95 +939,12 @@ 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, - 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_wobus -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"; -)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 db72fb2c..9026dae7 100644 --- a/src/nanobind/parcel_bindings.h +++ b/src/nanobind/parcel_bindings.h @@ -4,22 +4,451 @@ // clang-format off #include #include +#include // 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) + .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 +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. @@ -43,6 +472,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) @@ -72,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( @@ -149,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( @@ -168,6 +622,31 @@ float The virtual temperature of the parcel (K) )pbdoc"); + 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; + + 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); + + bind_lut_data(m_parcel, lut_data_template_doc, lifter_fqn, + lut_data_fqn); + + 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); + + 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") .value("FCST", sharp::LPL::FCST, "A Forecast Surface Parcel") @@ -175,43 +654,44 @@ float .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 @@ -225,87 +705,17 @@ 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 size_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); - }, - 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( - "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. @@ -333,24 +743,19 @@ 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( + .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 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 +767,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 @@ -385,21 +790,17 @@ 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( + .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 @@ -428,22 +829,18 @@ 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( + .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 @@ -465,9 +862,9 @@ 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( + .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. @@ -484,342 +881,53 @@ 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 - )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. - -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) - -Returns -------- -nwsspc.sharp.calc.parcel.Parcel - Parcel with most-unstable values - )pbdoc"); - - nb::class_(m_parcel, "DowndraftParcel", R"pbdoc( + )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 @@ -831,91 +939,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( - "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. @@ -937,26 +971,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. @@ -985,6 +1015,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 0e149384..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,365 +1166,17 @@ 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. - -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 : 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_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_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( - "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_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"); + 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); + }); m_therm.def("thetae", &sharp::thetae, nb::arg("pressure"), nb::arg("temperature"), nb::arg("dewpoint"), @@ -1456,20 +1212,17 @@ float "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( @@ -1504,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()); }, @@ -1532,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()); }, @@ -1561,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(), @@ -1597,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(), @@ -1653,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()); - - nb::capsule owner(buoy_arr, - [](void *p) noexcept { delete[] (float *)p; }); + check_equal_sizes(pcl_tmpk, env_tmpk); + const std::size_t NZ = pcl_tmpk.size(); - 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( @@ -1684,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; }, @@ -1723,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_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_parcel.py b/tests/python/test_parcel.py index eb0084cf..54e3406e 100644 --- a/tests/python/test_parcel.py +++ b/tests/python/test_parcel.py @@ -82,19 +82,62 @@ 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) + + 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, abs=1e-1)) + +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, abs=1e-1)) def test_surface_parcel(): pres = snd_data["pres"][0] @@ -119,6 +162,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"]) @@ -133,6 +177,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( @@ -173,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( 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_parcel.cpp b/tests/unit/test_parcel.cpp index 5b11546e..37540647 100644 --- a/tests/unit/test_parcel.cpp +++ b/tests/unit/test_parcel.cpp @@ -1,9 +1,9 @@ -#include "SHARPlib/thermo.h" #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include #include +#include #include #include 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") {