From ff0780ad7648f5b00b8fc70d65c69c251e688082 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 11:08:57 -0600 Subject: [PATCH 001/770] first attempt at a table #1049 --- docs/Users_Guide/statistics_list.rst | 203 +++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 docs/Users_Guide/statistics_list.rst diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst new file mode 100644 index 0000000000..8433c7bf9c --- /dev/null +++ b/docs/Users_Guide/statistics_list.rst @@ -0,0 +1,203 @@ +********************************** +METplus Laundry List of Statistics +********************************** + +.. statistics_list:: + + First attempt. This is only for the "A" items + + .. list-table:: Laundry list A + :widths: auto + :header-rows: 1 + + * - Type + - Statistics + - References + * - 2D Objects +" - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, + For paired objects: Distance between two objects centroids, Minimum distance between the boundaries of two objects, Minimum distance between the convex hulls of two objects, Difference between the axis angles of two objects, Ratio of the areas of two objects, Intersection area of two objects, Union area of two objects, Symmetric difference of two objects, Ratio of intersection areas, Ratio of complexities, Ratio of the nth percentile of intensity, Total interest value computed for a pair of simple objects, NetCDF files with the objects and raw data for further processing" + - See the WWRP/WGNE JWGFVR website for more details: https://www.cawcr.gov.au/projects/verification + * - A/BAL_WIND_34 + - TCMPR output format: a/bdeck 34-knot radius winds in full circle + - TC-Pairs + * - A/BAL_WIND_50 + - TCMPR output format: a/bdeck 50-knot radius winds in full circle + - TC-Pairs + * - A/BAL_WIND_64 + - TCMPR output format: a/bdeck 64-knot radius winds in full circle + - TC-Pairs + * - A/BDEPTH + - TCMPR output format: system depth, D-deep, M-medium, S-shallow, X-unknown + - TC-Pairs + * - A/BDIR + - TCMPR output format: storm direction in compass coordinates, 0 - 359 degrees + - TC-Pairs + * - A/BEYE + - TCMPR output format: eye diameter, 0 through 999 nm + - TC-Pairs + * - A/BGUSTS + - TCMPR output format: gusts, 0 through 995 kts + - TC-Pairs + * - A/BMRD + - TCMPR output format: radius of max winds, 0 - 999 nm + - TC-Pairs + * - A/BNE_WIND_34 + - TCMPR output format: a/bdeck 34-knot radius winds in NE quadrant + - TC-Pairs + * - A/BNE_WIND_50 + - TCMPR output format: a/bdeck 50-knot radius winds in NE quadrant + - TC-Pairs + * - A/BNE_WIND_64 + - TCMPR output format: a/bdeck 64-knot radius winds in NE quadrant + - TC-Pairs + * - A/BNW_WIND_34 + - TCMPR output format: a/bdeck 34-knot radius winds in NW quadrant + - TC-Pairs + * - A/BNW_WIND_50 + - TCMPR output format: a/bdeck 50-knot radius winds in NW quadrant + - TC-Pairs + * - A/BNW_WIND_64 + - TCMPR output format: a/bdeck 64-knot radius winds in NW quadrant + - TC-Pairs + * - A/BRADP + - TCMPR output format: pressure in millibars of the last closed isobar, 900 - 1050 mb + - TC-Pairs + * - A/BRRP + - TCMPR output format: radius of the last closed isobar in nm, 0 - 9999 nm + - TC-Pairs + * - A/BSE_WIND_34 + - TCMPR output format: a/bdeck 34-knot radius winds in SE quadrant + - TC-Pairs + * - A/BSE_WIND_50 + - TCMPR output format: a/bdeck 50-knot radius winds in SE quadrant + - TC-Pairs + * - A/BSE_WIND_64 + - TCMPR output format: a/bdeck 64-knot radius winds in SE quadrant + - TC-Pairs + * - A/BSPEED + - TCMPR output format: storm speed, 0 - 999 kts + - TC-Pairs + * - A/BSW_WIND_34 + - TCMPR output format: a/bdeck 34-knot radius winds in SW quadrant + - TC-Pairs + * - A/BSW_WIND_50 + - TCMPR output format: a/bdeck 50-knot radius winds in SW quadrant + - TC-Pairs + * - A/BSW_WIND_64 + - TCMPR output format: a/bdeck 64-knot radius winds in SW quadrant + - TC-Pairs + * - ACC +" - MODE output format: Accuracy +CTS output format: Accuracy including normal and bootstrap upper and lower confidence limits +MCTS output format: Accuracy, normal confidence limits and bootstrap confidence limits +NBRCTCS output format: Accuracy including normal and bootstrap upper and lower confidence limits" + - MODE-Tool, Point-Stat Tool & Grid-Stat Tool +" * - ACC, +ACC_NCL, +ACC_NCU, +ACC_BCL, +ACC_BCU" +" - CTS output format: Accuracy including normal and bootstrap upper and lower confidence limits +MCTS output format: Accuracy, normal confidence limits and bootstrap confidence limits +NBRCTCS output format: Accuracy including normal and bootstrap upper and lower confidence limits" + - Point-Stat Tool & Grid-Stat Tool + * - ADLAND +" - TCMPR output format: adeck distance to land (nm) +PROBRIRW output format: adeck distance to land (nm)" + - TC-Pairs +" * - AFSS, +AFSS_BCL, +AFSS_BCU" + - NBRCNT output format: Asymptotic Fractions Skill Score including bootstrap upper and lower confidence limits + - Grid-Stat Tool + * - AGEN_DLAND + - GENMPR output format: Forecast genesis event distance to land (nm) + - TC-Gen + * - AGEN_FHR + - GENMPR output format: Forecast hour of genesis event + - TC-Gen + * - AGEN_INIT + - GENMPR output format: Forecast initialization time + - TC-Gen + * - AGEN_LAT + - GENMPR output format: Latitude position of the forecast genesis event + - TC-Gen + * - AGEN_LON + - GENMPR output format: Longitude position of the forecast genesis event + - TC-Gen + * - ALAT +" - TCMPR output format: Latitude position of adeck model +PROBRIRW output format: Latitude position of edeck model" + - TC-Pairs + * - ALON +" - TCMPR output format: Longitude position of adeck model +PROBRIRW output format: Longitude position of edeck model" + - TC-Pairs + * - ALPHA +" - Point-Stat output: Error percent value used in confidence intervals +grid-stat output format: Error percent value used in confidence intervals +wavelet-stat output format: NA in Wavelet-Stat +TC-Gen output format: Error percent value used in confidence intervals" +" - Point-Stat Tool +Grid-Stat Tool +Wavelet-Stat Tool +TC-Gen" + * - ALTK_ERR + - TCMPR output format: Along track error (nm) + - TC-Pairs + * - AMAX_WIND + - TCMPR output format: adeck maximum wind speed + - TC-Pairs + * - AMODEL + - TCST output format: User provided text string designating model name + - TC-Pairs + * - AMSLP + - TCMPR output format: adeck mean sea level pressure + - TC-Pairs + * - ANGLE_DIFF + - MODE ascii object: Difference between the axis angles of two objects (in degrees) + - MODE-Tool + * - ANLY_USE + - GSI diagnostic conventional MPR output: Analysis usage (1 for yes, -1 for no) + - GSI-Tool +" * - ANOM_CORR_UNCNTR, +ANOM_CORR_UNCNTR_BCL, +ANOM_CORR_UNCNTR_BCU" + - CNT output format: The uncentered Anomaly Correlation excluding mean error including bootstrap upper and lower confidence limits + - Point-Stat Tool +" * - ANOM_CORR, +ANOM_CORR_NCL, +ANOM_CORR_NCU, +ANOM_CORR_BCL, +ANOM_CORR_BCU" + - CNT output format: The Anomaly Correlation including mean error with normal and bootstrap upper and lower confidence limits + - Point-Stat Tool + * - AREA +" - MODE ascii object: Object area (in grid squares) +MODE-time-domain 2D attribute output: 2D cross-sectional area" +" - MODE-Tool +MODE-time-domain" + * - AREA_RATIO +" - MODE ascii object: The forecast object area divided by the observation object area (unitless) +NOTE: Prior to met-10.0.0, defined as the lesser of the two object areas divided by the greater of the two" + - MODE-Tool + * - AREA_THRESH + - MODE ascii object: Area of the object containing data values in the raw field that meet the object definition threshold criteria (in grid squares) + - MODE-Tool + * - ASPECT_DIFF + - MODE ascii object: Absolute value of the difference between the aspect ratios of two objects (unitless) + - MODE-Tool + * - AWIND_END + - PROBRIRW output format: Forecast maximum wind speed at RI end + - TC-Pairs + * - AXIS_ANG +" - MODE ascii object: Object axis angle (in degrees) +MODE-time-domain 2D attribute output: Angle that the axis makes with the grid x direction +MODE-time-domain 3D attribute output: Angle that the axis plane of an object makes with the grid x direction" +" - MODE-Tool +MODE-time-domain" + * - AXIS_DIFF + - MODE-time-domain 3D pair attribute output: Difference in spatial axis plane angles + - MODE-time-domain + + From a8dcdd0c757172202571a094e028ffa4efc6c6e1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 11:29:07 -0600 Subject: [PATCH 002/770] 2nd attempt, smaller, different formatting table #1049 --- docs/Users_Guide/statistics_list.rst | 188 +-------------------------- 1 file changed, 6 insertions(+), 182 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8433c7bf9c..3ec27a739f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1,10 +1,10 @@ -********************************** +.. _statistics_list: + METplus Laundry List of Statistics -********************************** +================================== -.. statistics_list:: - First attempt. This is only for the "A" items + 2nd attempt. This is only for the "A" items .. list-table:: Laundry list A :widths: auto @@ -14,9 +14,8 @@ METplus Laundry List of Statistics - Statistics - References * - 2D Objects -" - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - For paired objects: Distance between two objects centroids, Minimum distance between the boundaries of two objects, Minimum distance between the convex hulls of two objects, Difference between the axis angles of two objects, Ratio of the areas of two objects, Intersection area of two objects, Union area of two objects, Symmetric difference of two objects, Ratio of intersection areas, Ratio of complexities, Ratio of the nth percentile of intensity, Total interest value computed for a pair of simple objects, NetCDF files with the objects and raw data for further processing" - - See the WWRP/WGNE JWGFVR website for more details: https://www.cawcr.gov.au/projects/verification + - For each object: + - See the WWRP/WGNE JWGFVR website * - A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle - TC-Pairs @@ -26,178 +25,3 @@ METplus Laundry List of Statistics * - A/BAL_WIND_64 - TCMPR output format: a/bdeck 64-knot radius winds in full circle - TC-Pairs - * - A/BDEPTH - - TCMPR output format: system depth, D-deep, M-medium, S-shallow, X-unknown - - TC-Pairs - * - A/BDIR - - TCMPR output format: storm direction in compass coordinates, 0 - 359 degrees - - TC-Pairs - * - A/BEYE - - TCMPR output format: eye diameter, 0 through 999 nm - - TC-Pairs - * - A/BGUSTS - - TCMPR output format: gusts, 0 through 995 kts - - TC-Pairs - * - A/BMRD - - TCMPR output format: radius of max winds, 0 - 999 nm - - TC-Pairs - * - A/BNE_WIND_34 - - TCMPR output format: a/bdeck 34-knot radius winds in NE quadrant - - TC-Pairs - * - A/BNE_WIND_50 - - TCMPR output format: a/bdeck 50-knot radius winds in NE quadrant - - TC-Pairs - * - A/BNE_WIND_64 - - TCMPR output format: a/bdeck 64-knot radius winds in NE quadrant - - TC-Pairs - * - A/BNW_WIND_34 - - TCMPR output format: a/bdeck 34-knot radius winds in NW quadrant - - TC-Pairs - * - A/BNW_WIND_50 - - TCMPR output format: a/bdeck 50-knot radius winds in NW quadrant - - TC-Pairs - * - A/BNW_WIND_64 - - TCMPR output format: a/bdeck 64-knot radius winds in NW quadrant - - TC-Pairs - * - A/BRADP - - TCMPR output format: pressure in millibars of the last closed isobar, 900 - 1050 mb - - TC-Pairs - * - A/BRRP - - TCMPR output format: radius of the last closed isobar in nm, 0 - 9999 nm - - TC-Pairs - * - A/BSE_WIND_34 - - TCMPR output format: a/bdeck 34-knot radius winds in SE quadrant - - TC-Pairs - * - A/BSE_WIND_50 - - TCMPR output format: a/bdeck 50-knot radius winds in SE quadrant - - TC-Pairs - * - A/BSE_WIND_64 - - TCMPR output format: a/bdeck 64-knot radius winds in SE quadrant - - TC-Pairs - * - A/BSPEED - - TCMPR output format: storm speed, 0 - 999 kts - - TC-Pairs - * - A/BSW_WIND_34 - - TCMPR output format: a/bdeck 34-knot radius winds in SW quadrant - - TC-Pairs - * - A/BSW_WIND_50 - - TCMPR output format: a/bdeck 50-knot radius winds in SW quadrant - - TC-Pairs - * - A/BSW_WIND_64 - - TCMPR output format: a/bdeck 64-knot radius winds in SW quadrant - - TC-Pairs - * - ACC -" - MODE output format: Accuracy -CTS output format: Accuracy including normal and bootstrap upper and lower confidence limits -MCTS output format: Accuracy, normal confidence limits and bootstrap confidence limits -NBRCTCS output format: Accuracy including normal and bootstrap upper and lower confidence limits" - - MODE-Tool, Point-Stat Tool & Grid-Stat Tool -" * - ACC, -ACC_NCL, -ACC_NCU, -ACC_BCL, -ACC_BCU" -" - CTS output format: Accuracy including normal and bootstrap upper and lower confidence limits -MCTS output format: Accuracy, normal confidence limits and bootstrap confidence limits -NBRCTCS output format: Accuracy including normal and bootstrap upper and lower confidence limits" - - Point-Stat Tool & Grid-Stat Tool - * - ADLAND -" - TCMPR output format: adeck distance to land (nm) -PROBRIRW output format: adeck distance to land (nm)" - - TC-Pairs -" * - AFSS, -AFSS_BCL, -AFSS_BCU" - - NBRCNT output format: Asymptotic Fractions Skill Score including bootstrap upper and lower confidence limits - - Grid-Stat Tool - * - AGEN_DLAND - - GENMPR output format: Forecast genesis event distance to land (nm) - - TC-Gen - * - AGEN_FHR - - GENMPR output format: Forecast hour of genesis event - - TC-Gen - * - AGEN_INIT - - GENMPR output format: Forecast initialization time - - TC-Gen - * - AGEN_LAT - - GENMPR output format: Latitude position of the forecast genesis event - - TC-Gen - * - AGEN_LON - - GENMPR output format: Longitude position of the forecast genesis event - - TC-Gen - * - ALAT -" - TCMPR output format: Latitude position of adeck model -PROBRIRW output format: Latitude position of edeck model" - - TC-Pairs - * - ALON -" - TCMPR output format: Longitude position of adeck model -PROBRIRW output format: Longitude position of edeck model" - - TC-Pairs - * - ALPHA -" - Point-Stat output: Error percent value used in confidence intervals -grid-stat output format: Error percent value used in confidence intervals -wavelet-stat output format: NA in Wavelet-Stat -TC-Gen output format: Error percent value used in confidence intervals" -" - Point-Stat Tool -Grid-Stat Tool -Wavelet-Stat Tool -TC-Gen" - * - ALTK_ERR - - TCMPR output format: Along track error (nm) - - TC-Pairs - * - AMAX_WIND - - TCMPR output format: adeck maximum wind speed - - TC-Pairs - * - AMODEL - - TCST output format: User provided text string designating model name - - TC-Pairs - * - AMSLP - - TCMPR output format: adeck mean sea level pressure - - TC-Pairs - * - ANGLE_DIFF - - MODE ascii object: Difference between the axis angles of two objects (in degrees) - - MODE-Tool - * - ANLY_USE - - GSI diagnostic conventional MPR output: Analysis usage (1 for yes, -1 for no) - - GSI-Tool -" * - ANOM_CORR_UNCNTR, -ANOM_CORR_UNCNTR_BCL, -ANOM_CORR_UNCNTR_BCU" - - CNT output format: The uncentered Anomaly Correlation excluding mean error including bootstrap upper and lower confidence limits - - Point-Stat Tool -" * - ANOM_CORR, -ANOM_CORR_NCL, -ANOM_CORR_NCU, -ANOM_CORR_BCL, -ANOM_CORR_BCU" - - CNT output format: The Anomaly Correlation including mean error with normal and bootstrap upper and lower confidence limits - - Point-Stat Tool - * - AREA -" - MODE ascii object: Object area (in grid squares) -MODE-time-domain 2D attribute output: 2D cross-sectional area" -" - MODE-Tool -MODE-time-domain" - * - AREA_RATIO -" - MODE ascii object: The forecast object area divided by the observation object area (unitless) -NOTE: Prior to met-10.0.0, defined as the lesser of the two object areas divided by the greater of the two" - - MODE-Tool - * - AREA_THRESH - - MODE ascii object: Area of the object containing data values in the raw field that meet the object definition threshold criteria (in grid squares) - - MODE-Tool - * - ASPECT_DIFF - - MODE ascii object: Absolute value of the difference between the aspect ratios of two objects (unitless) - - MODE-Tool - * - AWIND_END - - PROBRIRW output format: Forecast maximum wind speed at RI end - - TC-Pairs - * - AXIS_ANG -" - MODE ascii object: Object axis angle (in degrees) -MODE-time-domain 2D attribute output: Angle that the axis makes with the grid x direction -MODE-time-domain 3D attribute output: Angle that the axis plane of an object makes with the grid x direction" -" - MODE-Tool -MODE-time-domain" - * - AXIS_DIFF - - MODE-time-domain 3D pair attribute output: Difference in spatial axis plane angles - - MODE-time-domain - - From 181ad8b1fa4735a4ddfb19da81b4f23aea25cee6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 11:49:23 -0600 Subject: [PATCH 003/770] trying to get rid of git error message by adding this to the index #1049 --- docs/Users_Guide/index.rst | 1 + docs/Users_Guide/statistics_list.rst | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/index.rst b/docs/Users_Guide/index.rst index acd989ed11..a1ab31578a 100644 --- a/docs/Users_Guide/index.rst +++ b/docs/Users_Guide/index.rst @@ -85,6 +85,7 @@ is sponsored by NSF. quicksearch glossary references + statistics_list .. Indices and tables diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 3ec27a739f..ca4a434aec 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1,7 +1,6 @@ -.. _statistics_list: - +********************************** METplus Laundry List of Statistics -================================== +********************************** 2nd attempt. This is only for the "A" items From 2e035e0aff8a9759498fb830c15ce994e3394cd4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 12:00:54 -0600 Subject: [PATCH 004/770] alignment #1049 --- docs/Users_Guide/statistics_list.rst | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ca4a434aec..268406a6d3 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -5,22 +5,22 @@ METplus Laundry List of Statistics 2nd attempt. This is only for the "A" items - .. list-table:: Laundry list A + .. list-table:: Laundry list A. :widths: auto :header-rows: 1 - * - Type - - Statistics - - References - * - 2D Objects - - For each object: - - See the WWRP/WGNE JWGFVR website - * - A/BAL_WIND_34 - - TCMPR output format: a/bdeck 34-knot radius winds in full circle - - TC-Pairs - * - A/BAL_WIND_50 - - TCMPR output format: a/bdeck 50-knot radius winds in full circle - - TC-Pairs - * - A/BAL_WIND_64 - - TCMPR output format: a/bdeck 64-knot radius winds in full circle - - TC-Pairs + * - Type + - Statistics + - References + * - 2D Objects + - For each object: + - See the WWRP/WGNE JWGFVR website + * - A/BAL_WIND_34 + - TCMPR output format: a/bdeck 34-knot radius winds in full circle + - TC-Pairs + * - A/BAL_WIND_50 + - TCMPR output format: a/bdeck 50-knot radius winds in full circle + - TC-Pairs + * - A/BAL_WIND_64 + - TCMPR output format: a/bdeck 64-knot radius winds in full circle + - TC-Pairs From ab74c06dcbef441a15bc35797fc21351c55925cf Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 12:58:08 -0600 Subject: [PATCH 005/770] trying to text wrap in a table #1049 --- docs/Users_Guide/statistics_list.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 268406a6d3..4fe2907831 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,8 +3,11 @@ METplus Laundry List of Statistics ********************************** - 2nd attempt. This is only for the "A" items + 3nd attempt. text wrapping This is only for the "A" items +.. role:: raw-html(raw) + :format: html + .. list-table:: Laundry list A. :widths: auto :header-rows: 1 @@ -13,7 +16,7 @@ METplus Laundry List of Statistics - Statistics - References * - 2D Objects - - For each object: + - For each object: :raw-html:`
` Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - See the WWRP/WGNE JWGFVR website * - A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle From eb646db09b0a1edeb18cb4ee770ea42be90acb22 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 13:29:48 -0600 Subject: [PATCH 006/770] attempting to create text wrapping in tables #1049 --- docs/_templates/theme_overrides.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/_templates/theme_overrides.css diff --git a/docs/_templates/theme_overrides.css b/docs/_templates/theme_overrides.css new file mode 100644 index 0000000000..ab16bba14c --- /dev/null +++ b/docs/_templates/theme_overrides.css @@ -0,0 +1,16 @@ +/* Fix missing line-wrapping with Sphinx-RTD theme */ +/* https://github.com/platformio/platformio-docs/issues/5 */ + +/* override table width restrictions */ +@media screen and (min-width: 767px) { + + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + } +} \ No newline at end of file From 8f4bb5b5d0b7a7c318f9b07318aa90c83ea7173f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 13:46:57 -0600 Subject: [PATCH 007/770] removing role, trying to wrap text #1049 --- docs/Users_Guide/statistics_list.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4fe2907831..1c859b888a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,10 +3,7 @@ METplus Laundry List of Statistics ********************************** - 3nd attempt. text wrapping This is only for the "A" items - -.. role:: raw-html(raw) - :format: html + 5th attempt. text wrapping This is only for the "A" items .. list-table:: Laundry list A. :widths: auto From 9fd599b7f072cf56d99d1a8bbc19052a972b243b Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 13:51:30 -0600 Subject: [PATCH 008/770] removing html break, trying to wrap text #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 1c859b888a..ced6389b34 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -13,7 +13,7 @@ METplus Laundry List of Statistics - Statistics - References * - 2D Objects - - For each object: :raw-html:`
` Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, + - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - See the WWRP/WGNE JWGFVR website * - A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle From 5737f7abc8943cdc8da51e0acf691112fd36f85c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 14:06:37 -0600 Subject: [PATCH 009/770] adding code to wrap text #1049 --- docs/Users_Guide/statistics_list.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ced6389b34..ae8796da04 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,8 +3,13 @@ METplus Laundry List of Statistics ********************************** - 5th attempt. text wrapping This is only for the "A" items - + 6th attempt. text wrapping This is only for the "A" items + +/* override table no-wrap */ +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + .. list-table:: Laundry list A. :widths: auto :header-rows: 1 From e53ef157177824eb20d59d4fae0e83f7af97eaf6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 14:13:56 -0600 Subject: [PATCH 010/770] removing code to wrap text #1049 --- docs/Users_Guide/statistics_list.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ae8796da04..7903adf332 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,12 +3,7 @@ METplus Laundry List of Statistics ********************************** - 6th attempt. text wrapping This is only for the "A" items - -/* override table no-wrap */ -.wy-table-responsive table td, .wy-table-responsive table th { - white-space: normal; -} + 7th attempt. text wrapping This is only for the "A" items .. list-table:: Laundry list A. :widths: auto From ea2b3d6c509777ae03c80654e8c299522ba248ca Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 14:35:54 -0600 Subject: [PATCH 011/770] fixing naming typo #1049 --- docs/_templates/theme_override.css | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/_templates/theme_override.css diff --git a/docs/_templates/theme_override.css b/docs/_templates/theme_override.css new file mode 100644 index 0000000000..ab16bba14c --- /dev/null +++ b/docs/_templates/theme_override.css @@ -0,0 +1,16 @@ +/* Fix missing line-wrapping with Sphinx-RTD theme */ +/* https://github.com/platformio/platformio-docs/issues/5 */ + +/* override table width restrictions */ +@media screen and (min-width: 767px) { + + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + } +} \ No newline at end of file From e9ff6bc717abd00f899268d9017c129eb8006502 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 14:45:51 -0600 Subject: [PATCH 012/770] hitting returns in table #1049 --- docs/Users_Guide/statistics_list.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 7903adf332..65a4665313 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 7th attempt. text wrapping This is only for the "A" items + 8th attempt. text wrapping This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -13,7 +13,10 @@ METplus Laundry List of Statistics - Statistics - References * - 2D Objects - - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, + - For each object: Location of the centroid in grid units, +Location of the centroid in lat/lon degrees, Axis angle, Length of the +enclosing rectangle, Width of the enclosing rectangle, Object area, +Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - See the WWRP/WGNE JWGFVR website * - A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle From 85da3e630698a0c8f820aaa71a0efe9ecd642450 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 15:18:31 -0600 Subject: [PATCH 013/770] removing typo file, removing line breaks #1049 --- docs/Users_Guide/statistics_list.rst | 7 ++----- docs/_templates/theme_overrides.css | 16 ---------------- 2 files changed, 2 insertions(+), 21 deletions(-) delete mode 100644 docs/_templates/theme_overrides.css diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 65a4665313..696020e529 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 8th attempt. text wrapping This is only for the "A" items + 9th attempt. text wrapping This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -13,10 +13,7 @@ METplus Laundry List of Statistics - Statistics - References * - 2D Objects - - For each object: Location of the centroid in grid units, -Location of the centroid in lat/lon degrees, Axis angle, Length of the -enclosing rectangle, Width of the enclosing rectangle, Object area, -Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, + - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - See the WWRP/WGNE JWGFVR website * - A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle diff --git a/docs/_templates/theme_overrides.css b/docs/_templates/theme_overrides.css deleted file mode 100644 index ab16bba14c..0000000000 --- a/docs/_templates/theme_overrides.css +++ /dev/null @@ -1,16 +0,0 @@ -/* Fix missing line-wrapping with Sphinx-RTD theme */ -/* https://github.com/platformio/platformio-docs/issues/5 */ - -/* override table width restrictions */ -@media screen and (min-width: 767px) { - - .wy-table-responsive table td { - /* !important prevents the common CSS stylesheets from overriding - this as on RTD they are loaded after this stylesheet */ - white-space: normal !important; - } - - .wy-table-responsive { - overflow: visible !important; - } -} \ No newline at end of file From 263b213888cba8f09a92f4bd0539094effa9b2fd Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 15:44:12 -0600 Subject: [PATCH 014/770] attempting a simple table for text wrapping #1049 --- docs/Users_Guide/statistics_list.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 696020e529..7093bc1b9d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 9th attempt. text wrapping This is only for the "A" items + 10th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -24,3 +24,17 @@ METplus Laundry List of Statistics * - A/BAL_WIND_64 - TCMPR output format: a/bdeck 64-knot radius winds in full circle - TC-Pairs + +============== ======================= ================================== +Type Statistics References +-------------- ----------------------- ---------------------------------- +2D Objects For each object: See the WWRP/WGNE JWGFVR website + object: + Location of the + centroid in grid units + Location +-------------- ----------------------- ---------------------------------- +A/BAL_WIND_34 TCMPR output format: TC-Pairs + a/bdeck 34-knot radius + winds in full circle +============== ======================= ================================== From c64b2aaa783e096313cfcffa3fbd9548c07791c2 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 15:58:17 -0600 Subject: [PATCH 015/770] attempting to bold header row in simple table #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 7093bc1b9d..5e2d6bfcd6 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 10th attempt. text wrapping NEW table This is only for the "A" items + 11th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -27,7 +27,7 @@ METplus Laundry List of Statistics ============== ======================= ================================== Type Statistics References --------------- ----------------------- ---------------------------------- +============== ======================= ================================== 2D Objects For each object: See the WWRP/WGNE JWGFVR website object: Location of the From 51b15b637c760611a62e7b32b8d08baf7b120c23 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 16:18:49 -0600 Subject: [PATCH 016/770] simple table line break attempt #1049 --- docs/Users_Guide/statistics_list.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 5e2d6bfcd6..7f48ae1226 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 11th attempt. text wrapping NEW table This is only for the "A" items + 12th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -29,9 +29,8 @@ METplus Laundry List of Statistics Type Statistics References ============== ======================= ================================== 2D Objects For each object: See the WWRP/WGNE JWGFVR website - object: - Location of the - centroid in grid units + | Location of the + | centroid in grid units Location -------------- ----------------------- ---------------------------------- A/BAL_WIND_34 TCMPR output format: TC-Pairs From eba3e89f867e8e694fc870f0d16b508684f8c42c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 16:25:02 -0600 Subject: [PATCH 017/770] simple table line break attempt #2 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 7f48ae1226..cb392f3759 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -30,8 +30,8 @@ Type Statistics References ============== ======================= ================================== 2D Objects For each object: See the WWRP/WGNE JWGFVR website | Location of the - | centroid in grid units - Location + | centroid in grid + units Location -------------- ----------------------- ---------------------------------- A/BAL_WIND_34 TCMPR output format: TC-Pairs a/bdeck 34-knot radius From 0162415d62de0cf528e6e1ccecb989cc8d10d3f4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 5 Aug 2021 16:31:15 -0600 Subject: [PATCH 018/770] simple table line break attempt #3 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index cb392f3759..912aad1a34 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 12th attempt. text wrapping NEW table This is only for the "A" items + 13th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -31,7 +31,7 @@ Type Statistics References 2D Objects For each object: See the WWRP/WGNE JWGFVR website | Location of the | centroid in grid - units Location + | units Location -------------- ----------------------- ---------------------------------- A/BAL_WIND_34 TCMPR output format: TC-Pairs a/bdeck 34-knot radius From e9db8be29e2da4bf853b7e64ef38ab58c0a99684 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 10:05:47 -0600 Subject: [PATCH 019/770] trying another | #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 912aad1a34..4c3242759d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 13th attempt. text wrapping NEW table This is only for the "A" items + 14th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -28,7 +28,7 @@ METplus Laundry List of Statistics ============== ======================= ================================== Type Statistics References ============== ======================= ================================== -2D Objects For each object: See the WWRP/WGNE JWGFVR website +2D Objects | For each object: See the WWRP/WGNE JWGFVR website | Location of the | centroid in grid | units Location From a24adc55111e129b55af3ca33f93572d85110542 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 10:12:33 -0600 Subject: [PATCH 020/770] 2 trying another | #1049 --- docs/Users_Guide/statistics_list.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4c3242759d..d99f3e1bef 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 14th attempt. text wrapping NEW table This is only for the "A" items + 15th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -29,11 +29,12 @@ METplus Laundry List of Statistics Type Statistics References ============== ======================= ================================== 2D Objects | For each object: See the WWRP/WGNE JWGFVR website - | Location of the + Location of the | centroid in grid - | units Location + units Location -------------- ----------------------- ---------------------------------- -A/BAL_WIND_34 TCMPR output format: TC-Pairs - a/bdeck 34-knot radius +A/BAL_WIND_34 | TCMPR output format: TC-Pairs + a/bdeck + | 34-knot radius winds in full circle ============== ======================= ================================== From a4ee34503e655f85e1563cd1b207d9f36816f24f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 10:18:19 -0600 Subject: [PATCH 021/770] 3 trying another | #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index d99f3e1bef..d9ebc74446 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 15th attempt. text wrapping NEW table This is only for the "A" items + 16th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -36,5 +36,6 @@ Type Statistics References A/BAL_WIND_34 | TCMPR output format: TC-Pairs a/bdeck | 34-knot radius + winds in full circle ============== ======================= ================================== From 4e8f33a8228ce5b616c90d98d2b7e3cbb839de41 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 10:35:39 -0600 Subject: [PATCH 022/770] trying a blank line #1049 --- docs/Users_Guide/statistics_list.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index d9ebc74446..31441ae9ac 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 16th attempt. text wrapping NEW table This is only for the "A" items + 17th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -30,12 +30,13 @@ Type Statistics References ============== ======================= ================================== 2D Objects | For each object: See the WWRP/WGNE JWGFVR website Location of the + | centroid in grid units Location -------------- ----------------------- ---------------------------------- A/BAL_WIND_34 | TCMPR output format: TC-Pairs a/bdeck - | 34-knot radius + | 34-knot radius winds in full circle ============== ======================= ================================== From c5f277873ebe51f4165911f55f8e14c37a4350bc Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 10:43:21 -0600 Subject: [PATCH 023/770] trying reformating grid #1049 --- docs/Users_Guide/statistics_list.rst | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 31441ae9ac..e65167f13e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -25,18 +25,14 @@ METplus Laundry List of Statistics - TCMPR output format: a/bdeck 64-knot radius winds in full circle - TC-Pairs -============== ======================= ================================== -Type Statistics References -============== ======================= ================================== -2D Objects | For each object: See the WWRP/WGNE JWGFVR website - Location of the - - | centroid in grid - units Location --------------- ----------------------- ---------------------------------- -A/BAL_WIND_34 | TCMPR output format: TC-Pairs - a/bdeck - - | 34-knot radius - winds in full circle -============== ======================= ================================== +============== ========================== ================================== +Type Statistics References +============== ========================== ================================== +2D Objects | For each object: See the WWRP/WGNE JWGFVR website + | Location of the centroid + | in grid units Location +-------------- -------------------------- ---------------------------------- +A/BAL_WIND_34 | TCMPR output format: TC-Pairs + | a/bdeck 34-knot radius + | winds in full circle +============== ========================== ================================== From c0620996f30ad2c0986225f804985776d5beb4bd Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 10:56:52 -0600 Subject: [PATCH 024/770] 2 trying reformating grid #1049 --- docs/Users_Guide/statistics_list.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e65167f13e..40ffacfd46 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -25,14 +25,14 @@ METplus Laundry List of Statistics - TCMPR output format: a/bdeck 64-knot radius winds in full circle - TC-Pairs -============== ========================== ================================== -Type Statistics References -============== ========================== ================================== -2D Objects | For each object: See the WWRP/WGNE JWGFVR website - | Location of the centroid +============== =============================== ============================= +Type Statistics References +============== =============================== ============================= +2D Objects | For each object: | See the WWRP/WGNE + | Location of the centroid | JWGFVR website | in grid units Location --------------- -------------------------- ---------------------------------- -A/BAL_WIND_34 | TCMPR output format: TC-Pairs +-------------- -------------------------- ----------------------------- +A/BAL_WIND_34 | TCMPR output format: TC-Pairs | a/bdeck 34-knot radius | winds in full circle -============== ========================== ================================== +============== ========================== ============================= From fae4fe897127e952dd913571ae1dd00682b8896c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 11:07:40 -0600 Subject: [PATCH 025/770] 3 trying reformating grid #1049 --- docs/Users_Guide/statistics_list.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 40ffacfd46..bef1486773 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 17th attempt. text wrapping NEW table This is only for the "A" items + 20th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -31,8 +31,8 @@ Type Statistics References 2D Objects | For each object: | See the WWRP/WGNE | Location of the centroid | JWGFVR website | in grid units Location --------------- -------------------------- ----------------------------- -A/BAL_WIND_34 | TCMPR output format: TC-Pairs +-------------- ------------------------------- ----------------------------- +A/BAL_WIND_34 | TCMPR output format: TC-Pairs | a/bdeck 34-knot radius | winds in full circle ============== ========================== ============================= From e6a014548bca8a52185846f0b2780aa7ed78562f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 11:14:27 -0600 Subject: [PATCH 026/770] 4 trying reformating grid #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index bef1486773..c3dc7a7d34 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 20th attempt. text wrapping NEW table This is only for the "A" items + 21th attempt. text wrapping NEW table This is only for the "A" items .. list-table:: Laundry list A. :widths: auto @@ -35,4 +35,4 @@ Type Statistics References A/BAL_WIND_34 | TCMPR output format: TC-Pairs | a/bdeck 34-knot radius | winds in full circle -============== ========================== ============================= +============== =============================== ============================= From 768c16243b196ce9f809014bc04dc4a8cf9950f1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 11:32:41 -0600 Subject: [PATCH 027/770] adding in one more table row #1049 --- docs/Users_Guide/statistics_list.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c3dc7a7d34..b66947dec7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - 21th attempt. text wrapping NEW table This is only for the "A" items + Attempt #22. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -35,4 +35,8 @@ Type Statistics References A/BAL_WIND_34 | TCMPR output format: TC-Pairs | a/bdeck 34-knot radius | winds in full circle +-------------- ------------------------------- ----------------------------- +A/BAL_WIND_50 | TCMPR output format: TC-Pairs + | a/bdeck 50-knot radius + | winds in full circle ============== =============================== ============================= From 05f8594197aecc6edaeed0afd761f359e8d94cc8 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 11:44:11 -0600 Subject: [PATCH 028/770] adding more text #1049 --- docs/Users_Guide/statistics_list.rst | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b66947dec7..ab1f74b2cc 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #22. text wrapping NEW table This is only for a couple of items. + Attempt #23. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -31,6 +31,27 @@ Type Statistics References 2D Objects | For each object: | See the WWRP/WGNE | Location of the centroid | JWGFVR website | in grid units Location + | of the centroid in lat/lon + | degrees, Axis angle, Length + | of the enclosing rectangle, + | Width of the enclosing + | rectangle, Object area, + | Radius of curvature of the + | object defined in terms of + | third order moments, Center + | of curvature Ratio of the + | difference between the area + | of an object and the area of + | its convex hull divided by + | the area of the complex hull, + | percentiles of intensity of + | the raw field within the + | object, Percentile of + | intensity chosen for use in + | the percentile intensity + | ratio, Sum of the + | intensities of the raw field + | within the object, etc.... -------------- ------------------------------- ----------------------------- A/BAL_WIND_34 | TCMPR output format: TC-Pairs | a/bdeck 34-knot radius @@ -40,3 +61,6 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | a/bdeck 50-knot radius | winds in full circle ============== =============================== ============================= + + + For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, From 0e1355e845127a362a6dfd6b2edc8032cacccf91 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 11:57:01 -0600 Subject: [PATCH 029/770] trying glossary format #1049 --- docs/Users_Guide/statistics_list.rst | 33 ++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ab1f74b2cc..c8ef53bc53 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #23. text wrapping NEW table This is only for a couple of items. + Attempt #24. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -62,5 +62,34 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | winds in full circle ============== =============================== ============================= +.. test:: + :sorted: - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, + REGRID_DATA_PLANE_ONCE_PER_FIELD + If True, run RegridDataPlane separately for each field name/level combination specified in the configuration file. See :ref:`Field_Info +` for more information on how fields are specified. If False, run RegridDataPlane once with all of the fields specified. + + | *Used by:* RegridDataPlane + + CUSTOM_LOOP_LIST + List of strings that are used to run each item in the :term:`PROCESS_LIST` multiple times for each run time to allow the tool to be run +with different configurations. The filename template tag {custom?fmt=%s} can be used throughout the METplus configuration file. For example, +the text can be used to supply different configuration files (if the MET tool uses them) and output filenames/directories. If you have two co +nfiguration files, SeriesAnalysisConfig_one and SeriesAnalysisConfig_two, you can set:: + + [config] + CUSTOM_LOOP_LIST = one, two + SERIES_ANALYSIS_CONFIG_FILE = {CONFIG_DIR}/SeriesAnalysisConfig_{custom?fmt=%s} + + [dir] + SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/{custom?fmt=%s} + + With this configuration, SeriesAnalysis will be called twice. The first run will use SeriesAnalysisConfig_one and write output to {OUTPUT +_BASE}/one. The second run will use SeriesAnalysisConfig_two and write output to {OUTPUT_BASE}/two. + + If unset or left blank, the wrapper will run once per run time. There are also wrapper-specific configuration variables to define a custo +m string loop list for a single wrapper, i.e. :term:`SERIES_ANALYSIS_CUSTOM_LOOP_LIST` and :term:`PCP_COMBINE_CUSTOM_LOOP_LIST`. + + | *Used by:* Many + + From cfe7316d79a439fdcf8d83818dc932acff0fed34 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 12:05:28 -0600 Subject: [PATCH 030/770] 2 trying glossary format #1049 --- docs/Users_Guide/statistics_list.rst | 37 +++++++++++++--------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c8ef53bc53..9abd94e10f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #24. text wrapping NEW table This is only for a couple of items. + Attempt #25. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -62,33 +62,30 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | winds in full circle ============== =============================== ============================= -.. test:: +.. glossary:: :sorted: REGRID_DATA_PLANE_ONCE_PER_FIELD - If True, run RegridDataPlane separately for each field name/level combination specified in the configuration file. See :ref:`Field_Info -` for more information on how fields are specified. If False, run RegridDataPlane once with all of the fields specified. + If True, run RegridDataPlane separately for each field name/level + combination specified in the configuration file. See :ref:`Field_Info` + for more information on how fields are specified. If False, + run RegridDataPlane once with all of the fields specified. | *Used by:* RegridDataPlane CUSTOM_LOOP_LIST - List of strings that are used to run each item in the :term:`PROCESS_LIST` multiple times for each run time to allow the tool to be run -with different configurations. The filename template tag {custom?fmt=%s} can be used throughout the METplus configuration file. For example, -the text can be used to supply different configuration files (if the MET tool uses them) and output filenames/directories. If you have two co -nfiguration files, SeriesAnalysisConfig_one and SeriesAnalysisConfig_two, you can set:: + List of strings that are used to run each item in the :term:`PROCESS_LIST` + multiple times for each run time to allow + the tool to be run with different configurations. The filename + template tag {custom?fmt=%s} can be used throughout the + METplus configuration file. For example, the text can be used to supply + different configuration files (if the MET tool uses them) and + output filenames/directories. CUT - [config] - CUSTOM_LOOP_LIST = one, two - SERIES_ANALYSIS_CONFIG_FILE = {CONFIG_DIR}/SeriesAnalysisConfig_{custom?fmt=%s} - - [dir] - SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/{custom?fmt=%s} - - With this configuration, SeriesAnalysis will be called twice. The first run will use SeriesAnalysisConfig_one and write output to {OUTPUT -_BASE}/one. The second run will use SeriesAnalysisConfig_two and write output to {OUTPUT_BASE}/two. - - If unset or left blank, the wrapper will run once per run time. There are also wrapper-specific configuration variables to define a custo -m string loop list for a single wrapper, i.e. :term:`SERIES_ANALYSIS_CUSTOM_LOOP_LIST` and :term:`PCP_COMBINE_CUSTOM_LOOP_LIST`. + If unset or left blank, the wrapper will run once per run time. There + are also wrapper-specific configuration variables to define a custom + string loop list for a single wrapper, i.e. + :term:`SERIES_ANALYSIS_CUSTOM_LOOP_LIST` and :term:`PCP_COMBINE_CUSTOM_LOOP_LIST`. | *Used by:* Many From e6a14efdd6a47d5ca94dbd73a0eeb3ce8ac675d8 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 12:11:31 -0600 Subject: [PATCH 031/770] 3 trying glossary format #1049 --- docs/Users_Guide/statistics_list.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 9abd94e10f..3b12970b3a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -74,19 +74,19 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | *Used by:* RegridDataPlane CUSTOM_LOOP_LIST - List of strings that are used to run each item in the :term:`PROCESS_LIST` - multiple times for each run time to allow - the tool to be run with different configurations. The filename - template tag {custom?fmt=%s} can be used throughout the - METplus configuration file. For example, the text can be used to supply - different configuration files (if the MET tool uses them) and - output filenames/directories. CUT + List of strings that are used to run each item in the :term:`PROCESS_LIST` + multiple times for each run time to allow + the tool to be run with different configurations. The filename + template tag {custom?fmt=%s} can be used throughout the + METplus configuration file. For example, the text can be used to supply + different configuration files (if the MET tool uses them) and + output filenames/directories. CUT - If unset or left blank, the wrapper will run once per run time. There - are also wrapper-specific configuration variables to define a custom - string loop list for a single wrapper, i.e. - :term:`SERIES_ANALYSIS_CUSTOM_LOOP_LIST` and :term:`PCP_COMBINE_CUSTOM_LOOP_LIST`. + If unset or left blank, the wrapper will run once per run time. There + are also wrapper-specific configuration variables to define a custom + string loop list for a single wrapper, i.e. + :term:`SERIES_ANALYSIS_CUSTOM_LOOP_LIST` and :term:`PCP_COMBINE_CUSTOM_LOOP_LIST`. - | *Used by:* Many + | *Used by:* Many From 6d72b2d203594a88099d86e11a3b90fb85c7b786 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 12:16:34 -0600 Subject: [PATCH 032/770] 4 trying glossary format #1049 --- docs/Users_Guide/statistics_list.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 3b12970b3a..c5da058caa 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #25. text wrapping NEW table This is only for a couple of items. + Attempt #26. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -65,7 +65,7 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs .. glossary:: :sorted: - REGRID_DATA_PLANE_ONCE_PER_FIELD + BOB_BLA_LAW If True, run RegridDataPlane separately for each field name/level combination specified in the configuration file. See :ref:`Field_Info` for more information on how fields are specified. If False, @@ -73,7 +73,7 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | *Used by:* RegridDataPlane - CUSTOM_LOOP_LIST + MADE_UP_NAME List of strings that are used to run each item in the :term:`PROCESS_LIST` multiple times for each run time to allow the tool to be run with different configurations. The filename From a185ac015fd4a3990412e9a59269a28d284424b5 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 12:27:25 -0600 Subject: [PATCH 033/770] adding glossary format 2D objects #1049 --- docs/Users_Guide/statistics_list.rst | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c5da058caa..f94f51e83a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #26. text wrapping NEW table This is only for a couple of items. + Attempt #27. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -62,9 +62,35 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | winds in full circle ============== =============================== ============================= + +Examples using the glossary format: + .. glossary:: :sorted: + 2D Objects + For each object: Location of the centroid in grid units, Location of + the centroid in lat/lon degrees, Axis angle, Length of the enclosing + rectangle, Width of the enclosing rectangle, Object area, Radius of + curvature of the object defined in terms of third order moments, Center + of curvature, Ratio of the difference between the area of an object + and the area of its convex hull divided by the area of the complex + hull, percentiles of intensity of the raw field within the object, + Percentile of intensity chosen for use in the percentile intensity + ratio, Sum of the intensities of the raw field within the object, + For paired objects: Distance between two objects centroids, Minimum + distance between the boundaries of two objects, Minimum distance + between the convex hulls of two objects, Difference between the axis + angles of two objects, Ratio of the areas of two objects, Intersection + area of two objects, Union area of two objects, Symmetric difference + of two objects, Ratio of intersection areas, Ratio of complexities, + Ratio of the nth percentile of intensity, Total interest value + computed for a pair of simple objects, NetCDF files with the objects + and raw data for further processing + + | See the WWRP/WGNE JWGFVR website for more details: + https://www.cawcr.gov.au/projects/verification + BOB_BLA_LAW If True, run RegridDataPlane separately for each field name/level combination specified in the configuration file. See :ref:`Field_Info` From 5ee4874036cf9806b474d1777e6bf5d92fee6794 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 12:33:21 -0600 Subject: [PATCH 034/770] fixing glossary format 2D objects #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f94f51e83a..958cf76c88 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -68,7 +68,7 @@ Examples using the glossary format: .. glossary:: :sorted: - 2D Objects + 2D Objects For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of From da80eaed3bc3926b105e5c2a0dba902b0f82a39d Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 12:40:44 -0600 Subject: [PATCH 035/770] adding glossary items #1049 --- docs/Users_Guide/statistics_list.rst | 31 ++++++++-------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 958cf76c88..4d2e426f08 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #27. text wrapping NEW table This is only for a couple of items. + Attempt #28. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -88,31 +88,18 @@ Examples using the glossary format: computed for a pair of simple objects, NetCDF files with the objects and raw data for further processing - | See the WWRP/WGNE JWGFVR website for more details: + | *See the WWRP/WGNE JWGFVR website for more details:* https://www.cawcr.gov.au/projects/verification - BOB_BLA_LAW - If True, run RegridDataPlane separately for each field name/level - combination specified in the configuration file. See :ref:`Field_Info` - for more information on how fields are specified. If False, - run RegridDataPlane once with all of the fields specified. + A/BAL_WIND_34 + TCMPR output format: a/bdeck 34-knot radius winds in full circle - | *Used by:* RegridDataPlane + | *Reference:* TC-Pairs - MADE_UP_NAME - List of strings that are used to run each item in the :term:`PROCESS_LIST` - multiple times for each run time to allow - the tool to be run with different configurations. The filename - template tag {custom?fmt=%s} can be used throughout the - METplus configuration file. For example, the text can be used to supply - different configuration files (if the MET tool uses them) and - output filenames/directories. CUT + A/BAL_WIND_50 + TCMPR output format: a/bdeck 50-knot radius winds in full circle - If unset or left blank, the wrapper will run once per run time. There - are also wrapper-specific configuration variables to define a custom - string loop list for a single wrapper, i.e. - :term:`SERIES_ANALYSIS_CUSTOM_LOOP_LIST` and :term:`PCP_COMBINE_CUSTOM_LOOP_LIST`. - - | *Used by:* Many + | *Reference:* TC-Pairs + From 6618a53180ba71a9166a98df1a4225d85f62f788 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 13:28:05 -0600 Subject: [PATCH 036/770] fixing glossary format #1049 --- docs/Users_Guide/statistics_list.rst | 52 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4d2e426f08..010da8e65f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #28. text wrapping NEW table This is only for a couple of items. + Attempt #29. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -69,37 +69,37 @@ Examples using the glossary format: :sorted: 2D Objects - For each object: Location of the centroid in grid units, Location of - the centroid in lat/lon degrees, Axis angle, Length of the enclosing - rectangle, Width of the enclosing rectangle, Object area, Radius of - curvature of the object defined in terms of third order moments, Center - of curvature, Ratio of the difference between the area of an object - and the area of its convex hull divided by the area of the complex - hull, percentiles of intensity of the raw field within the object, - Percentile of intensity chosen for use in the percentile intensity - ratio, Sum of the intensities of the raw field within the object, - For paired objects: Distance between two objects centroids, Minimum - distance between the boundaries of two objects, Minimum distance - between the convex hulls of two objects, Difference between the axis - angles of two objects, Ratio of the areas of two objects, Intersection - area of two objects, Union area of two objects, Symmetric difference - of two objects, Ratio of intersection areas, Ratio of complexities, - Ratio of the nth percentile of intensity, Total interest value - computed for a pair of simple objects, NetCDF files with the objects - and raw data for further processing + For each object: Location of the centroid in grid units, Location of + the centroid in lat/lon degrees, Axis angle, Length of the enclosing + rectangle, Width of the enclosing rectangle, Object area, Radius of + curvature of the object defined in terms of third order moments, Center + of curvature, Ratio of the difference between the area of an object + and the area of its convex hull divided by the area of the complex + hull, percentiles of intensity of the raw field within the object, + Percentile of intensity chosen for use in the percentile intensity + ratio, Sum of the intensities of the raw field within the object, + For paired objects: Distance between two objects centroids, Minimum + distance between the boundaries of two objects, Minimum distance + between the convex hulls of two objects, Difference between the axis + angles of two objects, Ratio of the areas of two objects, Intersection + area of two objects, Union area of two objects, Symmetric difference + of two objects, Ratio of intersection areas, Ratio of complexities, + Ratio of the nth percentile of intensity, Total interest value + computed for a pair of simple objects, NetCDF files with the objects + and raw data for further processing - | *See the WWRP/WGNE JWGFVR website for more details:* - https://www.cawcr.gov.au/projects/verification + | *See the WWRP/WGNE JWGFVR website for more details:* + https://www.cawcr.gov.au/projects/verification A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle + TCMPR output format: a/bdeck 34-knot radius winds in full circle - | *Reference:* TC-Pairs + | *Reference:* TC-Pairs - A/BAL_WIND_50 - TCMPR output format: a/bdeck 50-knot radius winds in full circle + A/BAL_WIND_50 + TCMPR output format: a/bdeck 50-knot radius winds in full circle - | *Reference:* TC-Pairs + | *Reference:* TC-Pairs From 25dc0bbbebc3868344db37761f2048e3f01e7f05 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 6 Aug 2021 13:39:00 -0600 Subject: [PATCH 037/770] hopefully all examples are working #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 010da8e65f..ed8b10ae49 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,7 @@ METplus Laundry List of Statistics ********************************** - Attempt #29. text wrapping NEW table This is only for a couple of items. + Attempt #30. text wrapping NEW table This is only for a couple of items. .. list-table:: Laundry list A. :widths: auto @@ -88,7 +88,7 @@ Examples using the glossary format: computed for a pair of simple objects, NetCDF files with the objects and raw data for further processing - | *See the WWRP/WGNE JWGFVR website for more details:* + | *Reference: See the WWRP/WGNE JWGFVR website for more details:* https://www.cawcr.gov.au/projects/verification A/BAL_WIND_34 From 81556f676d97e3823e3a3da1ce7472dd02ea1a58 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 13:19:39 -0600 Subject: [PATCH 038/770] changing reference to tool --- docs/Users_Guide/statistics_list.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ed8b10ae49..ee889a5a52 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -11,7 +11,7 @@ METplus Laundry List of Statistics * - Type - Statistics - - References + - Tool * - 2D Objects - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - See the WWRP/WGNE JWGFVR website @@ -26,7 +26,7 @@ METplus Laundry List of Statistics - TC-Pairs ============== =============================== ============================= -Type Statistics References +Type Statistics Tool ============== =============================== ============================= 2D Objects | For each object: | See the WWRP/WGNE | Location of the centroid | JWGFVR website @@ -88,18 +88,18 @@ Examples using the glossary format: computed for a pair of simple objects, NetCDF files with the objects and raw data for further processing - | *Reference: See the WWRP/WGNE JWGFVR website for more details:* + | *Tool: See the WWRP/WGNE JWGFVR website for more details:* https://www.cawcr.gov.au/projects/verification A/BAL_WIND_34 TCMPR output format: a/bdeck 34-knot radius winds in full circle - | *Reference:* TC-Pairs + | *Tool:* TC-Pairs A/BAL_WIND_50 TCMPR output format: a/bdeck 50-knot radius winds in full circle - | *Reference:* TC-Pairs + | *Tool:* TC-Pairs From 5131e7f3aee2d427cf0e633ebfc3e0a7ca0dd9e7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 13:22:38 -0600 Subject: [PATCH 039/770] removing 2D object examples --- docs/Users_Guide/statistics_list.rst | 52 +--------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ee889a5a52..1ba9d05860 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -12,9 +12,6 @@ METplus Laundry List of Statistics * - Type - Statistics - Tool - * - 2D Objects - - For each object: Location of the centroid in grid units, Location of the centroid in lat/lon degrees, Axis angle, Length of the enclosing rectangle, Width of the enclosing rectangle, Object area, Radius of curvature of the object defined in terms of third order moments, Center of curvature, Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull, percentiles of intensity of the raw field within the object, Percentile of intensity chosen for use in the percentile intensity ratio, Sum of the intensities of the raw field within the object, - - See the WWRP/WGNE JWGFVR website * - A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle - TC-Pairs @@ -28,31 +25,6 @@ METplus Laundry List of Statistics ============== =============================== ============================= Type Statistics Tool ============== =============================== ============================= -2D Objects | For each object: | See the WWRP/WGNE - | Location of the centroid | JWGFVR website - | in grid units Location - | of the centroid in lat/lon - | degrees, Axis angle, Length - | of the enclosing rectangle, - | Width of the enclosing - | rectangle, Object area, - | Radius of curvature of the - | object defined in terms of - | third order moments, Center - | of curvature Ratio of the - | difference between the area - | of an object and the area of - | its convex hull divided by - | the area of the complex hull, - | percentiles of intensity of - | the raw field within the - | object, Percentile of - | intensity chosen for use in - | the percentile intensity - | ratio, Sum of the - | intensities of the raw field - | within the object, etc.... --------------- ------------------------------- ----------------------------- A/BAL_WIND_34 | TCMPR output format: TC-Pairs | a/bdeck 34-knot radius | winds in full circle @@ -68,29 +40,7 @@ Examples using the glossary format: .. glossary:: :sorted: - 2D Objects - For each object: Location of the centroid in grid units, Location of - the centroid in lat/lon degrees, Axis angle, Length of the enclosing - rectangle, Width of the enclosing rectangle, Object area, Radius of - curvature of the object defined in terms of third order moments, Center - of curvature, Ratio of the difference between the area of an object - and the area of its convex hull divided by the area of the complex - hull, percentiles of intensity of the raw field within the object, - Percentile of intensity chosen for use in the percentile intensity - ratio, Sum of the intensities of the raw field within the object, - For paired objects: Distance between two objects centroids, Minimum - distance between the boundaries of two objects, Minimum distance - between the convex hulls of two objects, Difference between the axis - angles of two objects, Ratio of the areas of two objects, Intersection - area of two objects, Union area of two objects, Symmetric difference - of two objects, Ratio of intersection areas, Ratio of complexities, - Ratio of the nth percentile of intensity, Total interest value - computed for a pair of simple objects, NetCDF files with the objects - and raw data for further processing - - | *Tool: See the WWRP/WGNE JWGFVR website for more details:* - https://www.cawcr.gov.au/projects/verification - + A/BAL_WIND_34 TCMPR output format: a/bdeck 34-knot radius winds in full circle From 18afe38a8cf2557addf302cae05fd4d5fc25dcbf Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 13:25:06 -0600 Subject: [PATCH 040/770] removing the unwrapped table --- docs/Users_Guide/statistics_list.rst | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 1ba9d05860..c226cecef8 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,25 +3,6 @@ METplus Laundry List of Statistics ********************************** - Attempt #30. text wrapping NEW table This is only for a couple of items. - - .. list-table:: Laundry list A. - :widths: auto - :header-rows: 1 - - * - Type - - Statistics - - Tool - * - A/BAL_WIND_34 - - TCMPR output format: a/bdeck 34-knot radius winds in full circle - - TC-Pairs - * - A/BAL_WIND_50 - - TCMPR output format: a/bdeck 50-knot radius winds in full circle - - TC-Pairs - * - A/BAL_WIND_64 - - TCMPR output format: a/bdeck 64-knot radius winds in full circle - - TC-Pairs - ============== =============================== ============================= Type Statistics Tool ============== =============================== ============================= From e6a57e943688d0c7152a3844a74926145186ebd8 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 15:39:57 -0600 Subject: [PATCH 041/770] adding ACC into the example --- docs/Users_Guide/statistics_list.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c226cecef8..8694b64da5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -13,6 +13,20 @@ A/BAL_WIND_34 | TCMPR output format: TC-Pairs A/BAL_WIND_50 | TCMPR output format: TC-Pairs | a/bdeck 50-knot radius | winds in full circle +-------------- ------------------------------- ----------------------------- +ACC | MODE output format: Accuracy MODE-Tool + | CTS output format: Accuracy Point-Stat Tool + | including normal and + | bootstrap upper and lower + | confidence limits + | MCTS output format: Accuracy, + | normal confidence limits + | and bootstrap confidence + | limits + | NBRCTCS output format: Grid-Stat Tool + | Accuracy including normal + | and bootstrap upper and lower + | confidence limits" ============== =============================== ============================= @@ -32,5 +46,14 @@ Examples using the glossary format: | *Tool:* TC-Pairs + ACC + | MODE output format: Accuracy + | CTS output format: Accuracy including normal and bootstrap + | upper and lower confidence limits + | MCTS output format: Accuracy, normal confidence limits and bootstrap + | confidence limits + | NBRCTCS output format: Accuracy including normal and bootstrap upper + | and lower confidence limits" + From 30344549edfa44d5bf15a00f073ecb23ce5bd29a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 15:50:51 -0600 Subject: [PATCH 042/770] fixing formatting #1049 --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8694b64da5..f47219c4b0 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -14,8 +14,8 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | a/bdeck 50-knot radius | winds in full circle -------------- ------------------------------- ----------------------------- -ACC | MODE output format: Accuracy MODE-Tool - | CTS output format: Accuracy Point-Stat Tool +ACC | MODE output format: Accuracy | MODE-Tool + | CTS output format: Accuracy | Point-Stat Tool | including normal and | bootstrap upper and lower | confidence limits @@ -23,7 +23,7 @@ ACC | MODE output format: Accuracy MODE-Tool | normal confidence limits | and bootstrap confidence | limits - | NBRCTCS output format: Grid-Stat Tool + | NBRCTCS output format: | Grid-Stat Tool | Accuracy including normal | and bootstrap upper and lower | confidence limits" @@ -54,6 +54,6 @@ Examples using the glossary format: | confidence limits | NBRCTCS output format: Accuracy including normal and bootstrap upper | and lower confidence limits" - + | *Tools:* MODE-Tool, Point-Stat Tool & Grid-Stat Tool From 93ccf264d8b87e709e79818f14bce8dcabb92eb3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 15:56:09 -0600 Subject: [PATCH 043/770] fixing formatting take 2 #1049 --- docs/Users_Guide/statistics_list.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f47219c4b0..52ca00f3cc 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -16,17 +16,17 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs -------------- ------------------------------- ----------------------------- ACC | MODE output format: Accuracy | MODE-Tool | CTS output format: Accuracy | Point-Stat Tool - | including normal and - | bootstrap upper and lower - | confidence limits - | MCTS output format: Accuracy, - | normal confidence limits - | and bootstrap confidence - | limits + | including normal and | + | bootstrap upper and lower | + | confidence limits | + | MCTS output format: Accuracy, | + | normal confidence limits | + | and bootstrap confidence | + | limits | | NBRCTCS output format: | Grid-Stat Tool - | Accuracy including normal - | and bootstrap upper and lower - | confidence limits" + | Accuracy including normal | + | and bootstrap upper and lower | + | confidence limits" | ============== =============================== ============================= @@ -53,7 +53,8 @@ Examples using the glossary format: | MCTS output format: Accuracy, normal confidence limits and bootstrap | confidence limits | NBRCTCS output format: Accuracy including normal and bootstrap upper - | and lower confidence limits" + | and lower confidence limits" + | | *Tools:* MODE-Tool, Point-Stat Tool & Grid-Stat Tool From 9b00c980fc89f42517143e6c2f4345b4dd4780a0 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 16:03:00 -0600 Subject: [PATCH 044/770] fixing formatting take 3 #1049 --- docs/Users_Guide/statistics_list.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 52ca00f3cc..432126679d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -15,6 +15,7 @@ A/BAL_WIND_50 | TCMPR output format: TC-Pairs | winds in full circle -------------- ------------------------------- ----------------------------- ACC | MODE output format: Accuracy | MODE-Tool + | | | CTS output format: Accuracy | Point-Stat Tool | including normal and | | bootstrap upper and lower | @@ -23,6 +24,7 @@ ACC | MODE output format: Accuracy | MODE-Tool | normal confidence limits | | and bootstrap confidence | | limits | + | | | NBRCTCS output format: | Grid-Stat Tool | Accuracy including normal | | and bootstrap upper and lower | From 627f2a63a51a58dea02f999bbe4d5f03e8abc5d3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 16:34:15 -0600 Subject: [PATCH 045/770] bolding and language change #1049 --- docs/Users_Guide/statistics_list.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 432126679d..16c26eb1cd 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -6,26 +6,26 @@ METplus Laundry List of Statistics ============== =============================== ============================= Type Statistics Tool ============== =============================== ============================= -A/BAL_WIND_34 | TCMPR output format: TC-Pairs +A/BAL_WIND_34 | TCMPR line type: TC-Pairs | a/bdeck 34-knot radius | winds in full circle -------------- ------------------------------- ----------------------------- -A/BAL_WIND_50 | TCMPR output format: TC-Pairs +A/BAL_WIND_50 | TCMPR line type: TC-Pairs | a/bdeck 50-knot radius | winds in full circle -------------- ------------------------------- ----------------------------- -ACC | MODE output format: Accuracy | MODE-Tool +ACC | MODE line type: Accuracy | MODE-Tool | | - | CTS output format: Accuracy | Point-Stat Tool + | CTS line type: Accuracy | Point-Stat Tool | including normal and | | bootstrap upper and lower | | confidence limits | - | MCTS output format: Accuracy, | + | MCTS line type: Accuracy, | | normal confidence limits | | and bootstrap confidence | | limits | | | - | NBRCTCS output format: | Grid-Stat Tool + | NBRCTCS line type: | Grid-Stat Tool | Accuracy including normal | | and bootstrap upper and lower | | confidence limits" | @@ -39,22 +39,22 @@ Examples using the glossary format: A/BAL_WIND_34 - TCMPR output format: a/bdeck 34-knot radius winds in full circle + **TCMPR line type**: a/bdeck 34-knot radius winds in full circle | *Tool:* TC-Pairs A/BAL_WIND_50 - TCMPR output format: a/bdeck 50-knot radius winds in full circle + **TCMPR line type**: a/bdeck 50-knot radius winds in full circle | *Tool:* TC-Pairs ACC - | MODE output format: Accuracy - | CTS output format: Accuracy including normal and bootstrap + | **MODE line type**: Accuracy + | **CTS line type**: Accuracy including normal and bootstrap | upper and lower confidence limits - | MCTS output format: Accuracy, normal confidence limits and bootstrap + | **MCTS line type**: Accuracy, normal confidence limits and bootstrap | confidence limits - | NBRCTCS output format: Accuracy including normal and bootstrap upper + | **NBRCTCS line type**: Accuracy including normal and bootstrap upper | and lower confidence limits" | | *Tools:* MODE-Tool, Point-Stat Tool & Grid-Stat Tool From 3bba3bf46e06cc3a2eaf2fc606ff35ce76e017b4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 16:57:24 -0600 Subject: [PATCH 046/770] fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 16c26eb1cd..5b0b40ad17 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -14,7 +14,7 @@ A/BAL_WIND_50 | TCMPR line type: TC-Pairs | a/bdeck 50-knot radius | winds in full circle -------------- ------------------------------- ----------------------------- -ACC | MODE line type: Accuracy | MODE-Tool +ACC | MODE line type: Accuracy | MODE-Tool | | | CTS line type: Accuracy | Point-Stat Tool | including normal and | From 37e83b455a968788e0935fa15f18a710d5f33f1a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 25 Aug 2021 17:07:26 -0600 Subject: [PATCH 047/770] fixing spacing again #1049 --- docs/Users_Guide/statistics_list.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 5b0b40ad17..721397ab42 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -20,6 +20,7 @@ ACC | MODE line type: Accuracy | MODE-Tool | including normal and | | bootstrap upper and lower | | confidence limits | + | | | MCTS line type: Accuracy, | | normal confidence limits | | and bootstrap confidence | From adba2c3b52ff67d06872fc8564d7ebc8bc2ef52a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 26 Aug 2021 09:05:50 -0600 Subject: [PATCH 048/770] changing title #1049 --- docs/Users_Guide/statistics_list.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 721397ab42..19e81aec74 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1,6 +1,6 @@ -********************************** -METplus Laundry List of Statistics -********************************** +****************************** +METplus Database of Statistics +****************************** ============== =============================== ============================= From 53890d3a010638c44dce228a11f315809495b21f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 26 Aug 2021 13:41:12 -0600 Subject: [PATCH 049/770] trying superscript #1049 --- docs/Users_Guide/statistics_list.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 19e81aec74..e3d611e112 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -37,8 +37,7 @@ Examples using the glossary format: .. glossary:: :sorted: - - + A/BAL_WIND_34 **TCMPR line type**: a/bdeck 34-knot radius winds in full circle @@ -50,7 +49,7 @@ Examples using the glossary format: | *Tool:* TC-Pairs ACC - | **MODE line type**: Accuracy + | **MODE line type**: Accuracy \ :sub:`1` | **CTS line type**: Accuracy including normal and bootstrap | upper and lower confidence limits | **MCTS line type**: Accuracy, normal confidence limits and bootstrap @@ -58,6 +57,6 @@ Examples using the glossary format: | **NBRCTCS line type**: Accuracy including normal and bootstrap upper | and lower confidence limits" | - | *Tools:* MODE-Tool, Point-Stat Tool & Grid-Stat Tool + | *Tools:* \ :sub:`1` \ MODE-Tool, Point-Stat Tool & Grid-Stat Tool From 3a635d8cb79c46c7329a5596710a5ce213ccbcbb Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 26 Aug 2021 13:51:07 -0600 Subject: [PATCH 050/770] superscript glossary #1049 --- docs/Users_Guide/statistics_list.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e3d611e112..ba89e2c229 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -49,14 +49,15 @@ Examples using the glossary format: | *Tool:* TC-Pairs ACC - | **MODE line type**: Accuracy \ :sub:`1` + | **MODE line type**: Accuracy \ :sup:`1` | **CTS line type**: Accuracy including normal and bootstrap - | upper and lower confidence limits + | upper and lower confidence limits \ :sup:`2,3` | **MCTS line type**: Accuracy, normal confidence limits and bootstrap - | confidence limits + | confidence limits \ :sup:`2,3` | **NBRCTCS line type**: Accuracy including normal and bootstrap upper - | and lower confidence limits" + | and lower confidence limits" \ :sup:`3` | - | *Tools:* \ :sub:`1` \ MODE-Tool, Point-Stat Tool & Grid-Stat Tool + | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool + & \ :sup:`3` \ Grid-Stat Tool From 796553ed62cf5cf605a139434cd77f3d4b9677a3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 26 Aug 2021 14:29:36 -0600 Subject: [PATCH 051/770] removing quotes #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ba89e2c229..6178ae350e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -29,7 +29,7 @@ ACC | MODE line type: Accuracy | MODE-Tool | NBRCTCS line type: | Grid-Stat Tool | Accuracy including normal | | and bootstrap upper and lower | - | confidence limits" | + | confidence limits | ============== =============================== ============================= @@ -55,7 +55,7 @@ Examples using the glossary format: | **MCTS line type**: Accuracy, normal confidence limits and bootstrap | confidence limits \ :sup:`2,3` | **NBRCTCS line type**: Accuracy including normal and bootstrap upper - | and lower confidence limits" \ :sup:`3` + | and lower confidence limits \ :sup:`3` | | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool From 48f0ddd542e22ef3bc035e4d7c987128b81ecb02 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 26 Aug 2021 14:40:30 -0600 Subject: [PATCH 052/770] fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6178ae350e..efc4aa60f1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -29,7 +29,7 @@ ACC | MODE line type: Accuracy | MODE-Tool | NBRCTCS line type: | Grid-Stat Tool | Accuracy including normal | | and bootstrap upper and lower | - | confidence limits | + | confidence limits | ============== =============================== ============================= From 44f7b1b9410cd0bdf772f5e562fdf2c8a536c41e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 11:02:42 -0600 Subject: [PATCH 053/770] Tara has decided to go with the glossary format. Removing table example. #1049 --- docs/Users_Guide/statistics_list.rst | 33 ---------------------------- 1 file changed, 33 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index efc4aa60f1..5ac347aea5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,39 +2,6 @@ METplus Database of Statistics ****************************** - -============== =============================== ============================= -Type Statistics Tool -============== =============================== ============================= -A/BAL_WIND_34 | TCMPR line type: TC-Pairs - | a/bdeck 34-knot radius - | winds in full circle --------------- ------------------------------- ----------------------------- -A/BAL_WIND_50 | TCMPR line type: TC-Pairs - | a/bdeck 50-knot radius - | winds in full circle --------------- ------------------------------- ----------------------------- -ACC | MODE line type: Accuracy | MODE-Tool - | | - | CTS line type: Accuracy | Point-Stat Tool - | including normal and | - | bootstrap upper and lower | - | confidence limits | - | | - | MCTS line type: Accuracy, | - | normal confidence limits | - | and bootstrap confidence | - | limits | - | | - | NBRCTCS line type: | Grid-Stat Tool - | Accuracy including normal | - | and bootstrap upper and lower | - | confidence limits | -============== =============================== ============================= - - -Examples using the glossary format: - .. glossary:: :sorted: From 54d2c5490baa4ad92f20d187de743098fbcab12c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:19:52 -0600 Subject: [PATCH 054/770] Adding 2D objects #1049 --- docs/Users_Guide/statistics_list.rst | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 5ac347aea5..8af314a1ca 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -4,7 +4,39 @@ METplus Database of Statistics .. glossary:: :sorted: - + + 2D Objects + | For each Object: + | Location of the centroid in grid units + | Location of the centroid in lat/lon degrees + | Axis angle, Length of the enclosing rectangle + | Width of the enclosing rectangle + | Object area + | Radius of curvature of the object defined in terms of third order + moments + | Center of curvature + | Ratio of the difference between the area of an object and the area + of its convex hull divided by the area of the complex hull + | percentiles of intensity of the raw field within the object + | Percentile of intensity chosen for use in the percentile intensity + ratio + | Sum of the intensities of the raw field within the object + | + | For paired objects: + Distance between two objects centroids, Minimum distance between the + boundaries of two objects + | Minimum distance between the convex hulls of two objects + | Difference between the axis angles of two objects + | Ratio of the areas of two objects + | Intersection area of two objects + | Union area of two objects + | Symmetric difference of two objects + | Ratio of intersection areas + | Ratio of complexities + | Ratio of the nth percentile of intensity + | Total interest value computed for a pair of simple objects + | NetCDF files with the objects and raw data for further processing + A/BAL_WIND_34 **TCMPR line type**: a/bdeck 34-knot radius winds in full circle From 0a7db634c74fb8ad3b01b6a7e9404783c6e3b077 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:24:38 -0600 Subject: [PATCH 055/770] 2D objects fix #1049 --- docs/Users_Guide/statistics_list.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8af314a1ca..d263d8b692 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -6,6 +6,7 @@ METplus Database of Statistics :sorted: 2D Objects + | For each Object: | Location of the centroid in grid units | Location of the centroid in lat/lon degrees @@ -36,7 +37,10 @@ METplus Database of Statistics | Ratio of the nth percentile of intensity | Total interest value computed for a pair of simple objects | NetCDF files with the objects and raw data for further processing - + + | *Tool:* See the WWRP/WGNE JWGFVR website for more details: + https://www.cawcr.gov.au/projects/verification + A/BAL_WIND_34 **TCMPR line type**: a/bdeck 34-knot radius winds in full circle From 0ea6a6884348e1a9ee4a306ed487cee1f0b5570c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:29:15 -0600 Subject: [PATCH 056/770] 2D objects fix attempt 2 #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index d263d8b692..1835f6a083 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -9,6 +9,7 @@ METplus Database of Statistics | For each Object: | Location of the centroid in grid units + | Location of the centroid in lat/lon degrees | Axis angle, Length of the enclosing rectangle | Width of the enclosing rectangle @@ -39,7 +40,7 @@ METplus Database of Statistics | NetCDF files with the objects and raw data for further processing | *Tool:* See the WWRP/WGNE JWGFVR website for more details: - https://www.cawcr.gov.au/projects/verification + | https://www.cawcr.gov.au/projects/verification A/BAL_WIND_34 **TCMPR line type**: a/bdeck 34-knot radius winds in full circle From bf8a9e27a899344a717488889df88d251984a83b Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:33:56 -0600 Subject: [PATCH 057/770] 2D objects fix attempt 3 #1049 --- docs/Users_Guide/statistics_list.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 1835f6a083..36f05dcd6a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -6,10 +6,8 @@ METplus Database of Statistics :sorted: 2D Objects - | For each Object: | Location of the centroid in grid units - | Location of the centroid in lat/lon degrees | Axis angle, Length of the enclosing rectangle | Width of the enclosing rectangle From 2ba1353e8adb1016a01e0424bec4576a6f5e65f6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:38:04 -0600 Subject: [PATCH 058/770] 2D objects fix attempt 4 #1049 --- docs/Users_Guide/statistics_list.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 36f05dcd6a..9a84489bbf 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -12,6 +12,7 @@ METplus Database of Statistics | Axis angle, Length of the enclosing rectangle | Width of the enclosing rectangle | Object area + | Radius of curvature of the object defined in terms of third order moments | Center of curvature From 5e858f374d195b34889c369d3b547901d88155bb Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:42:05 -0600 Subject: [PATCH 059/770] 2D objects fix attempt 5 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 9a84489bbf..8a6ab3de98 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -15,17 +15,21 @@ METplus Database of Statistics | Radius of curvature of the object defined in terms of third order moments + | Center of curvature | Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull + | percentiles of intensity of the raw field within the object | Percentile of intensity chosen for use in the percentile intensity ratio + | Sum of the intensities of the raw field within the object | | For paired objects: Distance between two objects centroids, Minimum distance between the boundaries of two objects + | Minimum distance between the convex hulls of two objects | Difference between the axis angles of two objects | Ratio of the areas of two objects From 4b750721322909271fcc93a8b61d2c2eddb0728f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:46:30 -0600 Subject: [PATCH 060/770] 2D objects fix attempt 7 #1049 --- docs/Users_Guide/statistics_list.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8a6ab3de98..f8938541ca 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -14,21 +14,21 @@ METplus Database of Statistics | Object area | Radius of curvature of the object defined in terms of third order - moments + moments | Center of curvature | Ratio of the difference between the area of an object and the area - of its convex hull divided by the area of the complex hull + of its convex hull divided by the area of the complex hull | percentiles of intensity of the raw field within the object | Percentile of intensity chosen for use in the percentile intensity - ratio + ratio | Sum of the intensities of the raw field within the object | | For paired objects: - Distance between two objects centroids, Minimum distance between the - boundaries of two objects + Distance between two objects centroids, Minimum distance between the + boundaries of two objects | Minimum distance between the convex hulls of two objects | Difference between the axis angles of two objects From 100338f13eaf161253d04c467bcea3383225b85e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 15:54:32 -0600 Subject: [PATCH 061/770] 2D objects fix attempt 8 #1049 --- docs/Users_Guide/statistics_list.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f8938541ca..da4ef08577 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -12,24 +12,19 @@ METplus Database of Statistics | Axis angle, Length of the enclosing rectangle | Width of the enclosing rectangle | Object area - | Radius of curvature of the object defined in terms of third order moments - | Center of curvature | Ratio of the difference between the area of an object and the area of its convex hull divided by the area of the complex hull - | percentiles of intensity of the raw field within the object | Percentile of intensity chosen for use in the percentile intensity ratio - | Sum of the intensities of the raw field within the object | | For paired objects: Distance between two objects centroids, Minimum distance between the boundaries of two objects - | Minimum distance between the convex hulls of two objects | Difference between the axis angles of two objects | Ratio of the areas of two objects From 6908e07d8913310007e44f25c7e224d360fee183 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 3 Sep 2021 16:11:03 -0600 Subject: [PATCH 062/770] ABR added #1049 --- docs/Users_Guide/statistics_list.rst | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index da4ef08577..98ac1606df 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -50,6 +50,78 @@ METplus Database of Statistics | *Tool:* TC-Pairs + A/BAL_WIND_64 + **TCMPR line type**: a/bdeck 64-knot radius winds in full circle + + | *Tool:* TC-Pairs + + A/BDEPTH + **TCMPR line type**: system depth, D-deep, M-medium, S-shallow, X-unknown + + | *Tool:* TC-Pairs + + A/BDIR + **TCMPR line type**: storm direction in compass coordinates, 0 - 359 + degrees + + | *Tool:* TC-Pairs + + A/BEYE + **TCMPR line type**: eye diameter, 0 through 999 nm + + | *Tool:* TC-Pairs + + A/BGUSTS + **TCMPR line type**: gusts, 0 through 995 kts + + | *Tool:* TC-Pairs + + A/BMRD + **TCMPR line type**: radius of max winds, 0 - 999 nm + + | *Tool:* TC-Pairs + + A/BNE_WIND_34 + **TCMPR line type**: a/bdeck 34-knot radius winds in NE quadrant + + | *Tool:* TC-Pairs + + A/BNE_WIND_50 + **TCMPR line type**: a/bdeck 50-knot radius winds in NE quadrant + + | *Tool:* TC-Pairs + + A/BNE_WIND_64 + **TCMPR line type**: a/bdeck 64-knot radius winds in NE quadrant + + | *Tool:* TC-Pairs + + A/BNW_WIND_34 + **TCMPR line type**: a/bdeck 34-knot radius winds in NW quadrant + + | *Tool:* TC-Pairs + + A/BNW_WIND_50 + **TCMPR line type**: a/bdeck 50-knot radius winds in NW quadrant + + | *Tool:* TC-Pairs + + A/BNW_WIND_64 + **TCMPR line type**: a/bdeck 64-knot radius winds in NW quadrant + + | *Tool:* TC-Pairs + + A/BRADP + **TCMPR line type**: pressure in millibars of the last closed isobar, + 900 - 1050 mb + + | *Tool:* TC-Pairs + + A/BRRP + **TCMPR line type**: radius of the last closed isobar in nm, 0 - 9999 nm + + | *Tool:* TC-Pairs + ACC | **MODE line type**: Accuracy \ :sup:`1` | **CTS line type**: Accuracy including normal and bootstrap From 69e669ec60921fe8814c651d26dca97d9755ec9a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 7 Sep 2021 09:27:48 -0600 Subject: [PATCH 063/770] adding remaining AB items to list #1049 --- docs/Users_Guide/statistics_list.rst | 39 ++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 98ac1606df..f2b61eeeec 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -6,7 +6,7 @@ METplus Database of Statistics :sorted: 2D Objects - | For each Object: + | **For each Object:** | Location of the centroid in grid units | Location of the centroid in lat/lon degrees | Axis angle, Length of the enclosing rectangle @@ -22,7 +22,7 @@ METplus Database of Statistics ratio | Sum of the intensities of the raw field within the object | - | For paired objects: + | **For paired objects:** Distance between two objects centroids, Minimum distance between the boundaries of two objects | Minimum distance between the convex hulls of two objects @@ -122,6 +122,41 @@ METplus Database of Statistics | *Tool:* TC-Pairs + A/BSE_WIND_34 + **TCMPR line type:** a/bdeck 34-knot radius winds in SE quadrant + + | *Tool:* TC-Pairs + + A/BSE_WIND_50 + **TCMPR line type:** a/bdeck 50-knot radius winds in SE quadrant + + | *Tool:* TC-Pairs + + A/BSE_WIND_64 + **TCMPR line type:** a/bdeck 64-knot radius winds in SE quadrant + + | *Tool:* TC-Pairs + + A/BSPEED + **TCMPR line type:** storm speed, 0 - 999 kts + + | *Tool:* TC-Pairs + + A/BSW_WIND_34 + **TCMPR line type:** a/bdeck 34-knot radius winds in SW quadrant + + | *Tool:* TC-Pairs + + A/BSW_WIND_50 + **TCMPR line type:** a/bdeck 50-knot radius winds in SW quadrant + + | *Tool:* TC-Pairs + + A/BSW_WIND_64 + **TCMPR line type:** a/bdeck 64-knot radius winds in SW quadrant + + | *Tool:* TC-Pairs + ACC | **MODE line type**: Accuracy \ :sup:`1` | **CTS line type**: Accuracy including normal and bootstrap From 0ed15848a4708b48c998c019fff8925cc05f6110 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 7 Sep 2021 09:46:39 -0600 Subject: [PATCH 064/770] adding remaining ACC_ items to list #1049 --- docs/Users_Guide/statistics_list.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f2b61eeeec..880ac987be 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -169,4 +169,15 @@ METplus Database of Statistics | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool - + ACC_NCL + ACC_NCU + ACC_BCL + ACC_BCU + | **CTS line type:** Accuracy including normal and bootstrap upper and + | lower confidence limits \ :sup:`2` + | **MCTS line type:** Accuracy, normal confidence limits and bootstrap + | confidence limits \ :sup:`2` + | **NBRCTCS line type:** Accuracy including normal and bootstrap upper + | and lower confidence limits \ :sup:`3` + | + | *Tools:* \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool From 88ad4122df19209fe8b5bd7ecfd71457a09832a1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 10:04:53 -0600 Subject: [PATCH 065/770] ADLAND & AFSS entries #1049 --- docs/Users_Guide/statistics_list.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 880ac987be..1b0f79ab01 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -181,3 +181,17 @@ METplus Database of Statistics | and lower confidence limits \ :sup:`3` | | *Tools:* \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool + + ADLAND + | **TCMPR line type:** adeck distance to land (nm) + | **PROBRIRW line type:** adeck distance to land (nm) + | + | *Tools:* TC-Pairs + + AFSS + AFSS_BCL + AFSS_BCU + | **NBRCNT line type:** Asymptotic Fractions Skill Score including + | bootstrap upper and lower confidence limits + | + | *Tools:* Grid-Stat Tool From a4404a80c60d207651c3f1a9f25db4c3230881c0 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 13:29:15 -0600 Subject: [PATCH 066/770] A entries #1049 --- docs/Users_Guide/statistics_list.rst | 63 +++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 1b0f79ab01..7b71d9044c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -186,7 +186,7 @@ METplus Database of Statistics | **TCMPR line type:** adeck distance to land (nm) | **PROBRIRW line type:** adeck distance to land (nm) | - | *Tools:* TC-Pairs + | *Tool:* TC-Pairs AFSS AFSS_BCL @@ -194,4 +194,63 @@ METplus Database of Statistics | **NBRCNT line type:** Asymptotic Fractions Skill Score including | bootstrap upper and lower confidence limits | - | *Tools:* Grid-Stat Tool + | *Tool:* Grid-Stat Tool + + AGEN_DLAND + | **GENMPR line type:** Forecast genesis event distance to land (nm) + | + | *Tool*: TC-Gen + + AGEN_FHR + | **GENMPR line type:** Forecast hour of genesis event + | + | *Tool*: TC-Gen + + AGEN_INIT + | **GENMPR line type:** Forecast initialization time + | + | *Tool*: TC-Gen + + AGEN_LAT + | **GENMPR line type:** Latitude position of the forecast genesis event + | + | *Tool*: TC-Gen + + AGEN_LON + | **GENMPR line type:** Longitude position of the forecast genesis event + | + | *Tool*: TC-Gen + + ALAT + | **TCMPR line type:** Latitude position of adeck model + | **PROBRIRW line type:** Latitude position of edeck model + | + | *Tool*: TC-Pairs + + ALON + | **TCMPR line type:** Longitude position of adeck model + | **PROBRIRW line type:** Longitude position of edeck model + | + | *Tool*: TC-Pairs + + ALPHA + | **Point-Stat line type:** Error percent value used in confidence + intervals \ :sup:`2` \ + | **grid-stat line type:** Error percent value used in confidence + | intervals \ :sup:`3` \ + | **wavelet-stat line type:** NA in Wavelet-Stat \ :sup:`4` \ + | **TC-Gen line type:** Error percent value used in confidence + | intervals \ :sup:`5` \ + | + | *Tools:* \ :sup:`2` \ Point-Stat Tool, + \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + \:sup:`5` \ TC-Gen + + + + + | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, + \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + \:sup:`5` \ TC-Gen + + From 5b6e0473e16a84cc7a194204acaa4aaabc5e22de Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 13:36:50 -0600 Subject: [PATCH 067/770] fixing formatting #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 7b71d9044c..37a83b3f40 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -235,7 +235,7 @@ METplus Database of Statistics ALPHA | **Point-Stat line type:** Error percent value used in confidence - intervals \ :sup:`2` \ + | intervals \ :sup:`2` \ | **grid-stat line type:** Error percent value used in confidence | intervals \ :sup:`3` \ | **wavelet-stat line type:** NA in Wavelet-Stat \ :sup:`4` \ @@ -249,7 +249,7 @@ METplus Database of Statistics - | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, + *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, \:sup:`5` \ TC-Gen From 5b67cca3b71bcea295fa3763cf3f2a32ad9b53b4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 13:44:35 -0600 Subject: [PATCH 068/770] fixing formatting #2 #1049 --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 37a83b3f40..13e129b85d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -243,14 +243,14 @@ METplus Database of Statistics | intervals \ :sup:`5` \ | | *Tools:* \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - \:sup:`5` \ TC-Gen + | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + | \:sup:`5` \ TC-Gen *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - \:sup:`5` \ TC-Gen + \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + \:sup:`5` \ TC-Gen From 1622c767fb7c7c017f7c9bf27adfabd570156025 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 13:51:55 -0600 Subject: [PATCH 069/770] removing new entries to make it run #1049 --- docs/Users_Guide/statistics_list.rst | 59 ---------------------------- 1 file changed, 59 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 13e129b85d..ddcb495f37 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -195,62 +195,3 @@ METplus Database of Statistics | bootstrap upper and lower confidence limits | | *Tool:* Grid-Stat Tool - - AGEN_DLAND - | **GENMPR line type:** Forecast genesis event distance to land (nm) - | - | *Tool*: TC-Gen - - AGEN_FHR - | **GENMPR line type:** Forecast hour of genesis event - | - | *Tool*: TC-Gen - - AGEN_INIT - | **GENMPR line type:** Forecast initialization time - | - | *Tool*: TC-Gen - - AGEN_LAT - | **GENMPR line type:** Latitude position of the forecast genesis event - | - | *Tool*: TC-Gen - - AGEN_LON - | **GENMPR line type:** Longitude position of the forecast genesis event - | - | *Tool*: TC-Gen - - ALAT - | **TCMPR line type:** Latitude position of adeck model - | **PROBRIRW line type:** Latitude position of edeck model - | - | *Tool*: TC-Pairs - - ALON - | **TCMPR line type:** Longitude position of adeck model - | **PROBRIRW line type:** Longitude position of edeck model - | - | *Tool*: TC-Pairs - - ALPHA - | **Point-Stat line type:** Error percent value used in confidence - | intervals \ :sup:`2` \ - | **grid-stat line type:** Error percent value used in confidence - | intervals \ :sup:`3` \ - | **wavelet-stat line type:** NA in Wavelet-Stat \ :sup:`4` \ - | **TC-Gen line type:** Error percent value used in confidence - | intervals \ :sup:`5` \ - | - | *Tools:* \ :sup:`2` \ Point-Stat Tool, - | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - | \:sup:`5` \ TC-Gen - - - - - *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - \:sup:`5` \ TC-Gen - - From 39d4564d25bdcb1226a7c028b66ddc7f7d0e591d Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 13:58:18 -0600 Subject: [PATCH 070/770] adding a couple back in #1049 --- docs/Users_Guide/statistics_list.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ddcb495f37..0c99c1172a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -195,3 +195,29 @@ METplus Database of Statistics | bootstrap upper and lower confidence limits | | *Tool:* Grid-Stat Tool + + AGEN_DLAND + | **GENMPR line type:** Forecast genesis event distance to land (nm) + | + | *Tool*: TC-Gen + + AGEN_FHR + | **GENMPR line type:** Forecast hour of genesis event + | + | *Tool*: TC-Gen + + AGEN_INIT + | **GENMPR line type:** Forecast initialization time + | + | *Tool*: TC-Gen + + AGEN_LAT + | **GENMPR line type:** Latitude position of the forecast genesis event + | + | *Tool*: TC-Gen + + AGEN_LON + | **GENMPR line type:** Longitude position of the forecast genesis event + | + | *Tool*: TC-Gen + From 92bf9ee9eda1fef76c666de54a3ea3284b7ff39a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 15:22:39 -0600 Subject: [PATCH 071/770] adding a couple more back in #1049 --- docs/Users_Guide/statistics_list.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0c99c1172a..d7c4af2ecb 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -221,3 +221,30 @@ METplus Database of Statistics | | *Tool*: TC-Gen + ALAT + | **TCMPR line type:** Latitude position of adeck model + | **PROBRIRW line type:** Latitude position of edeck model + | + | *Tool*: TC-Pairs + + ALON + | **TCMPR line type:** Longitude position of adeck model + | **PROBRIRW line type:** Longitude position of edeck model + | + | *Tool*: TC-Pairs + + ALPHA + | **Point-Stat line type:** Error percent value used in confidence + | intervals \ :sup:`2` \ + | **grid-stat line type:** Error percent value used in confidence + | intervals \ :sup:`3` \ + | **wavelet-stat line type:** NA in Wavelet-Stat \ :sup:`4` \ + | **TC-Gen line type:** Error percent value used in confidence + | intervals \ :sup:`5` \ + | + | *Tools:* \ :sup:`2` \ Point-Stat Tool, + | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + | \:sup:`5` \ TC-Gen + + + From 64897739324346a277658a47d24bdf01982f6e97 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 15:29:35 -0600 Subject: [PATCH 072/770] tool key at the bottom #1049 --- docs/Users_Guide/statistics_list.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index d7c4af2ecb..a4015344f0 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -246,5 +246,11 @@ METplus Database of Statistics | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, | \:sup:`5` \ TC-Gen + Key for Tools + | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, + | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + | \:sup:`5` \ TC-Gen + + From f7ccc85920a0f6a66984cef32e148a930f6bb059 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 15:37:31 -0600 Subject: [PATCH 073/770] spacing changes #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index a4015344f0..37eabc1854 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -248,8 +248,8 @@ METplus Database of Statistics Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - | \:sup:`5` \ TC-Gen + \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + \:sup:`5` \ TC-Gen From 16d7f15d6c137629a20bb10d1fbe5e3aaa0f64a4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 15:41:16 -0600 Subject: [PATCH 074/770] spacing changes another try #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 37eabc1854..c0a61ea035 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -248,8 +248,8 @@ METplus Database of Statistics Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - \:sup:`5` \ TC-Gen + \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, + \:sup:`5` \ TC-Gen From 8bcf816d644b53ba1a3b48c5091318bb1c82dcf7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 15:46:05 -0600 Subject: [PATCH 075/770] fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c0a61ea035..98b8384ad1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -243,13 +243,13 @@ METplus Database of Statistics | intervals \ :sup:`5` \ | | *Tools:* \ :sup:`2` \ Point-Stat Tool, - | \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - | \:sup:`5` \ TC-Gen + | \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, + | \ :sup:`5` \ TC-Gen Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \:sup:`4` \ Wavelet-Stat Tool, - \:sup:`5` \ TC-Gen + \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, + \ :sup:`5` \ TC-Gen From 46cc69a5a5277b71244652d65bd4cd41137a773e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 15:54:25 -0600 Subject: [PATCH 076/770] fixing spacing attempt 2 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 98b8384ad1..69892191e7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -248,8 +248,8 @@ METplus Database of Statistics Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, - \ :sup:`5` \ TC-Gen + \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, + \ :sup:`5` \ TC-Gen From 1af5734ec2fce87d7b5f37eee06dc7912aec28cc Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 8 Sep 2021 16:00:06 -0600 Subject: [PATCH 077/770] fixing spacing attempt 3 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 69892191e7..e9847dcaa4 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -243,8 +243,8 @@ METplus Database of Statistics | intervals \ :sup:`5` \ | | *Tools:* \ :sup:`2` \ Point-Stat Tool, - | \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, - | \ :sup:`5` \ TC-Gen + \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, + \ :sup:`5` \ TC-Gen Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, From a9465273c57ac6601350119b9f0c4692501d813c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 9 Sep 2021 10:29:56 -0600 Subject: [PATCH 078/770] through AREA items #1049 --- docs/Users_Guide/statistics_list.rst | 76 +++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e9847dcaa4..3bcb64225e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -245,11 +245,83 @@ METplus Database of Statistics | *Tools:* \ :sup:`2` \ Point-Stat Tool, \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, \ :sup:`5` \ TC-Gen - + + ALTK_ERR + | **TCMPR line type:** Along track error (nm) + | + | *Tool:* TC-Pairs + + AMAX_WIND + | **TCMPR line type:** adeck maximum wind speed + | + | *Tool:* TC-Pairs + + AMODEL + | **TCST line type:** User provided text string designating model name + | + | *Tool:* TC-Pairs + + AMSLP + | **TCMPR line type:** adeck mean sea level pressure + | + | *Tool:* TC-Pairs + + ANGLE_DIFF + | **MODE ascii object:** Difference between the axis angles of + two objects (in degrees) + | + | *Tool:* MODE-Tool + + ANLY_USE + | **GSI diagnostic conventional MPR output:** Analysis usage (1 for + yes, -1 for no) + | + | *Tool:* GSI-Tool + + ANOM_CORR_UNCNTR + ANOM_CORR_UNCNTR_BCL + ANOM_CORR_UNCNTR_BCU + | **CNT line type:** The uncentered Anomaly Correlation excluding + mean error including bootstrap upper and lower confidence limits + | + | *Tool:* Point-Stat Tool + + ANOM_CORR + ANOM_CORR_NCL + ANOM_CORR_NCU + ANOM_CORR_BCL + ANOM_CORR_BCU + | **CNT line type:** The Anomaly Correlation including mean error + with normal and bootstrap upper and lower confidence limits + | + | *Tool:* Point-Stat Tool + + AREA + | **MODE ascii object:** Object area (in grid squares) \ :sup:`1` \ + | **MODE-time-domain 2D attribute output:** 2D cross-sectional + area \ :sup:`6` \ + | + | *Tool:* \ :sup:`1` \ MODE-Tool \ :sup:`6` \ MODE-time-domain + + AREA_RATIO + | **MODE ascii object:** The forecast object area divided by the + observation object area (unitless) + | **NOTE:** Prior to met-10.0.0, defined as the lesser of the + two object areas divided by the greater of the two" + | + | *Tool:* MODE-Tool + + AREA_THRESH + | **MODE ascii object:** Area of the object containing data values + in the raw field that meet the object definition threshold + criteria (in grid squares) + | + | *Tool:* MODE-Tool + Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, - \ :sup:`5` \ TC-Gen + \ :sup:`5` \ TC-Gen, \ :sup:`6` \ MODE-time-domain From b4ca478ce4814f0212e0c049d57b9358b7692cc6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 9 Sep 2021 10:36:17 -0600 Subject: [PATCH 079/770] AMODEL listed twice but it's not #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 3bcb64225e..0f6682f5ef 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -255,7 +255,8 @@ METplus Database of Statistics | **TCMPR line type:** adeck maximum wind speed | | *Tool:* TC-Pairs - + + AMODEL | **TCST line type:** User provided text string designating model name | From 74ca4bd6cc8d7e17fa57373435c9bbb25119d4a9 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 9 Sep 2021 10:40:43 -0600 Subject: [PATCH 080/770] AMODEL listed twice but it's not #2 #1049 --- docs/Users_Guide/statistics_list.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0f6682f5ef..ed9f92c00f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -256,8 +256,7 @@ METplus Database of Statistics | | *Tool:* TC-Pairs - - AMODEL + AMODEL | **TCST line type:** User provided text string designating model name | | *Tool:* TC-Pairs From 1e48cb122a7524efea7f14d1cc4113c1b51eb3c3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 9 Sep 2021 10:45:01 -0600 Subject: [PATCH 081/770] AMODEL added question marks so it will run #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ed9f92c00f..cacedea27a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -256,7 +256,7 @@ METplus Database of Statistics | | *Tool:* TC-Pairs - AMODEL + AMODEL??? | **TCST line type:** User provided text string designating model name | | *Tool:* TC-Pairs From fb601d33fd298da1c67900e8fa18f64c3a023b67 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 9 Sep 2021 13:57:27 -0600 Subject: [PATCH 082/770] final A entries #1049 --- docs/Users_Guide/statistics_list.rst | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index cacedea27a..0936aa0344 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -307,7 +307,7 @@ METplus Database of Statistics | **MODE ascii object:** The forecast object area divided by the observation object area (unitless) | **NOTE:** Prior to met-10.0.0, defined as the lesser of the - two object areas divided by the greater of the two" + two object areas divided by the greater of the two | | *Tool:* MODE-Tool @@ -317,6 +317,33 @@ METplus Database of Statistics criteria (in grid squares) | | *Tool:* MODE-Tool + + ASPECT_DIFF + | **MODE ascii object:** Absolute value of the difference between + the aspect ratios of two objects (unitless) + | + | *Tool:* MODE-Tool + + AWIND_END + | **PROBRIRW line type:** Forecast maximum wind speed at RI end + | + | *Tool:* TC-Pairs + + AXIS_ANG + | **MODE ascii object:** Object axis angle (in degrees) \ :sup:`1` \ + | **MODE-time-domain 2D attribute output:** Angle that the axis + makes with the grid x direction \ :sup:`6` \ + | **MODE-time-domain 3D attribute output:** Angle that the axis plane + of an object makes with the grid x direction \ :sup:`6` \ + | + | *Tool:* \ :sup:`1` \ MODE-Tool \ :sup:`6` \ MODE-time-domain + + AXIS_DIFF + | **MODE-time-domain 3D pair attribute output:** Difference in spatial + axis plane angles + | + | *Tool:* MODE-time-domain + Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, From bab2029ebe8c698f90a3feb8697cbe67ce377bca Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 17 Sep 2021 10:40:51 -0600 Subject: [PATCH 083/770] naming the glossary statistics in hopes of not conflicting with original glossary #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0936aa0344..f28f3f0df5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,7 +2,7 @@ METplus Database of Statistics ****************************** -.. glossary:: +.. glossary:: statistics :sorted: 2D Objects From 71ef935bd80d64e0422c24cd622fb128d2aac473 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 17 Sep 2021 10:46:25 -0600 Subject: [PATCH 084/770] removing glossary statistics space #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f28f3f0df5..ea8aabb180 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,7 +2,7 @@ METplus Database of Statistics ****************************** -.. glossary:: statistics +.. glossary::statistics :sorted: 2D Objects From 7978549a344890ecd7a2dbc0ae8db6429b6eb715 Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Fri, 17 Sep 2021 11:04:30 -0600 Subject: [PATCH 085/770] Testing multiple glossary names --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ea8aabb180..f28f3f0df5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,7 +2,7 @@ METplus Database of Statistics ****************************** -.. glossary::statistics +.. glossary:: statistics :sorted: 2D Objects From f9fd1dac485b980ea53072c92d8612ba29b01c76 Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Fri, 17 Sep 2021 11:26:41 -0600 Subject: [PATCH 086/770] Per #1067, working on attempts to have multiple glossaries with the same term --- docs/Users_Guide/statistics_list.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f28f3f0df5..b1d3d0341c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -4,7 +4,7 @@ METplus Database of Statistics .. glossary:: statistics :sorted: - + 2D Objects | **For each Object:** | Location of the centroid in grid units @@ -256,7 +256,8 @@ METplus Database of Statistics | | *Tool:* TC-Pairs - AMODEL??? + + :term:`AMODEL` | **TCST line type:** User provided text string designating model name | | *Tool:* TC-Pairs From cd8afca57298c2fa25c081ab6c7aed60e45fa6d0 Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Fri, 17 Sep 2021 11:31:31 -0600 Subject: [PATCH 087/770] Per #1067, working on attempts to have multiple glossaries with the same term --- docs/Users_Guide/glossary.rst | 3 ++- docs/Users_Guide/statistics_list.rst | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index c2a874e28f..0d90d87253 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -2,7 +2,8 @@ METplus Configuration Glossary ****************************** -.. glossary:: +.. glossary:: configuation + :priority: main :sorted: REGRID_DATA_PLANE_ONCE_PER_FIELD diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b1d3d0341c..b4d2136f7c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,6 +3,7 @@ METplus Database of Statistics ****************************** .. glossary:: statistics + :priority: secondary :sorted: 2D Objects @@ -256,7 +257,7 @@ METplus Database of Statistics | | *Tool:* TC-Pairs - + AMODEL :term:`AMODEL` | **TCST line type:** User provided text string designating model name | From f3df342a53b0c99cf149f2267da9ad5051d8daac Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Fri, 17 Sep 2021 11:36:20 -0600 Subject: [PATCH 088/770] Per #1067, working on attempts to have multiple glossaries with the same term --- docs/Users_Guide/glossary.rst | 3 +-- docs/Users_Guide/statistics_list.rst | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 0d90d87253..c2a874e28f 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -2,8 +2,7 @@ METplus Configuration Glossary ****************************** -.. glossary:: configuation - :priority: main +.. glossary:: :sorted: REGRID_DATA_PLANE_ONCE_PER_FIELD diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b4d2136f7c..49ff7fca8c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,6 @@ METplus Database of Statistics ****************************** .. glossary:: statistics - :priority: secondary :sorted: 2D Objects From b419916800edbeff8e4eb21c806faa834292d114 Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Fri, 17 Sep 2021 11:43:35 -0600 Subject: [PATCH 089/770] Per #1067, removing code with attempts at mutiple glossaries --- docs/Users_Guide/statistics_list.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 49ff7fca8c..b36a6a703f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,7 +2,7 @@ METplus Database of Statistics ****************************** -.. glossary:: statistics +.. glossary:: :sorted: 2D Objects @@ -257,7 +257,6 @@ METplus Database of Statistics | *Tool:* TC-Pairs AMODEL - :term:`AMODEL` | **TCST line type:** User provided text string designating model name | | *Tool:* TC-Pairs From 9b86f04d133e3c774646e4bafa911b6f8fcb5984 Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Fri, 17 Sep 2021 11:47:31 -0600 Subject: [PATCH 090/770] Per #1067, adding back three question marks due to duplicate term --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b36a6a703f..85ecdd6fd1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -256,7 +256,7 @@ METplus Database of Statistics | | *Tool:* TC-Pairs - AMODEL + AMODEL??? | **TCST line type:** User provided text string designating model name | | *Tool:* TC-Pairs From d821584bfcdb45b5fd7d4597a9628fbce63387b1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 11:55:36 -0600 Subject: [PATCH 091/770] paring down list, adding a table for review --- docs/Users_Guide/statistics_list.rst | 347 ++------------------------- 1 file changed, 19 insertions(+), 328 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ea8aabb180..c65b862748 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,160 +2,26 @@ METplus Database of Statistics ****************************** -.. glossary::statistics +.. list-table:: Statistics List + :widths: auto + :header-rows: 1 + + * - Statistics Long Name + - METplus Name + - Statistic Type + - Tools + - Statistics + * - Accuracy + - ACC + - Categorical + - Point-Stat Tool & MODE Tool + - CTS, MCTS, NBRCTS + MODE output format: Accuracy + + + +.. glossary:: statistics :sorted: - - 2D Objects - | **For each Object:** - | Location of the centroid in grid units - | Location of the centroid in lat/lon degrees - | Axis angle, Length of the enclosing rectangle - | Width of the enclosing rectangle - | Object area - | Radius of curvature of the object defined in terms of third order - moments - | Center of curvature - | Ratio of the difference between the area of an object and the area - of its convex hull divided by the area of the complex hull - | percentiles of intensity of the raw field within the object - | Percentile of intensity chosen for use in the percentile intensity - ratio - | Sum of the intensities of the raw field within the object - | - | **For paired objects:** - Distance between two objects centroids, Minimum distance between the - boundaries of two objects - | Minimum distance between the convex hulls of two objects - | Difference between the axis angles of two objects - | Ratio of the areas of two objects - | Intersection area of two objects - | Union area of two objects - | Symmetric difference of two objects - | Ratio of intersection areas - | Ratio of complexities - | Ratio of the nth percentile of intensity - | Total interest value computed for a pair of simple objects - | NetCDF files with the objects and raw data for further processing - - | *Tool:* See the WWRP/WGNE JWGFVR website for more details: - | https://www.cawcr.gov.au/projects/verification - - A/BAL_WIND_34 - **TCMPR line type**: a/bdeck 34-knot radius winds in full circle - - | *Tool:* TC-Pairs - - A/BAL_WIND_50 - **TCMPR line type**: a/bdeck 50-knot radius winds in full circle - - | *Tool:* TC-Pairs - - A/BAL_WIND_64 - **TCMPR line type**: a/bdeck 64-knot radius winds in full circle - - | *Tool:* TC-Pairs - - A/BDEPTH - **TCMPR line type**: system depth, D-deep, M-medium, S-shallow, X-unknown - - | *Tool:* TC-Pairs - - A/BDIR - **TCMPR line type**: storm direction in compass coordinates, 0 - 359 - degrees - - | *Tool:* TC-Pairs - - A/BEYE - **TCMPR line type**: eye diameter, 0 through 999 nm - - | *Tool:* TC-Pairs - - A/BGUSTS - **TCMPR line type**: gusts, 0 through 995 kts - - | *Tool:* TC-Pairs - - A/BMRD - **TCMPR line type**: radius of max winds, 0 - 999 nm - - | *Tool:* TC-Pairs - - A/BNE_WIND_34 - **TCMPR line type**: a/bdeck 34-knot radius winds in NE quadrant - - | *Tool:* TC-Pairs - - A/BNE_WIND_50 - **TCMPR line type**: a/bdeck 50-knot radius winds in NE quadrant - - | *Tool:* TC-Pairs - - A/BNE_WIND_64 - **TCMPR line type**: a/bdeck 64-knot radius winds in NE quadrant - - | *Tool:* TC-Pairs - - A/BNW_WIND_34 - **TCMPR line type**: a/bdeck 34-knot radius winds in NW quadrant - - | *Tool:* TC-Pairs - - A/BNW_WIND_50 - **TCMPR line type**: a/bdeck 50-knot radius winds in NW quadrant - - | *Tool:* TC-Pairs - - A/BNW_WIND_64 - **TCMPR line type**: a/bdeck 64-knot radius winds in NW quadrant - - | *Tool:* TC-Pairs - - A/BRADP - **TCMPR line type**: pressure in millibars of the last closed isobar, - 900 - 1050 mb - - | *Tool:* TC-Pairs - - A/BRRP - **TCMPR line type**: radius of the last closed isobar in nm, 0 - 9999 nm - - | *Tool:* TC-Pairs - - A/BSE_WIND_34 - **TCMPR line type:** a/bdeck 34-knot radius winds in SE quadrant - - | *Tool:* TC-Pairs - - A/BSE_WIND_50 - **TCMPR line type:** a/bdeck 50-knot radius winds in SE quadrant - - | *Tool:* TC-Pairs - - A/BSE_WIND_64 - **TCMPR line type:** a/bdeck 64-knot radius winds in SE quadrant - - | *Tool:* TC-Pairs - - A/BSPEED - **TCMPR line type:** storm speed, 0 - 999 kts - - | *Tool:* TC-Pairs - - A/BSW_WIND_34 - **TCMPR line type:** a/bdeck 34-knot radius winds in SW quadrant - - | *Tool:* TC-Pairs - - A/BSW_WIND_50 - **TCMPR line type:** a/bdeck 50-knot radius winds in SW quadrant - - | *Tool:* TC-Pairs - - A/BSW_WIND_64 - **TCMPR line type:** a/bdeck 64-knot radius winds in SW quadrant - - | *Tool:* TC-Pairs ACC | **MODE line type**: Accuracy \ :sup:`1` @@ -169,181 +35,6 @@ METplus Database of Statistics | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool - ACC_NCL - ACC_NCU - ACC_BCL - ACC_BCU - | **CTS line type:** Accuracy including normal and bootstrap upper and - | lower confidence limits \ :sup:`2` - | **MCTS line type:** Accuracy, normal confidence limits and bootstrap - | confidence limits \ :sup:`2` - | **NBRCTCS line type:** Accuracy including normal and bootstrap upper - | and lower confidence limits \ :sup:`3` - | - | *Tools:* \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool - - ADLAND - | **TCMPR line type:** adeck distance to land (nm) - | **PROBRIRW line type:** adeck distance to land (nm) - | - | *Tool:* TC-Pairs - - AFSS - AFSS_BCL - AFSS_BCU - | **NBRCNT line type:** Asymptotic Fractions Skill Score including - | bootstrap upper and lower confidence limits - | - | *Tool:* Grid-Stat Tool - - AGEN_DLAND - | **GENMPR line type:** Forecast genesis event distance to land (nm) - | - | *Tool*: TC-Gen - - AGEN_FHR - | **GENMPR line type:** Forecast hour of genesis event - | - | *Tool*: TC-Gen - - AGEN_INIT - | **GENMPR line type:** Forecast initialization time - | - | *Tool*: TC-Gen - - AGEN_LAT - | **GENMPR line type:** Latitude position of the forecast genesis event - | - | *Tool*: TC-Gen - - AGEN_LON - | **GENMPR line type:** Longitude position of the forecast genesis event - | - | *Tool*: TC-Gen - - ALAT - | **TCMPR line type:** Latitude position of adeck model - | **PROBRIRW line type:** Latitude position of edeck model - | - | *Tool*: TC-Pairs - - ALON - | **TCMPR line type:** Longitude position of adeck model - | **PROBRIRW line type:** Longitude position of edeck model - | - | *Tool*: TC-Pairs - - ALPHA - | **Point-Stat line type:** Error percent value used in confidence - | intervals \ :sup:`2` \ - | **grid-stat line type:** Error percent value used in confidence - | intervals \ :sup:`3` \ - | **wavelet-stat line type:** NA in Wavelet-Stat \ :sup:`4` \ - | **TC-Gen line type:** Error percent value used in confidence - | intervals \ :sup:`5` \ - | - | *Tools:* \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, - \ :sup:`5` \ TC-Gen - - ALTK_ERR - | **TCMPR line type:** Along track error (nm) - | - | *Tool:* TC-Pairs - - AMAX_WIND - | **TCMPR line type:** adeck maximum wind speed - | - | *Tool:* TC-Pairs - - AMODEL??? - | **TCST line type:** User provided text string designating model name - | - | *Tool:* TC-Pairs - - AMSLP - | **TCMPR line type:** adeck mean sea level pressure - | - | *Tool:* TC-Pairs - - ANGLE_DIFF - | **MODE ascii object:** Difference between the axis angles of - two objects (in degrees) - | - | *Tool:* MODE-Tool - - ANLY_USE - | **GSI diagnostic conventional MPR output:** Analysis usage (1 for - yes, -1 for no) - | - | *Tool:* GSI-Tool - - ANOM_CORR_UNCNTR - ANOM_CORR_UNCNTR_BCL - ANOM_CORR_UNCNTR_BCU - | **CNT line type:** The uncentered Anomaly Correlation excluding - mean error including bootstrap upper and lower confidence limits - | - | *Tool:* Point-Stat Tool - - ANOM_CORR - ANOM_CORR_NCL - ANOM_CORR_NCU - ANOM_CORR_BCL - ANOM_CORR_BCU - | **CNT line type:** The Anomaly Correlation including mean error - with normal and bootstrap upper and lower confidence limits - | - | *Tool:* Point-Stat Tool - - AREA - | **MODE ascii object:** Object area (in grid squares) \ :sup:`1` \ - | **MODE-time-domain 2D attribute output:** 2D cross-sectional - area \ :sup:`6` \ - | - | *Tool:* \ :sup:`1` \ MODE-Tool \ :sup:`6` \ MODE-time-domain - - AREA_RATIO - | **MODE ascii object:** The forecast object area divided by the - observation object area (unitless) - | **NOTE:** Prior to met-10.0.0, defined as the lesser of the - two object areas divided by the greater of the two - | - | *Tool:* MODE-Tool - - AREA_THRESH - | **MODE ascii object:** Area of the object containing data values - in the raw field that meet the object definition threshold - criteria (in grid squares) - | - | *Tool:* MODE-Tool - - ASPECT_DIFF - | **MODE ascii object:** Absolute value of the difference between - the aspect ratios of two objects (unitless) - | - | *Tool:* MODE-Tool - - AWIND_END - | **PROBRIRW line type:** Forecast maximum wind speed at RI end - | - | *Tool:* TC-Pairs - - AXIS_ANG - | **MODE ascii object:** Object axis angle (in degrees) \ :sup:`1` \ - | **MODE-time-domain 2D attribute output:** Angle that the axis - makes with the grid x direction \ :sup:`6` \ - | **MODE-time-domain 3D attribute output:** Angle that the axis plane - of an object makes with the grid x direction \ :sup:`6` \ - | - | *Tool:* \ :sup:`1` \ MODE-Tool \ :sup:`6` \ MODE-time-domain - - AXIS_DIFF - | **MODE-time-domain 3D pair attribute output:** Difference in spatial - axis plane angles - | - | *Tool:* MODE-time-domain - Key for Tools | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, From 61122bc3ca022c7850d43100d0924a91a69ed5c4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 13:09:50 -0600 Subject: [PATCH 092/770] problems with table --- docs/Users_Guide/statistics_list.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index a4d6f3f38f..f8387384e8 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -4,8 +4,7 @@ METplus Database of Statistics .. list-table:: Statistics List :widths: auto - :header-rows: 1 - + :header-rows: 1 * - Statistics Long Name - METplus Name - Statistic Type From d274f31f7052febec23c438a60f52cd6e9606927 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 13:13:57 -0600 Subject: [PATCH 093/770] still trying to get it to publish --- docs/Users_Guide/statistics_list.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f8387384e8..2b114fc4df 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -4,7 +4,8 @@ METplus Database of Statistics .. list-table:: Statistics List :widths: auto - :header-rows: 1 + :header-rows: 1 + * - Statistics Long Name - METplus Name - Statistic Type @@ -19,8 +20,8 @@ METplus Database of Statistics -.. glossary:: statistics - :sorted: +#.. glossary:: statistics +# :sorted: ACC | **MODE line type**: Accuracy \ :sup:`1` From bec500fc884a9e3f87e4c72623896750d5a358f7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 13:23:45 -0600 Subject: [PATCH 094/770] still trying to get a second line in the table --- docs/Users_Guide/statistics_list.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2b114fc4df..f27d8be050 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,6 +2,9 @@ METplus Database of Statistics ****************************** +.. role:: raw-html(raw) + :format: html + .. list-table:: Statistics List :widths: auto :header-rows: 1 From 87b82c640a8c457186a5b1cb79773b453eae4a78 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 13:32:18 -0600 Subject: [PATCH 095/770] still trying to get a second line in the table 2 --- docs/Users_Guide/statistics_list.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f27d8be050..2813797ba6 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -18,8 +18,7 @@ METplus Database of Statistics - ACC - Categorical - Point-Stat Tool & MODE Tool - - CTS, MCTS, NBRCTS - MODE output format: Accuracy + - CTS, MCTS, NBRCTS :raw-html:`
` MODE output format: Accuracy From 7148139e5496f7c717376e33569d0d4b112019a9 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 13:59:46 -0600 Subject: [PATCH 096/770] testing different formatting --- docs/Users_Guide/statistics_list.rst | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2813797ba6..9ec781d20e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -26,13 +26,8 @@ METplus Database of Statistics # :sorted: ACC - | **MODE line type**: Accuracy \ :sup:`1` - | **CTS line type**: Accuracy including normal and bootstrap - | upper and lower confidence limits \ :sup:`2,3` - | **MCTS line type**: Accuracy, normal confidence limits and bootstrap - | confidence limits \ :sup:`2,3` - | **NBRCTCS line type**: Accuracy including normal and bootstrap upper - | and lower confidence limits \ :sup:`3` + | **Statistics Long Name**: Accuracy \:sup:`1` + | **Statistic Type & Tool**: CTS \ :sup:`2,3`, MCTS \ :sup:`2,3` & NBRCTCS \ :sup:`3` | | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool From 7aa39f39acc9dd55645badcbfe382f79ad626fa1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 14:09:39 -0600 Subject: [PATCH 097/770] testing different formatting --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 9ec781d20e..40170389c9 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -27,7 +27,8 @@ METplus Database of Statistics ACC | **Statistics Long Name**: Accuracy \:sup:`1` - | **Statistic Type & Tool**: CTS \ :sup:`2,3`, MCTS \ :sup:`2,3` & NBRCTCS \ :sup:`3` + | **Statistic Type**: Categorical + | **Tools**: CTS \ :sup:`2,3`, MCTS \ :sup:`2,3`, NBRCTCS \ :sup:`3` & MODE output format: Accuracy \ :sup:`1` | | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool From 6ac43ac3e9aa7ab9592795047deb1a3857441f93 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 14:11:37 -0600 Subject: [PATCH 098/770] trying to comment out the glossary inner workings --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 40170389c9..2d2735c7ee 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -22,8 +22,8 @@ METplus Database of Statistics -#.. glossary:: statistics -# :sorted: +..#.. glossary:: statistics +..# :sorted: ACC | **Statistics Long Name**: Accuracy \:sup:`1` From c583c2103f14334b0b47bdf46adf01ba06b3eda6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 12 Oct 2021 14:14:45 -0600 Subject: [PATCH 099/770] formatting again --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2d2735c7ee..03f280a6b7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -28,7 +28,7 @@ METplus Database of Statistics ACC | **Statistics Long Name**: Accuracy \:sup:`1` | **Statistic Type**: Categorical - | **Tools**: CTS \ :sup:`2,3`, MCTS \ :sup:`2,3`, NBRCTCS \ :sup:`3` & MODE output format: Accuracy \ :sup:`1` + | **Tools & Statistics**: CTS \ :sup:`2,3`, MCTS \ :sup:`2,3`, NBRCTCS \ :sup:`3` & MODE output format: Accuracy \ :sup:`1` | | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool & \ :sup:`3` \ Grid-Stat Tool From 7f968fa18b86ce74f3a5ccb07a89cde1b175d05d Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 13 Oct 2021 11:07:33 -0600 Subject: [PATCH 100/770] line breaks in table #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 03f280a6b7..32cd2e95c5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -9,7 +9,7 @@ METplus Database of Statistics :widths: auto :header-rows: 1 - * - Statistics Long Name + * - Statistics :raw-html:`
` Long Name - METplus Name - Statistic Type - Tools @@ -17,7 +17,7 @@ METplus Database of Statistics * - Accuracy - ACC - Categorical - - Point-Stat Tool & MODE Tool + - Point-Stat Tool :raw-html:`
` & MODE Tool - CTS, MCTS, NBRCTS :raw-html:`
` MODE output format: Accuracy From 4e2f63c6c196626fc1ad513ea0d25abe4a7d4c4a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 13 Oct 2021 15:54:32 -0600 Subject: [PATCH 101/770] adding in some more for an example #1049 --- docs/Users_Guide/statistics_list.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 32cd2e95c5..17fd51242d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -19,7 +19,21 @@ METplus Database of Statistics - Categorical - Point-Stat Tool :raw-html:`
` & MODE Tool - CTS, MCTS, NBRCTS :raw-html:`
` MODE output format: Accuracy - + * - Asymptotic Fractions Skill Score + - AFSS + - + - Grid-Stat Tool + - NBRCNT + * - Along track error (nm) + - ALTK_ERR + - + - TC-Pairs Tool + - TCMPR + * - + - ANGLE_DIFF + - + - MODE Tool + - MODE ..#.. glossary:: statistics From eddfeabc483b4b3efcd1ea32cbeb6f2177c80ca1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 18 Oct 2021 10:17:15 -0600 Subject: [PATCH 102/770] adding new entries in through AREA #1049 --- docs/Users_Guide/statistics_list.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 17fd51242d..55c6305d90 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -29,12 +29,26 @@ METplus Database of Statistics - - TC-Pairs Tool - TCMPR - * - + * - Difference between the axis angles of two objects (in degrees) - ANGLE_DIFF - - MODE Tool - MODE - + * - The Anomaly Correlation including mean error with normal and bootstrap upper and lower confidence limits + - ANOM_CORR + - + - Point-Stat, Grid-Stat, Series-Analysis, Stat-Analysis + - CNT + * - The uncentered Anomaly Correlation excluding mean error including bootstrap upper and lower confidence limits + - ANOM_CORR_UNCNTR + - + - Point-Stat, Grid-Stat, Series-Analysis, Stat-Analysis + - CNT + * - Object area (in grid squares) + - AREA + - + - MODE and MTD + - MODE ascii object ..#.. glossary:: statistics ..# :sorted: From c8a46e6e88f2b28e0739cb64447eb756b07d5a42 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 18 Oct 2021 10:26:04 -0600 Subject: [PATCH 103/770] fixing line breaks #1049 --- docs/Users_Guide/statistics_list.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 55c6305d90..509e1fa7d4 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -17,7 +17,7 @@ METplus Database of Statistics * - Accuracy - ACC - Categorical - - Point-Stat Tool :raw-html:`
` & MODE Tool + - Point-Stat Tool :raw-html:`
` MODE Tool - CTS, MCTS, NBRCTS :raw-html:`
` MODE output format: Accuracy * - Asymptotic Fractions Skill Score - AFSS @@ -29,20 +29,20 @@ METplus Database of Statistics - - TC-Pairs Tool - TCMPR - * - Difference between the axis angles of two objects (in degrees) + * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) - ANGLE_DIFF - - MODE Tool - MODE - * - The Anomaly Correlation including mean error with normal and bootstrap upper and lower confidence limits + * - The Anomaly Correlation :raw-html:`
` including mean error with normal :raw-html:`
` and bootstrap upper and :raw-html:`
` lower confidence limits - ANOM_CORR - - - Point-Stat, Grid-Stat, Series-Analysis, Stat-Analysis + - Point-Stat, Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` Stat-Analysis - CNT - * - The uncentered Anomaly Correlation excluding mean error including bootstrap upper and lower confidence limits + * - The uncentered Anomaly Correlation :raw-html:`
` excluding mean error including :raw-html:`
` bootstrap upper and lower :raw-html:`
` confidence limits - ANOM_CORR_UNCNTR - - - Point-Stat, Grid-Stat, Series-Analysis, Stat-Analysis + - Point-Stat, Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` Stat-Analysis - CNT * - Object area (in grid squares) - AREA From af56292021db8a9fce0c9f8ea3c94b86aa20df07 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 18 Oct 2021 10:36:31 -0600 Subject: [PATCH 104/770] fixing line breaks #1049 #2 --- docs/Users_Guide/statistics_list.rst | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 509e1fa7d4..b56fec32c8 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -34,15 +34,26 @@ METplus Database of Statistics - - MODE Tool - MODE - * - The Anomaly Correlation :raw-html:`
` including mean error with normal :raw-html:`
` and bootstrap upper and :raw-html:`
` lower confidence limits + * - The Anomaly Correlation :raw-html:`
` + including mean error with normal :raw-html:`
` + and bootstrap upper and :raw-html:`
` + lower confidence limits - ANOM_CORR - - - Point-Stat, Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` Stat-Analysis + - Point-Stat, Grid-Stat :raw-html:`
` + Series-Analysis :raw-html:`
` + Stat-Analysis - CNT - * - The uncentered Anomaly Correlation :raw-html:`
` excluding mean error including :raw-html:`
` bootstrap upper and lower :raw-html:`
` confidence limits - - ANOM_CORR_UNCNTR + * - The uncentered Anomaly Correlation :raw-html:`
` + excluding mean error including :raw-html:`
` + bootstrap upper and lower :raw-html:`
` + confidence limits + - ANOM_CORR :raw-html:`
` _UNCNTR - - - Point-Stat, Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` Stat-Analysis + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Series-Analysis :raw-html:`
` + Stat-Analysis - CNT * - Object area (in grid squares) - AREA From dbd5f753c19de9b93642bbc412a2102ff5c1d1a0 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 18 Oct 2021 10:42:32 -0600 Subject: [PATCH 105/770] fixing line breaks and warning messages #1049 --- docs/Users_Guide/statistics_list.rst | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b56fec32c8..ff7d88e175 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -17,8 +17,10 @@ METplus Database of Statistics * - Accuracy - ACC - Categorical - - Point-Stat Tool :raw-html:`
` MODE Tool - - CTS, MCTS, NBRCTS :raw-html:`
` MODE output format: Accuracy + - Point-Stat Tool :raw-html:`
` + MODE Tool + - CTS, MCTS, NBRCTS :raw-html:`
` + MODE output format: Accuracy * - Asymptotic Fractions Skill Score - AFSS - @@ -29,15 +31,16 @@ METplus Database of Statistics - - TC-Pairs Tool - TCMPR - * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) + * - Difference between the axis :raw-html:`
` + angles of two objects (in degrees) - ANGLE_DIFF - - MODE Tool - MODE * - The Anomaly Correlation :raw-html:`
` - including mean error with normal :raw-html:`
` - and bootstrap upper and :raw-html:`
` - lower confidence limits + including mean error with normal :raw-html:`
` + and bootstrap upper and :raw-html:`
` + lower confidence limits - ANOM_CORR - - Point-Stat, Grid-Stat :raw-html:`
` @@ -45,9 +48,9 @@ METplus Database of Statistics Stat-Analysis - CNT * - The uncentered Anomaly Correlation :raw-html:`
` - excluding mean error including :raw-html:`
` - bootstrap upper and lower :raw-html:`
` - confidence limits + excluding mean error including :raw-html:`
` + bootstrap upper and lower :raw-html:`
` + confidence limits - ANOM_CORR :raw-html:`
` _UNCNTR - - Point-Stat :raw-html:`
` From f54f6aa3827f904d9f5dc8c6a6affe6972605c2a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 18 Oct 2021 10:50:41 -0600 Subject: [PATCH 106/770] fixing line breaks #1049 --- docs/Users_Guide/statistics_list.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ff7d88e175..86140efc8b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -19,7 +19,9 @@ METplus Database of Statistics - Categorical - Point-Stat Tool :raw-html:`
` MODE Tool - - CTS, MCTS, NBRCTS :raw-html:`
` + - CTS :raw-html:`
` + MCTS :raw-html:`
` + NBRCTS :raw-html:`
` MODE output format: Accuracy * - Asymptotic Fractions Skill Score - AFSS @@ -43,14 +45,15 @@ METplus Database of Statistics lower confidence limits - ANOM_CORR - - - Point-Stat, Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` Stat-Analysis - CNT - * - The uncentered Anomaly Correlation :raw-html:`
` - excluding mean error including :raw-html:`
` - bootstrap upper and lower :raw-html:`
` - confidence limits + * - The uncentered Anomaly :raw-html:`
` + Correlation excluding mean :raw-html:`
` + error including bootstrap upper :raw-html:`
` + and lower confidence limits - ANOM_CORR :raw-html:`
` _UNCNTR - - Point-Stat :raw-html:`
` @@ -61,7 +64,8 @@ METplus Database of Statistics * - Object area (in grid squares) - AREA - - - MODE and MTD + - MODE :raw-html:`
` + MTD - MODE ascii object ..#.. glossary:: statistics From b8c14d9303f5bc4f0636336ae4bd4a849f03e809 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 21 Oct 2021 15:54:13 -0600 Subject: [PATCH 107/770] adding AREA_RATIO through ASPECT_DIFF --- docs/Users_Guide/statistics_list.rst | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 86140efc8b..38728a17f4 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -67,6 +67,38 @@ METplus Database of Statistics - MODE :raw-html:`
` MTD - MODE ascii object + * - The forecast object area :raw-html:`
` + divided by the observation :raw-html:`
` + object area (unitless) :raw-html:`
` + NOTE: Prior to met-10.0.0, :raw-html:`
` + defined as the lesser of :raw-html:`
` + the two object areas :raw-html:`
` + divided by the greater :raw-html:`
` + of the two + - AREA_RATIO + - + - MODE Tool + - MODE ascii object: + * - Area of the object :raw-html:`
` + containing data values :raw-html:`
` + in the raw field :raw-html:`
` + that meet the object :raw-html:`
` + definition threshold :raw-html:`
+ criteria (in grid squares) + - AREA_THRESH + - + - MODE Tool + - MODE ascii object: + * - Absolute value of :raw-html:`
` + the difference :raw-html:`
` + between the aspect :raw-html:`
` + ratios of two objects :raw-html:`
` + (unitless) + - ASPECT_DIFF + - + - MODE Tool + - MODE ascii object: + ..#.. glossary:: statistics ..# :sorted: From 95506281a1ce237fa2ff86caaddd4ada28ffb90e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 21 Oct 2021 16:11:16 -0600 Subject: [PATCH 108/770] fixing typo #1049 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 38728a17f4..f05046ef0a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -83,7 +83,7 @@ METplus Database of Statistics containing data values :raw-html:`
` in the raw field :raw-html:`
` that meet the object :raw-html:`
` - definition threshold :raw-html:`
+ definition threshold :raw-html:`
` criteria (in grid squares) - AREA_THRESH - From 49136d9914e140c32f7c382b96b3f92062a64263 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 21 Oct 2021 16:16:36 -0600 Subject: [PATCH 109/770] fixing typo #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f05046ef0a..6c7552c42b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -78,7 +78,7 @@ METplus Database of Statistics - AREA_RATIO - - MODE Tool - - MODE ascii object: + - MODE ascii object * - Area of the object :raw-html:`
` containing data values :raw-html:`
` in the raw field :raw-html:`
` @@ -88,7 +88,7 @@ METplus Database of Statistics - AREA_THRESH - - MODE Tool - - MODE ascii object: + - MODE ascii object * - Absolute value of :raw-html:`
` the difference :raw-html:`
` between the aspect :raw-html:`
` From 6ced8a00b5327273d18cccf480c9ab29e5baddae Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 21 Oct 2021 16:26:26 -0600 Subject: [PATCH 110/770] fixing typo #1049 take 2 --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6c7552c42b..5c6285880a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -97,7 +97,7 @@ METplus Database of Statistics - ASPECT_DIFF - - MODE Tool - - MODE ascii object: + - MODE ascii object ..#.. glossary:: statistics From 2a7b085fb4286ef3f7ec3d7176a0805944b097c3 Mon Sep 17 00:00:00 2001 From: bikegeek <3753118+bikegeek@users.noreply.github.com> Date: Wed, 27 Oct 2021 15:48:05 -0600 Subject: [PATCH 111/770] Feature 1091 extent cycloneplotter (#1218) * Github #1091 add support to define plot's extent * Github #1091 Add support for defining plot's extent * Github #1091 add support for defining plot extent * GitHub #1091 Provide support to define plotting a bounding box of interest and replace deprecated cartopy attributes. * Github Issue #1091 Add configuration support to define the bounding box defining the area of interest to plot. * Github Issue #1091 provide support to define bounding box defining the area of interest to plot. * Github Issue #1091 Removed unused shapely imports * GItHub Issue #1091 Removed duplicate CYCLONE_PLOTTER_GLOBAL_PLOT * Github #1091 Include config settings needed to indicate the region of interest to be plotted. * Github Issue #1091 Keep only the North Hemisphere lon and lats in the config file to serve as example for plotting a specific area of the map. --- metplus/wrappers/cyclone_plotter_wrapper.py | 1228 +++++++++-------- .../CyclonePlotter/CyclonePlotter.conf | 31 + ...ter_fcstGFS_obsGFS_UserScript_ExtraTC.conf | 25 +- .../Plotter_fcstGFS_obsGFS_ExtraTC.conf | 22 +- 4 files changed, 742 insertions(+), 564 deletions(-) diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index 2497ff654f..5c177f6fc9 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -1,562 +1,668 @@ -"""!@namespace ExtraTropicalCyclonePlotter -A Python class that generates plots of extra tropical cyclone forecast data, - replicating the NCEP tropical and extra tropical cyclone tracks and - verification plots http://www.emc.ncep.noaa.gov/mmb/gplou/emchurr/glblgen/ -""" - -import os -import time -import datetime -import re -import sys -from collections import namedtuple - - -# handle if module can't be loaded to run wrapper -WRAPPER_CANNOT_RUN = False -EXCEPTION_ERR = '' -try: - import pandas as pd - import matplotlib.pyplot as plt - import matplotlib.ticker as mticker - import cartopy.crs as ccrs - import cartopy.feature as cfeature - import cartopy - from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER - - ##If the script is run on a limited-internet access machine, the CARTOPY_DIR environment setting - ##will need to be set in the user-specific system configuration file. Review the Installation section - ##of the User's Guide for more details. - if os.getenv('CARTOPY_DIR'): - cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir')) - -except Exception as err_msg: - WRAPPER_CANNOT_RUN = True - EXCEPTION_ERR = err_msg - -import produtil.setup - -from ..util import met_util as util -from ..util import do_string_sub -from . import CommandBuilder - - -class CyclonePlotterWrapper(CommandBuilder): - """! Generate plots of extra tropical storm forecast tracks. - Reads input from ATCF files generated from MET TC-Pairs - """ - - def __init__(self, config, instance=None, config_overrides=None): - self.app_name = 'cyclone_plotter' - - super().__init__(config, - instance=instance, - config_overrides=config_overrides) - - if WRAPPER_CANNOT_RUN: - self.log_error("There was a problem importing modules: " - f"{EXCEPTION_ERR}\n") - return - - self.input_data = self.config.getdir('CYCLONE_PLOTTER_INPUT_DIR') - self.output_dir = self.config.getdir('CYCLONE_PLOTTER_OUTPUT_DIR') - self.init_date = self.config.getraw('config', - 'CYCLONE_PLOTTER_INIT_DATE') - self.init_hr = self.config.getraw('config', 'CYCLONE_PLOTTER_INIT_HR') - - init_time_fmt = self.config.getstr('config', 'INIT_TIME_FMT', '') - - if init_time_fmt: - clock_time = datetime.datetime.strptime( - self.config.getstr('config', - 'CLOCK_TIME'), - '%Y%m%d%H%M%S' - ) - - init_beg = self.config.getraw('config', 'INIT_BEG') - if init_beg: - init_beg_dt = util.get_time_obj(init_beg, - init_time_fmt, - clock_time, - logger=self.logger) - self.init_date = do_string_sub(self.init_date, init=init_beg_dt) - self.init_hr = do_string_sub(self.init_hr, init=init_beg_dt) - - self.model = self.config.getstr('config', 'CYCLONE_PLOTTER_MODEL') - self.title = self.config.getstr('config', - 'CYCLONE_PLOTTER_PLOT_TITLE') - self.gen_ascii = ( - self.config.getbool('config', - 'CYCLONE_PLOTTER_GENERATE_TRACK_ASCII') - ) - # Create a set to keep track of unique storm_ids for each track file. - self.unique_storm_id = set() - # Data structure to separate data based on storm id. - self.storm_id_dict = {} - - # Data/info which we want to retrieve from the track files. - self.columns_of_interest = ['AMODEL', 'STORM_ID', 'INIT', - 'LEAD', 'VALID', 'ALAT', 'ALON'] - self.circle_marker_size = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE') - ) - self.annotation_font_size = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_ANNOTATION_FONT_SIZE') - ) - - self.legend_font_size = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_LEGEND_FONT_SIZE') - ) - - # Map centered on Pacific Ocean - self.central_latitude = 180.0 - - self.cross_marker_size = (self.config.getint('config', - 'CYCLONE_PLOTTER_CROSS_MARKER_SIZE') - ) - self.resolution_dpi = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_RESOLUTION_DPI') - ) - - self.add_watermark = self.config.getbool('config', - 'CYCLONE_PLOTTER_ADD_WATERMARK', - True) - # Set the marker symbols, '+' for a small cross, '.' for a small circle. - # NOTE: originally, 'o' was used. 'o' is also a circle, but it creates a - # very large filled circle on the plots that appears too large. - self.cross_marker = '+' - self.circle_marker = '.' - - - def run_all_times(self): - """! Calls the defs needed to create the cyclone plots - run_all_times() is required by CommandBuilder. - - """ - self.sanitized_df = self.retrieve_data() - self.create_plot() - - - def retrieve_data(self): - """! Retrieve data from track files. - Returns: - sanitized_df: a pandas dataframe containing the - "sanitized" longitudes, as well as some markers and - lead group information needed for generating - scatter plots. - - """ - self.logger.debug("Begin retrieving data...") - all_tracks_list = [] - - # Store the data in the track list. - if os.path.isdir(self.input_data): - self.logger.debug("Get data from all files in the directory " + - self.input_data) - # Get the list of all files (full file path) in this directory - all_input_files = util.get_files(self.input_data, ".*.tcst", - self.logger) - - # read each file into pandas then concatenate them together - df_list = [pd.read_csv(file, delim_whitespace=True) for file in all_input_files] - combined = pd.concat(df_list, ignore_index=True) - - # check for empty dataframe, set error message and exit - if combined.empty: - self.logger.error("No data found in specified files. Please check your config file settings.") - sys.exit("No data found.") - - # if there are any NaN values in the ALAT, ALON, STORM_ID, LEAD, INIT, AMODEL, or VALID column, - # drop that row of data (axis=0). We need all these columns to contain valid data in order - # to create a meaningful plot. - combined_df = combined.copy(deep=True) - combined_df = combined.dropna(axis=0, how='any', - subset=self.columns_of_interest) - - # Retrieve and create the columns of interest - self.logger.debug(f"Number of rows of data: {combined_df.shape[0]}") - combined_subset = combined_df[self.columns_of_interest] - df = combined_subset.copy(deep=True) - df.allows_duplicate_labels = False - # INIT, LEAD, VALID correspond to the column headers from the MET - # TC tool output. INIT_YMD, INIT_HOUR, VALID_DD, and VALID_HOUR are - # new columns (for a new dataframe) created from these MET columns. - df['INIT'] = df['INIT'].astype(str) - df['INIT_YMD'] = (df['INIT'].str[:8]).astype(int) - df['INIT_HOUR'] = (df['INIT'].str[9:11]).astype(int) - df['LEAD'] = df['LEAD']/10000 - df['LEAD'] = df['LEAD'].astype(int) - df['VALID_DD'] = (df['VALID'].str[6:8]).astype(int) - df['VALID_HOUR'] = (df['VALID'].str[9:11]).astype(int) - df['VALID'] = df['VALID'].astype(int) - - # Subset the dataframe to include only the data relevant to the user's criteria as - # specified in the configuration file. - init_date = int(self.init_date) - init_hh = int(self.init_hr) - model_name = self.model - - if model_name: - self.logger.debug("Subsetting based on " + str(init_date) + " " + str(init_hh) + - ", and model:" + model_name ) - mask = df[(df['AMODEL'] == model_name) & (df['INIT_YMD'] >= init_date) & - (df['INIT_HOUR'] >= init_hh)] - else: - # no model specified, just subset on init date and init hour - mask = df[(df['INIT_YMD'] >= init_date) & - (df['INIT_HOUR'] >= init_hh)] - self.logger.debug("Subsetting based on " + str(init_date) + ", and "+ str(init_hh)) - - user_criteria_df = mask - # reset the index so things are ordered properly in the new dataframe - user_criteria_df.reset_index(inplace=True) - - # Aggregate the ALON values based on unique storm id to facilitate "sanitizing" the - # longitude values (to handle lons that cross the International Date Line). - unique_storm_ids_set = set(user_criteria_df['STORM_ID']) - self.unique_storm_ids = list(unique_storm_ids_set) - nunique = len(self.unique_storm_ids) - self.logger.debug(f" {nunique} unique storm ids identified") - - # Use named tuples to store the relevant storm track information (their index value in the dataframe, - # track id, and ALON values and later on the SLON (sanitized ALON values). - TrackPt = namedtuple("TrackPt", "indices track alons alats") - - # named tuple holding "sanitized" longitudes - SanTrackPt = namedtuple("SanTrackPt", "indices track alons slons") - - # Keep track of the unique storm tracks by their storm_id - storm_track_dict = {} - - for cur_unique in self.unique_storm_ids: - idx_list = user_criteria_df.index[user_criteria_df['STORM_ID'] == cur_unique].tolist() - alons = [] - alats = [] - indices = [] - - for idx in idx_list: - alons.append(user_criteria_df.loc[idx, 'ALON']) - alats.append(user_criteria_df.loc[idx, 'ALAT']) - indices.append(idx) - - # create the track_pt tuple and add it to the storm track dictionary - track_pt = TrackPt(indices, cur_unique, alons, alats) - storm_track_dict[cur_unique] = track_pt - - # create a new dataframe to contain the sanitized lons (i.e. the original ALONs that have - # been cleaned up when crossing the International Date Line) - sanitized_df = user_criteria_df.copy(deep=True) - - # Now we have a dictionary that helps in aggregating the data based on - # storm tracks (via storm id) and will contain the "sanitized" lons - sanitized_storm_tracks = {} - for key in storm_track_dict: - # "Sanitize" the longitudes to shift the lons that cross the International Date Line. - # Create a new SanTrackPt named tuple and add that to a new dictionary - # that keeps track of the sanitized data based on the storm id - # sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) - sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) - sanitized_track_pt = SanTrackPt(storm_track_dict[key].indices, storm_track_dict[key].track, - storm_track_dict[key].alons, sanitized_lons) - sanitized_storm_tracks[key] = sanitized_track_pt - - # fill in the sanitized dataframe, sanitized_df - for key in sanitized_storm_tracks: - # now use the indices of the storm tracks to correctly assign the sanitized - # lons to the appropriate row in the dataframe to maintain the row ordering of - # the original dataframe - idx_list = sanitized_storm_tracks[key].indices - - for i, idx in enumerate(idx_list): - sanitized_df.loc[idx,'SLON'] = sanitized_storm_tracks[key].slons[i] - - # Set some useful values used for plotting. - # Set the IS_FIRST value to True if this is the first - # point in the storm track, False - # otherwise - if i == 0: - sanitized_df.loc[idx, 'IS_FIRST'] = True - else: - sanitized_df.loc[idx, 'IS_FIRST'] = False - - # Set the lead group to the character '0' if the valid hour is 0 or 12, - # or to the charcter '6' if the valid hour is 6 or 18. Set the marker - # to correspond to the valid hour: 'o' (open circle) for 0 or 12 valid hour, - # or '+' (small plus/cross) for 6 or 18. - if sanitized_df.loc[idx, 'VALID_HOUR'] == 0 or sanitized_df.loc[idx, 'VALID_HOUR'] == 12: - sanitized_df.loc[idx, 'LEAD_GROUP'] ='0' - sanitized_df.loc[idx, 'MARKER'] = self.circle_marker - elif sanitized_df.loc[idx, 'VALID_HOUR'] == 6 or sanitized_df.loc[idx, 'VALID_HOUR'] == 18: - sanitized_df.loc[idx, 'LEAD_GROUP'] = '6' - sanitized_df.loc[idx, 'MARKER'] = self.cross_marker - - # Write output ASCII file (csv) summarizing the information extracted from the input - # which is used to generate the plot. - if self.gen_ascii: - self.logger.debug(f" output dir: {self.output_dir}") - util.mkdir_p(self.output_dir) - ascii_track_parts = [self.init_date, '.csv'] - ascii_track_output_name = ''.join(ascii_track_parts) - final_df_filename = os.path.join(self.output_dir, ascii_track_output_name) - - # Make sure that the dataframe is sorted by STORM_ID, INIT_YMD, INIT_HOUR, and LEAD - # to ensure that the line plot is connecting the points in the correct order. - # sanitized_df.sort_values(by=['STORM_ID', 'INIT_YMD', 'INIT_HOUR', 'LEAD'], - # inplace=True).reset_index(drop=True,inplace=True) - # sanitized_df.reset_index(inplace=True,drop=True) - final_df = sanitized_df.sort_values(by=['STORM_ID', 'INIT_YMD', 'INIT_HOUR', 'LEAD'], ignore_index=True) - final_df.to_csv(final_df_filename) - else: - # The user's specified directory isn't valid, log the error and exit. - self.logger.error("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory, check config file.") - sys.exit("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory.") - return final_df - - - def create_plot(self): - """ - Create the plot, using Cartopy - - """ - - # Use PlateCarree projection for scatter plots - # and Geodetic projection for line plots. - cm_lon = self.central_latitude - ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=cm_lon)) - prj = ccrs.PlateCarree() - # for transforming the annotations (matplotlib to cartopy workaround from Stack Overflow) - transform = ccrs.PlateCarree()._as_mpl_transform(ax) - - # Add land, coastlines, and ocean - ax.add_feature(cfeature.LAND) - ax.coastlines() - ax.add_feature(cfeature.OCEAN) - - # keep map zoomed out to full world. If this - # is absent, the default behavior is to zoom - # into the portion of the map that contains points. - ax.set_global() - - # Add grid lines for longitude and latitude - gl = ax.gridlines(crs=prj, - draw_labels=True, linewidth=1, color='gray', - alpha=0.5, linestyle='--') - gl.xlabels_top = False - gl.ylabels_left = False - gl.xlines = True - gl.xformatter = LONGITUDE_FORMATTER - gl.yformatter = LATITUDE_FORMATTER - gl.xlabel_style = {'size': 9, 'color': 'blue'} - gl.xlabel_style = {'color': 'black', 'weight': 'normal'} - - # Plot title - plt.title(self.title + "\nFor forecast with initial time = " + - self.init_date) - - # Optional: Create the NCAR watermark with a timestamp - # This will appear in the bottom right corner of the plot, below - # the x-axis. NOTE: The timestamp is in the user's local time zone - # and not in UTC time. - if self.add_watermark: - ts = time.time() - st = datetime.datetime.fromtimestamp(ts).strftime( - '%Y-%m-%d %H:%M:%S') - watermark = 'DTC METplus\nplot created at: ' + st - plt.text(60, -130, watermark, fontsize=5, alpha=0.25) - - # Make sure the output directory exists, and create it if it doesn't. - util.mkdir_p(self.output_dir) - - # get the points for the scatter plots (and the relevant information for annotations, etc.) - points_list = self.get_plot_points() - - # Legend labels - lead_group_0_legend = "Indicates a position at 00 or 12 UTC" - lead_group_6_legend = "Indicates a position at 06 or 18 UTC" - - # to be consistent with the NOAA website, use red for annotations, markers, and lines. - pt_color = 'red' - cross_marker_size = self.cross_marker_size - circle_marker_size = self.circle_marker_size - - # Get all the lat and lon (i.e. x and y) points for the '+' and 'o' marker types - # to be used in generating the scatter plots (one for the 0/12 hr and one for the 6/18 hr lead - # groups). Also collect ALL the lons and lats, which will be used to generate the - # line plot (the last plot that goes on top of all the scatter plots). - cross_lons = [] - cross_lats = [] - cross_annotations = [] - circle_lons = [] - circle_lats = [] - circle_annotations = [] - - for idx,pt in enumerate(points_list): - if pt.marker == self.cross_marker: - cross_lons.append(pt.lon) - cross_lats.append(pt.lat) - cross_annotations.append(pt.annotation) - # cross_marker = pt.marker - elif pt.marker == self.circle_marker: - circle_lons.append(pt.lon) - circle_lats.append(pt.lat) - circle_annotations.append(pt.annotation) - # circle_marker = pt.marker - - # Now generate the scatter plots for the lead group 0/12 hr ('+' marker) and the - # lead group 6/18 hr ('.' marker). - plt.scatter(circle_lons, circle_lats, s=self.circle_marker_size, c=pt_color, - marker=self.circle_marker, zorder=2, label=lead_group_0_legend, transform=prj ) - plt.scatter(cross_lons, cross_lats, s=self.cross_marker_size, c=pt_color, - marker=self.cross_marker, zorder=2, label=lead_group_6_legend, transform=prj) - - # annotations for the scatter plots - counter = 0 - for x,y in zip(circle_lons, circle_lats): - plt.annotate(circle_annotations[counter], (x,y+1), xycoords=transform, color=pt_color, - fontsize=self.annotation_font_size) - counter += 1 - - counter = 0 - for x, y in zip(cross_lons, cross_lats): - plt.annotate(cross_annotations[counter], (x, y + 1), xycoords=transform, color=pt_color, - fontsize=self.annotation_font_size) - counter += 1 - - # Dummy point to add the additional label explaining the labelling of the first - # point in the storm track - plt.scatter(0, 0, zorder=2, marker=None, c='', - label="Date (dd/hhz) is the first " + - "time storm was able to be tracked in model") - - # Settings for the legend box location. - ax.legend(loc='lower left', bbox_to_anchor=(0, -0.4), - fancybox=True, shadow=True, scatterpoints=1, - prop={'size':self.legend_font_size}) - - # Generate the line plot - # First collect all the lats and lons for each storm track. Then for each storm track, - # generate a line plot. - pts_by_track_dict = self.get_points_by_track() - - for key in pts_by_track_dict: - lons = [] - lats = [] - for idx, pt in enumerate(pts_by_track_dict[key]): - lons.append(pt.lon) - lats.append(pt.lat) - - # Create the line plot for the current storm track, use the Geodetic coordinate reference system - # to correctly connect adjacent points that have been sanitized and cross the - # International Date line or the Prime Meridian. - plt.plot(lons, lats, linestyle='-', color=pt_color, linewidth=.4, transform=ccrs.Geodetic(), zorder=3) - - # Write the plot to the output directory - out_filename_parts = [self.init_date, '.png'] - output_plot_name = ''.join(out_filename_parts) - plot_filename = os.path.join(self.output_dir, output_plot_name) - if self.resolution_dpi > 0: - plt.savefig(plot_filename, dpi=self.resolution_dpi) - else: - # use Matplotlib's default if no resolution is set in config file - plt.savefig(plot_filename) - - - def get_plot_points(self): - """ - Get the lon and lat points to be plotted, along with any other plotting-relevant - information like the marker, whether this is a first point (to be used in - annotating the first point using the valid day date and valid hour), etc. - - :return: A list of named tuples that represent the points to plot with corresponding - plotting information - """ - - # Create a named tuple to store the point information - PlotPt = namedtuple("PlotPt", "storm_id lon lat is_first marker valid_dd valid_hour annotation") - - points_list = [] - storm_id = self.sanitized_df['STORM_ID'] - lons = self.sanitized_df['SLON'] - lats = self.sanitized_df['ALAT'] - is_first_list = self.sanitized_df['IS_FIRST'] - marker_list = self.sanitized_df['MARKER'] - valid_dd_list = self.sanitized_df['VALID_DD'] - valid_hour_list = self.sanitized_df['VALID_HOUR'] - annotation_list = [] - - for idx, cur_lon in enumerate(lons): - if is_first_list[idx] is True: - annotation = str(valid_dd_list[idx]).zfill(2) + '/' + \ - str(valid_hour_list[idx]).zfill(2) + 'z' - else: - annotation = None - - annotation_list.append(annotation) - cur_pt = PlotPt(storm_id, lons[idx], lats[idx], is_first_list[idx], marker_list[idx], - valid_dd_list[idx], valid_hour_list[idx], annotation) - points_list.append(cur_pt) - - return points_list - - - def get_points_by_track(self): - """ - Get all the lats and lons for each storm track. Used to generate the line - plot of the storm tracks. - - Args: - - :return: - points_by_track: Points aggregated by storm track. - Returns a dictionary where the key is the storm_id - and values are the points (lon,lat) stored in a named tuple - """ - track_dict = {} - LonLat = namedtuple("LonLat", "lon lat") - for cur_unique in self.unique_storm_ids: - # retrieve the ALAT and ALON values that correspond to the rows for a unique storm id. - # i.e. Get the index value(s) corresponding to this unique storm id - idx_list = self.sanitized_df.index[self.sanitized_df['STORM_ID'] == cur_unique].tolist() - sanitized_lons_and_lats = [] - indices = [] - for idx in idx_list: - cur_lonlat = LonLat(self.sanitized_df.loc[idx, 'SLON'], self.sanitized_df.loc[idx, 'ALAT']) - sanitized_lons_and_lats.append(cur_lonlat) - indices.append(idx) - - # update the track dictionary - track_dict[cur_unique] = sanitized_lons_and_lats - - return track_dict - - - @staticmethod - def sanitize_lonlist(lon): - """ - Solution from Stack Overflow for "sanitizing" longitudes that cross the International Date Line - https://stackoverflow.com/questions/67730660/plotting-line-across-international-dateline-with-cartopy - - Args: - @param lon: A list of longitudes (float) that correspond to a storm track - - Returns: - new_list: a list of "sanitized" lons that are "corrected" for crossing the - International Date Line - """ - - new_list = [] - oldval = 0 - # used to compare adjacent longitudes in a storm track - treshold = 10 - for ix, ea in enumerate(lon): - diff = oldval - ea - if (ix > 0): - if (diff > treshold): - ea = ea + 360 - oldval = ea - new_list.append(ea) + +"""!@namespace ExtraTropicalCyclonePlotter +A Python class that generates plots of extra tropical cyclone forecast data, + replicating the NCEP tropical and extra tropical cyclone tracks and + verification plots http://www.emc.ncep.noaa.gov/mmb/gplou/emchurr/glblgen/ +""" + +import os +import time +import datetime +import re +import sys +from collections import namedtuple + + +# handle if module can't be loaded to run wrapper +WRAPPER_CANNOT_RUN = False +EXCEPTION_ERR = '' +try: + import pandas as pd + import matplotlib.pyplot as plt + import matplotlib.ticker as mticker + import cartopy.crs as ccrs + import cartopy.feature as cfeature + import cartopy + from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER + + ##If the script is run on a limited-internet access machine, the CARTOPY_DIR environment setting + ##will need to be set in the user-specific system configuration file. Review the Installation section + ##of the User's Guide for more details. + if os.getenv('CARTOPY_DIR'): + cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir')) + +except Exception as err_msg: + WRAPPER_CANNOT_RUN = True + EXCEPTION_ERR = err_msg + +import produtil.setup + +from ..util import met_util as util +from ..util import do_string_sub +from . import CommandBuilder + + +class CyclonePlotterWrapper(CommandBuilder): + """! Generate plots of extra tropical storm forecast tracks. + Reads input from ATCF files generated from MET TC-Pairs + """ + + def __init__(self, config, instance=None, config_overrides={}): + self.app_name = 'cyclone_plotter' + + super().__init__(config, + instance=instance, + config_overrides=config_overrides) + + if WRAPPER_CANNOT_RUN: + self.log_error("There was a problem importing modules: " + f"{EXCEPTION_ERR}\n") + return + + self.input_data = self.config.getdir('CYCLONE_PLOTTER_INPUT_DIR') + self.output_dir = self.config.getdir('CYCLONE_PLOTTER_OUTPUT_DIR') + self.init_date = self.config.getraw('config', + 'CYCLONE_PLOTTER_INIT_DATE') + self.init_hr = self.config.getraw('config', 'CYCLONE_PLOTTER_INIT_HR') + + init_time_fmt = self.config.getstr('config', 'INIT_TIME_FMT', '') + + if init_time_fmt: + clock_time = datetime.datetime.strptime( + self.config.getstr('config', + 'CLOCK_TIME'), + '%Y%m%d%H%M%S' + ) + + init_beg = self.config.getraw('config', 'INIT_BEG') + if init_beg: + init_beg_dt = util.get_time_obj(init_beg, + init_time_fmt, + clock_time, + logger=self.logger) + self.init_date = do_string_sub(self.init_date, init=init_beg_dt) + self.init_hr = do_string_sub(self.init_hr, init=init_beg_dt) + + self.model = self.config.getstr('config', 'CYCLONE_PLOTTER_MODEL') + self.title = self.config.getstr('config', + 'CYCLONE_PLOTTER_PLOT_TITLE') + self.gen_ascii = ( + self.config.getbool('config', + 'CYCLONE_PLOTTER_GENERATE_TRACK_ASCII') + ) + # Create a set to keep track of unique storm_ids for each track file. + self.unique_storm_id = set() + # Data structure to separate data based on storm id. + self.storm_id_dict = {} + + # Data/info which we want to retrieve from the track files. + self.columns_of_interest = ['AMODEL', 'STORM_ID', 'INIT', + 'LEAD', 'VALID', 'ALAT', 'ALON'] + self.circle_marker_size = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE') + ) + self.annotation_font_size = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_ANNOTATION_FONT_SIZE') + ) + + self.legend_font_size = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_LEGEND_FONT_SIZE') + ) + + # Map centered on Pacific Ocean + self.central_longitude = 180.0 + + self.cross_marker_size = (self.config.getint('config', + 'CYCLONE_PLOTTER_CROSS_MARKER_SIZE') + ) + self.resolution_dpi = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_RESOLUTION_DPI') + ) + + self.add_watermark = self.config.getbool('config', + 'CYCLONE_PLOTTER_ADD_WATERMARK', + True) + # Set the marker symbols, '+' for a small cross, '.' for a small circle. + # NOTE: originally, 'o' was used. 'o' is also a circle, but it creates a + # very large filled circle on the plots that appears too large. + self.cross_marker = '+' + self.circle_marker = '.' + + # Map extent, global if CYCLONE_PLOTTER_GLOBAL_PLOT is True + self.is_global_extent = (self.config.getbool('config', + 'CYCLONE_PLOTTER_GLOBAL_PLOT') + ) + self.logger.debug(f"global_extent value: {self.is_global_extent}") + + # User-defined map extent, lons and lats + if self.is_global_extent: + self.logger.debug("Global extent") + else: + self.logger.debug("Getting lons and lats that define the plot's extent") + west_lon = (self.config.getstr('config', + 'CYCLONE_PLOTTER_WEST_LON') + ) + east_lon = (self.config.getstr('config', + 'CYCLONE_PLOTTER_EAST_LON') + ) + north_lat = (self.config.getstr('config', + 'CYCLONE_PLOTTER_NORTH_LAT') + ) + south_lat = (self.config.getstr('config', + 'CYCLONE_PLOTTER_SOUTH_LAT') + ) + + # Check for unconfigured lons and lats needed for defining the extent + if not west_lon: + self.logger.error("Missing CYCLONE_PLOTTER_WEST_LON in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_WEST_LON please check config file") + else: + self.west_lon = (float(west_lon)) + if not east_lon: + self.logger.error("Missing CYCLONE_PLOTTER_EAST_LON in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_EAST_LON please check config file") + else: + self.east_lon = (float(east_lon)) + if not south_lat: + self.logger.error("Missing CYCLONE_PLOTTER_SOUTH_LAT in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_SOUTH_LAT please check config file") + else: + self.south_lat = float(south_lat) + if not north_lat: + self.logger.error("Missing CYCLONE_PLOTTER_NORTH_LAT in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_NORTH_LAT please check config file") + else: + self.north_lat = float(north_lat) + + self.extent_region = [self.west_lon, self.east_lon, self.south_lat, self.north_lat] + self.logger.debug(f"extent region: {self.extent_region}") + + + def run_all_times(self): + """! Calls the defs needed to create the cyclone plots + run_all_times() is required by CommandBuilder. + + """ + self.sanitized_df = self.retrieve_data() + self.create_plot() + + + def retrieve_data(self): + """! Retrieve data from track files. + Returns: + sanitized_df: a pandas dataframe containing the + "sanitized" longitudes, as well as some markers and + lead group information needed for generating + scatter plots. + + """ + self.logger.debug("Begin retrieving data...") + all_tracks_list = [] + + # Store the data in the track list. + if os.path.isdir(self.input_data): + self.logger.debug("Get data from all files in the directory " + + self.input_data) + # Get the list of all files (full file path) in this directory + all_input_files = util.get_files(self.input_data, ".*.tcst", + self.logger) + + # read each file into pandas then concatenate them together + df_list = [pd.read_csv(file, delim_whitespace=True) for file in all_input_files] + combined = pd.concat(df_list, ignore_index=True) + + # check for empty dataframe, set error message and exit + if combined.empty: + self.logger.error("No data found in specified files. Please check your config file settings.") + sys.exit("No data found.") + + # if there are any NaN values in the ALAT, ALON, STORM_ID, LEAD, INIT, AMODEL, or VALID column, + # drop that row of data (axis=0). We need all these columns to contain valid data in order + # to create a meaningful plot. + combined_df = combined.copy(deep=True) + combined_df = combined.dropna(axis=0, how='any', + subset=self.columns_of_interest) + + # Retrieve and create the columns of interest + self.logger.debug(f"Number of rows of data: {combined_df.shape[0]}") + combined_subset = combined_df[self.columns_of_interest] + df = combined_subset.copy(deep=True) + df.allows_duplicate_labels = False + # INIT, LEAD, VALID correspond to the column headers from the MET + # TC tool output. INIT_YMD, INIT_HOUR, VALID_DD, and VALID_HOUR are + # new columns (for a new dataframe) created from these MET columns. + df['INIT'] = df['INIT'].astype(str) + df['INIT_YMD'] = (df['INIT'].str[:8]).astype(int) + df['INIT_HOUR'] = (df['INIT'].str[9:11]).astype(int) + df['LEAD'] = df['LEAD']/10000 + df['LEAD'] = df['LEAD'].astype(int) + df['VALID_DD'] = (df['VALID'].str[6:8]).astype(int) + df['VALID_HOUR'] = (df['VALID'].str[9:11]).astype(int) + df['VALID'] = df['VALID'].astype(int) + + # Subset the dataframe to include only the data relevant to the user's criteria as + # specified in the configuration file. + init_date = int(self.init_date) + init_hh = int(self.init_hr) + model_name = self.model + + if model_name: + self.logger.debug("Subsetting based on " + str(init_date) + " " + str(init_hh) + + ", and model:" + model_name ) + mask = df[(df['AMODEL'] == model_name) & (df['INIT_YMD'] >= init_date) & + (df['INIT_HOUR'] >= init_hh)] + else: + # no model specified, just subset on init date and init hour + mask = df[(df['INIT_YMD'] >= init_date) & + (df['INIT_HOUR'] >= init_hh)] + self.logger.debug("Subsetting based on " + str(init_date) + ", and "+ str(init_hh)) + + user_criteria_df = mask + # reset the index so things are ordered properly in the new dataframe + user_criteria_df.reset_index(inplace=True) + + # Aggregate the ALON values based on unique storm id to facilitate "sanitizing" the + # longitude values (to handle lons that cross the International Date Line). + unique_storm_ids_set = set(user_criteria_df['STORM_ID']) + self.unique_storm_ids = list(unique_storm_ids_set) + nunique = len(self.unique_storm_ids) + self.logger.debug(f" {nunique} unique storm ids identified") + + # Use named tuples to store the relevant storm track information (their index value in the dataframe, + # track id, and ALON values and later on the SLON (sanitized ALON values). + TrackPt = namedtuple("TrackPt", "indices track alons alats") + + # named tuple holding "sanitized" longitudes + SanTrackPt = namedtuple("SanTrackPt", "indices track alons slons") + + # Keep track of the unique storm tracks by their storm_id + storm_track_dict = {} + + for cur_unique in self.unique_storm_ids: + idx_list = user_criteria_df.index[user_criteria_df['STORM_ID'] == cur_unique].tolist() + alons = [] + alats = [] + indices = [] + + for idx in idx_list: + alons.append(user_criteria_df.loc[idx, 'ALON']) + alats.append(user_criteria_df.loc[idx, 'ALAT']) + indices.append(idx) + + # create the track_pt tuple and add it to the storm track dictionary + track_pt = TrackPt(indices, cur_unique, alons, alats) + storm_track_dict[cur_unique] = track_pt + + # create a new dataframe to contain the sanitized lons (i.e. the original ALONs that have + # been cleaned up when crossing the International Date Line) + sanitized_df = user_criteria_df.copy(deep=True) + + # Now we have a dictionary that helps in aggregating the data based on + # storm tracks (via storm id) and will contain the "sanitized" lons + sanitized_storm_tracks = {} + for key in storm_track_dict: + # "Sanitize" the longitudes to shift the lons that cross the International Date Line. + # Create a new SanTrackPt named tuple and add that to a new dictionary + # that keeps track of the sanitized data based on the storm id + # sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) + sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) + sanitized_track_pt = SanTrackPt(storm_track_dict[key].indices, storm_track_dict[key].track, + storm_track_dict[key].alons, sanitized_lons) + sanitized_storm_tracks[key] = sanitized_track_pt + + # fill in the sanitized dataframe, sanitized_df + for key in sanitized_storm_tracks: + # now use the indices of the storm tracks to correctly assign the sanitized + # lons to the appropriate row in the dataframe to maintain the row ordering of + # the original dataframe + idx_list = sanitized_storm_tracks[key].indices + + for i, idx in enumerate(idx_list): + sanitized_df.loc[idx,'SLON'] = sanitized_storm_tracks[key].slons[i] + + # Set some useful values used for plotting. + # Set the IS_FIRST value to True if this is the first + # point in the storm track, False + # otherwise + if i == 0: + sanitized_df.loc[idx, 'IS_FIRST'] = True + else: + sanitized_df.loc[idx, 'IS_FIRST'] = False + + # Set the lead group to the character '0' if the valid hour is 0 or 12, + # or to the charcter '6' if the valid hour is 6 or 18. Set the marker + # to correspond to the valid hour: 'o' (open circle) for 0 or 12 valid hour, + # or '+' (small plus/cross) for 6 or 18. + if sanitized_df.loc[idx, 'VALID_HOUR'] == 0 or sanitized_df.loc[idx, 'VALID_HOUR'] == 12: + sanitized_df.loc[idx, 'LEAD_GROUP'] ='0' + sanitized_df.loc[idx, 'MARKER'] = self.circle_marker + elif sanitized_df.loc[idx, 'VALID_HOUR'] == 6 or sanitized_df.loc[idx, 'VALID_HOUR'] == 18: + sanitized_df.loc[idx, 'LEAD_GROUP'] = '6' + sanitized_df.loc[idx, 'MARKER'] = self.cross_marker + + # If the user has specified a region of interest rather than the + # global extent, subset the data even further to points that are within a bounding box. + if not self.is_global_extent: + self.logger.debug(f"Subset the data based on the region of interest.") + subset_by_region_df = self.subset_by_region(sanitized_df) + final_df = subset_by_region_df.copy(deep=True) + else: + final_df = sanitized_df.copy(deep=True) + + # Write output ASCII file (csv) summarizing the information extracted from the input + # which is used to generate the plot. + if self.gen_ascii: + self.logger.debug(f" output dir: {self.output_dir}") + util.mkdir_p(self.output_dir) + ascii_track_parts = [self.init_date, '.csv'] + ascii_track_output_name = ''.join(ascii_track_parts) + final_df_filename = os.path.join(self.output_dir, ascii_track_output_name) + + # Make sure that the dataframe is sorted by STORM_ID, INIT_YMD, INIT_HOUR, and LEAD + # to ensure that the line plot is connecting the points in the correct order. + final_sorted_df = final_df.sort_values(by=['STORM_ID', 'INIT_YMD', 'INIT_HOUR', 'LEAD'], ignore_index=True) + final_df.reset_index(drop=True,inplace=True) + final_sorted_df.to_csv(final_df_filename) + else: + # The user's specified directory isn't valid, log the error and exit. + self.logger.error("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory, check config file.") + sys.exit("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory.") + + return final_sorted_df + + + def create_plot(self): + """ + Create the plot, using Cartopy + + """ + + # Use PlateCarree projection for scatter plots + # and Geodetic projection for line plots. + cm_lon = self.central_longitude + prj = ccrs.PlateCarree(central_longitude=cm_lon) + ax = plt.axes(projection=prj) + + # for transforming the annotations (matplotlib to cartopy workaround from Stack Overflow) + transform = ccrs.PlateCarree()._as_mpl_transform(ax) + + # Add land, coastlines, and ocean + ax.add_feature(cfeature.LAND) + ax.coastlines() + ax.add_feature(cfeature.OCEAN) + + # keep map zoomed out to full world (ie global extent) if CYCLONE_PLOTTER_GLOBAL_PLOT is + # yes or True, otherwise use the lons and lats defined in the config file to + # create a polygon (rectangular box) defining the region of interest. + if self.is_global_extent: + ax.set_global() + self.logger.debug("Generating a plot of the global extent") + else: + self.logger.debug(f"Generating a plot of the user-defined extent:{self.west_lon}, {self.east_lon}, " + f"{self.south_lat}, {self.north_lat}") + extent_list = [self.west_lon, self.east_lon, self.south_lat, self.north_lat] + self.logger.debug(f"Setting map extent to: {self.west_lon}, {self.east_lon}, {self.south_lat}, {self.north_lat}") + # Bounding box will not necessarily be centered about the 180 degree longitude, so + # DO NOT explicitly set the central longitude. + ax.set_extent(extent_list, ccrs.PlateCarree()) + + # Add grid lines for longitude and latitude + gl = ax.gridlines(crs=ccrs.PlateCarree(), + draw_labels=True, linewidth=1, color='gray', + alpha=0.5, linestyle='--') + + gl.top_labels = False + gl.left_labels = True + gl.xlines = True + gl.xformatter = LONGITUDE_FORMATTER + gl.yformatter = LATITUDE_FORMATTER + gl.xlabel_style = {'size': 9, 'color': 'blue'} + gl.xlabel_style = {'color': 'black', 'weight': 'normal'} + + # Plot title + plt.title(self.title + "\nFor forecast with initial time = " + + self.init_date) + + # Optional: Create the NCAR watermark with a timestamp + # This will appear in the bottom right corner of the plot, below + # the x-axis. NOTE: The timestamp is in the user's local time zone + # and not in UTC time. + if self.add_watermark: + ts = time.time() + st = datetime.datetime.fromtimestamp(ts).strftime( + '%Y-%m-%d %H:%M:%S') + watermark = 'DTC METplus\nplot created at: ' + st + plt.text(60, -130, watermark, fontsize=5, alpha=0.25) + + # Make sure the output directory exists, and create it if it doesn't. + util.mkdir_p(self.output_dir) + + # get the points for the scatter plots (and the relevant information for annotations, etc.) + points_list = self.get_plot_points() + + # Legend labels + lead_group_0_legend = "Indicates a position at 00 or 12 UTC" + lead_group_6_legend = "Indicates a position at 06 or 18 UTC" + + # to be consistent with the NOAA website, use red for annotations, markers, and lines. + pt_color = 'red' + cross_marker_size = self.cross_marker_size + circle_marker_size = self.circle_marker_size + + # Get all the lat and lon (i.e. x and y) points for the '+' and 'o' marker types + # to be used in generating the scatter plots (one for the 0/12 hr and one for the 6/18 hr lead + # groups). Also collect ALL the lons and lats, which will be used to generate the + # line plot (the last plot that goes on top of all the scatter plots). + cross_lons = [] + cross_lats = [] + cross_annotations = [] + circle_lons = [] + circle_lats = [] + circle_annotations = [] + + for idx,pt in enumerate(points_list): + if pt.marker == self.cross_marker: + cross_lons.append(pt.lon) + cross_lats.append(pt.lat) + cross_annotations.append(pt.annotation) + # cross_marker = pt.marker + elif pt.marker == self.circle_marker: + circle_lons.append(pt.lon) + circle_lats.append(pt.lat) + circle_annotations.append(pt.annotation) + # circle_marker = pt.marker + + # Now generate the scatter plots for the lead group 0/12 hr ('+' marker) and the + # lead group 6/18 hr ('.' marker). + plt.scatter(circle_lons, circle_lats, s=self.circle_marker_size, c=pt_color, + marker=self.circle_marker, zorder=2, label=lead_group_0_legend, transform=ccrs.PlateCarree()) + plt.scatter(cross_lons, cross_lats, s=self.cross_marker_size, c=pt_color, + marker=self.cross_marker, zorder=2, label=lead_group_6_legend, transform=ccrs.PlateCarree()) + + # annotations for the scatter plots + counter = 0 + for x,y in zip(circle_lons, circle_lats): + plt.annotate(circle_annotations[counter], (x,y+1), xycoords=transform, color=pt_color, + fontsize=self.annotation_font_size) + counter += 1 + + counter = 0 + for x, y in zip(cross_lons, cross_lats): + plt.annotate(cross_annotations[counter], (x, y + 1), xycoords=transform, color=pt_color, + fontsize=self.annotation_font_size) + counter += 1 + + # Dummy point to add the additional label explaining the labelling of the first + # point in the storm track + plt.scatter(0, 0, zorder=2, marker=None, c='', + label="Date (dd/hhz) is the first " + + "time storm was able to be tracked in model") + + # Settings for the legend box location. + ax.legend(loc='lower left', bbox_to_anchor=(0, -0.4), + fancybox=True, shadow=True, scatterpoints=1, + prop={'size':self.legend_font_size}) + + # Generate the line plot + # First collect all the lats and lons for each storm track. Then for each storm track, + # generate a line plot. + pts_by_track_dict = self.get_points_by_track() + + for key in pts_by_track_dict: + lons = [] + lats = [] + for idx, pt in enumerate(pts_by_track_dict[key]): + lons.append(pt.lon) + lats.append(pt.lat) + + # Create the line plot for the current storm track, use the Geodetic coordinate reference system + # to correctly connect adjacent points that have been sanitized and cross the + # International Date line or the Prime Meridian. + plt.plot(lons, lats, linestyle='-', color=pt_color, linewidth=.4, transform=ccrs.Geodetic(), zorder=3) + + # Write the plot to the output directory + out_filename_parts = [self.init_date, '.png'] + output_plot_name = ''.join(out_filename_parts) + plot_filename = os.path.join(self.output_dir, output_plot_name) + if self.resolution_dpi > 0: + plt.savefig(plot_filename, dpi=self.resolution_dpi) + else: + # use Matplotlib's default if no resolution is set in config file + plt.savefig(plot_filename) + + + def get_plot_points(self): + """ + Get the lon and lat points to be plotted, along with any other plotting-relevant + information like the marker, whether this is a first point (to be used in + annotating the first point using the valid day date and valid hour), etc. + + :return: A list of named tuples that represent the points to plot with corresponding + plotting information + """ + + # Create a named tuple to store the point information + PlotPt = namedtuple("PlotPt", "storm_id lon lat is_first marker valid_dd valid_hour annotation") + + points_list = [] + storm_id = self.sanitized_df['STORM_ID'] + lons = self.sanitized_df['SLON'] + lats = self.sanitized_df['ALAT'] + is_first_list = self.sanitized_df['IS_FIRST'] + marker_list = self.sanitized_df['MARKER'] + valid_dd_list = self.sanitized_df['VALID_DD'] + valid_hour_list = self.sanitized_df['VALID_HOUR'] + annotation_list = [] + + for idx, cur_lon in enumerate(lons): + if is_first_list[idx] is True: + annotation = str(valid_dd_list[idx]).zfill(2) + '/' + \ + str(valid_hour_list[idx]).zfill(2) + 'z' + else: + annotation = None + + annotation_list.append(annotation) + cur_pt = PlotPt(storm_id, lons[idx], lats[idx], is_first_list[idx], marker_list[idx], + valid_dd_list[idx], valid_hour_list[idx], annotation) + points_list.append(cur_pt) + + return points_list + + + def get_points_by_track(self): + """ + Get all the lats and lons for each storm track. Used to generate the line + plot of the storm tracks. + + Args: + + :return: + points_by_track: Points aggregated by storm track. + Returns a dictionary where the key is the storm_id + and values are the points (lon,lat) stored in a named tuple + """ + track_dict = {} + LonLat = namedtuple("LonLat", "lon lat") + for cur_unique in self.unique_storm_ids: + # retrieve the ALAT and ALON values that correspond to the rows for a unique storm id. + # i.e. Get the index value(s) corresponding to this unique storm id + idx_list = self.sanitized_df.index[self.sanitized_df['STORM_ID'] == cur_unique].tolist() + sanitized_lons_and_lats = [] + indices = [] + for idx in idx_list: + cur_lonlat = LonLat(self.sanitized_df.loc[idx, 'SLON'], self.sanitized_df.loc[idx, 'ALAT']) + sanitized_lons_and_lats.append(cur_lonlat) + indices.append(idx) + + # update the track dictionary + track_dict[cur_unique] = sanitized_lons_and_lats + + return track_dict + + + def subset_by_region(self, sanitized_df): + """ + Args: + @param: sanitized_df the pandas dataframe containing + the "sanitized" longitudes and other useful + plotting information + + Returns: + :return: + """ + self.logger.debug("Subsetting by region...") + + # Copy the sanitized_df dataframe + sanitized_by_region_df = sanitized_df.copy(deep=True) + + # Iterate over ALL the rows and if any point is within the polygon, + # save it's index so we can create a new dataframe with just the + # relevant data. + for index, row in sanitized_by_region_df.iterrows(): + if (self.west_lon <= row['ALON'] <= self.east_lon) and (self.south_lat <= row['ALAT'] <= self.north_lat): + sanitized_by_region_df.loc[index,'INSIDE'] = True + else: + sanitized_by_region_df.loc[index,'INSIDE'] = False + + # Now filter the input dataframe based on the whether points are inside + # the specified boundaries. + masked = sanitized_by_region_df[sanitized_by_region_df['INSIDE'] == True] + masked.reset_index(drop=True,inplace=True) + + if len(masked) == 0: + sys.exit("No data in region specified, please check your lon and lat values in the config file.") + + return masked + + + @staticmethod + def sanitize_lonlist(lon_list): + """ + Solution from Stack Overflow for "sanitizing" longitudes that cross the International Date Line + https://stackoverflow.com/questions/67730660/plotting-line-across-international-dateline-with-cartopy + + Args: + @param lon_list: A list of longitudes (float) that correspond to a storm track + + Returns: + new_list: a list of "sanitized" lons that are "corrected" for crossing the + International Date Line + """ + + new_list = [] + oldval = 0 + # used to compare adjacent longitudes in a storm track + treshold = 10 + for ix, ea in enumerate(lon_list): + diff = oldval - ea + if (ix > 0): + if (diff > treshold): + ea = ea + 360 + oldval = ea + new_list.append(ea) + return new_list \ No newline at end of file diff --git a/parm/use_cases/met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf b/parm/use_cases/met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf index 9d8ce2730b..aa3d15a506 100644 --- a/parm/use_cases/met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf +++ b/parm/use_cases/met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf @@ -24,6 +24,37 @@ CYCLONE_PLOTTER_INIT_DATE = 20150301 CYCLONE_PLOTTER_INIT_HR = 12 ;; hh format CYCLONE_PLOTTER_MODEL = GFSO CYCLONE_PLOTTER_PLOT_TITLE = Model Forecast Storm Tracks +## +# Indicate the region of the globe to plot +# + +# Set to Y[y]es or True to plot entire global extent. N[n]o or False +# to generate a plot of a defined region of the world, then define lons and +# lats below. +CYCLONE_PLOTTER_GLOBAL_PLOT = no + +## +# Indicate the region (i.e. define a bounding box) to plot +# + +# Set to Y[y]es or True to plot entire global extent, N[n]o or False +# to generate a plot of a defined region of the world (and define lons and +# lats below). +CYCLONE_PLOTTER_GLOBAL_PLOT = no + +# ***IMPORTANT*** If CYCLONE_PLOTTER_GLOBAL_PLOT +# is set to False or N[n]o, then define the region of the world to plot. +# Longitudes can range from -180 to 180 degrees and latitudes from -90 to 90 degrees + +# -------------------------------- +# EXAMPLE OF BOUNDING BOX SETTINGS +# -------------------------------- +# NORTHERN HEMISPHERE +CYCLONE_PLOTTER_WEST_LON = -180 +CYCLONE_PLOTTER_EAST_LON = 179 +CYCLONE_PLOTTER_SOUTH_LAT = 0 +CYCLONE_PLOTTER_NORTH_LAT = 90 + ## # Indicate the size of symbol (point size) diff --git a/parm/use_cases/model_applications/tc_and_extra_tc/CyclonePlotter_fcstGFS_obsGFS_UserScript_ExtraTC.conf b/parm/use_cases/model_applications/tc_and_extra_tc/CyclonePlotter_fcstGFS_obsGFS_UserScript_ExtraTC.conf index 94a0773846..3549ba23c5 100644 --- a/parm/use_cases/model_applications/tc_and_extra_tc/CyclonePlotter_fcstGFS_obsGFS_UserScript_ExtraTC.conf +++ b/parm/use_cases/model_applications/tc_and_extra_tc/CyclonePlotter_fcstGFS_obsGFS_UserScript_ExtraTC.conf @@ -103,11 +103,32 @@ CYCLONE_PLOTTER_INIT_HR ={init?fmt=%H} CYCLONE_PLOTTER_MODEL = GFSO CYCLONE_PLOTTER_PLOT_TITLE = Model Forecast Storm Tracks +## +# Indicate the region (i.e. define a bounding box) to plot +# + +# Set to Y[y]es or True to plot entire global extent, N[n]o or False +# to generate a plot of a defined region of the world (and define lons and +# lats below). +CYCLONE_PLOTTER_GLOBAL_PLOT = no + +# ***IMPORTANT*** If CYCLONE_PLOTTER_GLOBAL_PLOT +# is set to False or N[n]o, then define the region of the world to plot. +# Longitudes can range from -180 to 180 degrees and latitudes from -90 to 90 degrees + +# -------------------------------- +# EXAMPLE OF BOUNDING BOX SETTINGS +# -------------------------------- +# NORTHERN HEMISPHERE +CYCLONE_PLOTTER_WEST_LON = -180 +CYCLONE_PLOTTER_EAST_LON = 179 +CYCLONE_PLOTTER_SOUTH_LAT = 0 +CYCLONE_PLOTTER_NORTH_LAT = 90 ## # Indicate the size of symbol (point size) -CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE = 2 -CYCLONE_PLOTTER_CROSS_MARKER_SIZE = 3 +CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE = 4 +CYCLONE_PLOTTER_CROSS_MARKER_SIZE = 6 ## # Indicate text size of annotation label diff --git a/parm/use_cases/model_applications/tc_and_extra_tc/Plotter_fcstGFS_obsGFS_ExtraTC.conf b/parm/use_cases/model_applications/tc_and_extra_tc/Plotter_fcstGFS_obsGFS_ExtraTC.conf index 7734c929d9..cd213f47ac 100644 --- a/parm/use_cases/model_applications/tc_and_extra_tc/Plotter_fcstGFS_obsGFS_ExtraTC.conf +++ b/parm/use_cases/model_applications/tc_and_extra_tc/Plotter_fcstGFS_obsGFS_ExtraTC.conf @@ -26,7 +26,6 @@ TC_PAIRS_OUTPUT_TEMPLATE = {date?fmt=%Y%m}/{basin?fmt=%s}q{date?fmt=%Y%m%d%H}.gf # EXTRA TROPICAL CYCLONE PLOT OPTIONS... # PROCESS_LIST = TCPairs, CyclonePlotter - LOOP_ORDER = processes LOOP_BY = init @@ -134,6 +133,27 @@ CYCLONE_PLOTTER_MODEL = GFSO CYCLONE_PLOTTER_PLOT_TITLE = Model Forecast Storm Tracks ## +# Indicate the region (i.e. define a bounding box) to plot +# + +# Set to Y[y]es or True to plot entire global extent, N[n]o or False +# to generate a plot of a defined region of the world (and define lons and +# lats below). +CYCLONE_PLOTTER_GLOBAL_PLOT = no + +# ***IMPORTANT*** If CYCLONE_PLOTTER_GLOBAL_PLOT +# is set to False or N[n]o, then define the region of the world to plot. +# Longitudes can range from -180 to 180 degrees and latitudes from -90 to 90 degrees + +# -------------------------------- +# EXAMPLE OF BOUNDING BOX SETTINGS +# -------------------------------- +# NORTHERN HEMISPHERE +CYCLONE_PLOTTER_WEST_LON = -180 +CYCLONE_PLOTTER_EAST_LON = 179 +CYCLONE_PLOTTER_SOUTH_LAT = 0 +CYCLONE_PLOTTER_NORTH_LAT = 90 + # Indicate the size of symbol (point size) CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE = 2 CYCLONE_PLOTTER_CROSS_MARKER_SIZE = 3 From 5406975cd1b4c07f7d693412a0d04b95cf86c6d6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 28 Oct 2021 16:59:13 -0600 Subject: [PATCH 112/770] adding AXIS_ANG to BCMSE #1049 --- docs/Users_Guide/statistics_list.rst | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 5c6285880a..95cca0e61a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -98,6 +98,36 @@ METplus Database of Statistics - - MODE Tool - MODE ascii object + * - Object axis angle (in degrees) + - AXIS_ANG + - + - MODE, MTD + - Attribute output + * - Difference in spatial axis plane angles + - AXIS_DIFF + - + - MTD + - Attribute Output + * - Baddeley’s Delta Metric + - BADDELEY + - + - Grid-Stat + - DMAP + * - Bias Adjusted Gilbert Skill Score + - BAGSS + - + - Point-Stat, Grid-Stat + - CTS, NBRCTS + * - Base Rate + - BASER + - + - Point-Stat Tool, Grid-Stat, Wavelet-Stat, MODE + - CTS, ECLV, MODE, NBRCTCS, PSTD, PJC + * - Bias-corrected mean squared error + - BCMSE + - + - Point-Stat, Grid-Stat, Ensemble-Stat + - CNT, SSVAR ..#.. glossary:: statistics From 2c2db6517b942a40d84b2e2f42f7bc41990702e6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 28 Oct 2021 17:08:25 -0600 Subject: [PATCH 113/770] adding spacing #1049 --- docs/Users_Guide/statistics_list.rst | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 95cca0e61a..16a433d77e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -98,12 +98,14 @@ METplus Database of Statistics - - MODE Tool - MODE ascii object - * - Object axis angle (in degrees) + * - Object axis angle :raw-html:`
` + (in degrees) - AXIS_ANG - - MODE, MTD - Attribute output - * - Difference in spatial axis plane angles + * - Difference in spatial :raw-html:`
` + axis plane angles - AXIS_DIFF - - MTD @@ -113,7 +115,8 @@ METplus Database of Statistics - - Grid-Stat - DMAP - * - Bias Adjusted Gilbert Skill Score + * - Bias Adjusted Gilbert :raw-html:`
` + Skill Score - BAGSS - - Point-Stat, Grid-Stat @@ -121,12 +124,16 @@ METplus Database of Statistics * - Base Rate - BASER - - - Point-Stat Tool, Grid-Stat, Wavelet-Stat, MODE - - CTS, ECLV, MODE, NBRCTCS, PSTD, PJC - * - Bias-corrected mean squared error + - Point-Stat Tool, Grid-Stat, :raw-html:`
` + Wavelet-Stat, MODE + - CTS, ECLV, MODE, :raw-html:`
` + NBRCTCS, PSTD, PJC + * - Bias-corrected mean :raw-html:`
` + squared error - BCMSE - - - Point-Stat, Grid-Stat, Ensemble-Stat + - Point-Stat, Grid-Stat, :raw-html:`
` + Ensemble-Stat - CNT, SSVAR From b1e1772421680de61ba58e43658aca219a84592b Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 28 Oct 2021 17:15:54 -0600 Subject: [PATCH 114/770] adding spacing removing commas #1049 --- docs/Users_Guide/statistics_list.rst | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 16a433d77e..8559cb6480 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -102,7 +102,8 @@ METplus Database of Statistics (in degrees) - AXIS_ANG - - - MODE, MTD + - MODE :raw-html:`
` + MTD - Attribute output * - Difference in spatial :raw-html:`
` axis plane angles @@ -120,21 +121,30 @@ METplus Database of Statistics - BAGSS - - Point-Stat, Grid-Stat - - CTS, NBRCTS + - CTS :raw-html:`
` + NBRCTS * - Base Rate - BASER - - - Point-Stat Tool, Grid-Stat, :raw-html:`
` - Wavelet-Stat, MODE - - CTS, ECLV, MODE, :raw-html:`
` - NBRCTCS, PSTD, PJC + - Point-Stat Tool :raw-html:`
` + Grid-Stat :raw-html:`
` + Wavelet-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + ECLV :raw-html:`
` + MODE :raw-html:`
` + NBRCTCS :raw-html:`
` + PSTD :raw-html:`
` + PJC * - Bias-corrected mean :raw-html:`
` squared error - BCMSE - - - Point-Stat, Grid-Stat, :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` Ensemble-Stat - - CNT, SSVAR + - CNT :raw-html:`
` + SSVAR ..#.. glossary:: statistics From 406fed6be0533011b2dcb11ef6c145be5652456d Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 28 Oct 2021 17:21:35 -0600 Subject: [PATCH 115/770] removing comma #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8559cb6480..befb4f0a8e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -120,7 +120,8 @@ METplus Database of Statistics Skill Score - BAGSS - - - Point-Stat, Grid-Stat + - Point-Stat, :raw-html:`
` + Grid-Stat - CTS :raw-html:`
` NBRCTS * - Base Rate From ca65f6f6d83d9832a46ac56d7954761c364de5d4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 29 Oct 2021 11:49:18 -0600 Subject: [PATCH 116/770] BOUNDARY_DIST thru BSS_SMPL #1049 --- docs/Users_Guide/statistics_list.rst | 35 ++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index befb4f0a8e..f10ff5ab4f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -120,7 +120,7 @@ METplus Database of Statistics Skill Score - BAGSS - - - Point-Stat, :raw-html:`
` + - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` NBRCTS @@ -146,7 +146,38 @@ METplus Database of Statistics Ensemble-Stat - CNT :raw-html:`
` SSVAR - + * - Minimum distance between :raw-html:`
` + the boundaries of two objects + - BOUNDARY_DIST + - + - MODE + - Attribute Output + * - Brier Score + - BRIER + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Climatological Brier Score + - BRIERCL + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Brier Skill Score relative :raw-html:`
` + to sample climatology + - BSS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Brier Skill Score relative :raw-html:`
` + to external climatology + - BSS_SMPL + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD ..#.. glossary:: statistics ..# :sorted: From 471b3b94d83b7917a24dcab56d044c7db5fd0de7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 29 Oct 2021 14:08:25 -0600 Subject: [PATCH 117/770] BOUNDARY_DIST splitting across 2 lines #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f10ff5ab4f..08128cc107 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -148,7 +148,8 @@ METplus Database of Statistics SSVAR * - Minimum distance between :raw-html:`
` the boundaries of two objects - - BOUNDARY_DIST + - BOUNDARY :raw-html:`
` + _DIST - - MODE - Attribute Output From 8b90b2ef3a9410c864d4689dde377bd46f53850c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 1 Nov 2021 15:43:56 -0600 Subject: [PATCH 118/770] cleaning up typos #1049 --- docs/Users_Guide/statistics_list.rst | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 08128cc107..efd4b0099b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -9,16 +9,18 @@ METplus Database of Statistics :widths: auto :header-rows: 1 - * - Statistics :raw-html:`
` Long Name + * - Statistics :raw-html:`
` + Long Name - METplus Name - Statistic Type - Tools - - Statistics + - METplus :raw-html:`
` + Line Type * - Accuracy - ACC - Categorical - - Point-Stat Tool :raw-html:`
` - MODE Tool + - Point-Stat :raw-html:`
` + MODE - CTS :raw-html:`
` MCTS :raw-html:`
` NBRCTS :raw-html:`
` @@ -26,23 +28,21 @@ METplus Database of Statistics * - Asymptotic Fractions Skill Score - AFSS - - - Grid-Stat Tool + - Grid-Stat - NBRCNT * - Along track error (nm) - ALTK_ERR - - - TC-Pairs Tool + - TC-Pairs - TCMPR * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) - ANGLE_DIFF - - - MODE Tool + - MODE - MODE - * - The Anomaly Correlation :raw-html:`
` - including mean error with normal :raw-html:`
` - and bootstrap upper and :raw-html:`
` - lower confidence limits + * - Anomaly Correlation :raw-html:`
` + including mean error - ANOM_CORR - - Point-Stat :raw-html:`
` @@ -50,7 +50,7 @@ METplus Database of Statistics Series-Analysis :raw-html:`
` Stat-Analysis - CNT - * - The uncentered Anomaly :raw-html:`
` + * - Uncentered Anomaly :raw-html:`
` Correlation excluding mean :raw-html:`
` error including bootstrap upper :raw-html:`
` and lower confidence limits @@ -67,7 +67,7 @@ METplus Database of Statistics - MODE :raw-html:`
` MTD - MODE ascii object - * - The forecast object area :raw-html:`
` + * - Forecast object area :raw-html:`
` divided by the observation :raw-html:`
` object area (unitless) :raw-html:`
` NOTE: Prior to met-10.0.0, :raw-html:`
` @@ -77,7 +77,7 @@ METplus Database of Statistics of the two - AREA_RATIO - - - MODE Tool + - MODE - MODE ascii object * - Area of the object :raw-html:`
` containing data values :raw-html:`
` @@ -87,7 +87,7 @@ METplus Database of Statistics criteria (in grid squares) - AREA_THRESH - - - MODE Tool + - MODE - MODE ascii object * - Absolute value of :raw-html:`
` the difference :raw-html:`
` @@ -96,7 +96,7 @@ METplus Database of Statistics (unitless) - ASPECT_DIFF - - - MODE Tool + - MODE - MODE ascii object * - Object axis angle :raw-html:`
` (in degrees) @@ -110,7 +110,7 @@ METplus Database of Statistics - AXIS_DIFF - - MTD - - Attribute Output + - Attribute output * - Baddeley’s Delta Metric - BADDELEY - @@ -127,7 +127,7 @@ METplus Database of Statistics * - Base Rate - BASER - - - Point-Stat Tool :raw-html:`
` + - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Wavelet-Stat :raw-html:`
` MODE @@ -152,7 +152,7 @@ METplus Database of Statistics _DIST - - MODE - - Attribute Output + - Attribute output * - Brier Score - BRIER - From 3c84e3f035ec24612ca896b203560ab11ebf1b16 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 1 Nov 2021 16:06:21 -0600 Subject: [PATCH 119/770] calibration thru centriod_dist #1049 --- docs/Users_Guide/statistics_list.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index efd4b0099b..71e35c99b3 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -179,6 +179,29 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - PSTD + * - Calibration when forecast :raw-html:`
` + is between the ith and :raw-html:`
` + i+1th probability :raw-html:`
` + thresholds (repeated) + - CALIBRATION_i + - + - Point-Stat + - PJC + * - Total great circle distance :raw-html:`
` + travelled by the 2D spatial :raw-html:`
` + centroid over the lifetime :raw-html:`
` + of the 3D object + - CDIST_TRAVELLED + - + - MODE-td + - MODE-time-domain 3D + * - Distance between two :raw-html:`
` + objects centroids :raw-html:`
` + (in grid units) + - CENTROID_DIST + - + - MODE + - MODE ascii object ..#.. glossary:: statistics ..# :sorted: From 4d8483120cf5036dab555b4520d57aceb9f1d7e2 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 1 Nov 2021 16:39:53 -0600 Subject: [PATCH 120/770] centriod_lat thru centroid_y #1049 --- docs/Users_Guide/statistics_list.rst | 52 ++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 71e35c99b3..34ff14da24 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -183,7 +183,8 @@ METplus Database of Statistics is between the ith and :raw-html:`
` i+1th probability :raw-html:`
` thresholds (repeated) - - CALIBRATION_i + - CALIBRATION :raw-html:`
` + _i - - Point-Stat - PJC @@ -191,17 +192,56 @@ METplus Database of Statistics travelled by the 2D spatial :raw-html:`
` centroid over the lifetime :raw-html:`
` of the 3D object - - CDIST_TRAVELLED + - CDIST :raw-html:`
` + _TRAVELLED - - - MODE-td - - MODE-time-domain 3D + - MTD + - MTD 3D * - Distance between two :raw-html:`
` objects centroids :raw-html:`
` (in grid units) - - CENTROID_DIST + - CENTROID :raw-html:`
` + _DIST - - MODE - - MODE ascii object + - MODE ascii object + * - Latitude of centroid :raw-html:`
` + Location of the centroid + - CENTROID_LAT + - + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D attribute output :raw-html:`
` + MODE ascii object + * - Longitude of centroid :raw-html:`
` + Location of the centroid + - CENTROID_LON + - + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D attribute output :raw-html:`
` + MODE ascii object + * - t coordinate of centroid + - CENTROID_T + - + - MTD + - MTD 3D attribute output + * - x coordinate of centroid :raw-html:`
` + Location of the centroid + - CENTROID_X + - + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D attribute output :raw-html:`
` + MODE ascii object + * - y coordinate of centroid :raw-html:`
` + Location of the centroid + - CENTROID_Y + - + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D attribute output :raw-html:`
` + MODE ascii object ..#.. glossary:: statistics ..# :sorted: From 13cb1cc3d5aa48c33eee63e3255fdb6f462ac053 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 1 Nov 2021 16:48:32 -0600 Subject: [PATCH 121/770] fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 34ff14da24..fc4875ae77 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -207,7 +207,8 @@ METplus Database of Statistics - MODE ascii object * - Latitude of centroid :raw-html:`
` Location of the centroid - - CENTROID_LAT + - CENTROID :raw-html:`
` + _LAT - - MTD :raw-html:`
` MODE @@ -215,7 +216,8 @@ METplus Database of Statistics MODE ascii object * - Longitude of centroid :raw-html:`
` Location of the centroid - - CENTROID_LON + - CENTROID :raw-html:`
` + _LON - - MTD :raw-html:`
` MODE From e2aa0de15abdfde03e09eb7396d7ff50b42e0f29 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 1 Nov 2021 16:49:19 -0600 Subject: [PATCH 122/770] removing test glossary #1049 --- docs/Users_Guide/statistics_list.rst | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index fc4875ae77..eee6af180f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -245,24 +245,3 @@ METplus Database of Statistics - MTD 2D & 3D attribute output :raw-html:`
` MODE ascii object -..#.. glossary:: statistics -..# :sorted: - - ACC - | **Statistics Long Name**: Accuracy \:sup:`1` - | **Statistic Type**: Categorical - | **Tools & Statistics**: CTS \ :sup:`2,3`, MCTS \ :sup:`2,3`, NBRCTCS \ :sup:`3` & MODE output format: Accuracy \ :sup:`1` - | - | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool - & \ :sup:`3` \ Grid-Stat Tool - - - - Key for Tools - | *Tools:* \ :sup:`1` \ MODE-Tool, \ :sup:`2` \ Point-Stat Tool, - \ :sup:`3` \ Grid-Stat Tool, \ :sup:`4` \ Wavelet-Stat Tool, - \ :sup:`5` \ TC-Gen, \ :sup:`6` \ MODE-time-domain - - - - From 1eff166a7746836b805a68fccc0a824f6edde83c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 10:33:15 -0600 Subject: [PATCH 123/770] climo_mean thru crtk_err #1049 --- docs/Users_Guide/statistics_list.rst | 90 +++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index eee6af180f..3df8d70e4b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -244,4 +244,92 @@ METplus Database of Statistics MODE - MTD 2D & 3D attribute output :raw-html:`
` MODE ascii object - + * - Climatological mean value + - CLIMO_MEAN + - + - Point-Stat :raw-html:`
` + Ensemble-Stat + - MPR :raw-html:`
` + ORANK + * - Climatological standard :raw-html:`
` + deviation value + - CLIMO_STDEV + - + - Point-Stat :raw-html:`
` + Ensemble-Stat + - MPR :raw-html:`
` + ORANK + * - Ratio of the difference :raw-html:`
` + between the area of an :raw-html:`
` + object and the area of :raw-html:`
` + its convex hull divided :raw-html:`
` + by the area of the :raw-html:`
` + complex hull (unitless) + - COMPLEXITY + - + - MODE + - MODE ascii object + * - Ratio of complexities of :raw-html:`
` + two objects defined as :raw-html:`
` + the lesser of the forecast :raw-html:`
` + complexity divided by the :raw-html:`
` + observation complexity or :raw-html:`
` + its reciprocal (unitless) + - COMPLEXITY_RATIO + - + - MODE + - MODE ascii object + * - Minimum distance between :raw-html:`
` + the convex hulls of two :raw-html:`
` + objects (in grid units) + - CONVEX_HULL + _DIST + - + - MODE + - MODE ascii object + * - The Continuous Ranked :raw-html:`
` + Probability Score :raw-html:`
` + (normal dist.) + - CRPS + - + - Ensemble-Stat + - ECNT + * - The Continuous Ranked :raw-html:`
` + Probability Score :raw-html:`
` + (empirical dist.) + - CRPS_EMP + - + - Ensemble-Stat + - ECNT + * - Climatological Continuous :raw-html:`
` + Ranked Probability Score :raw-html:`
` + (normal dist.) + - CRPSCL + - + - Ensemble-Stat + - ECNT + * - Climatological Continuous :raw-html:`
` + Ranked Probability Score :raw-html:`
` + (empirical dist.) + - CRPSCL_EMP + - + - Ensemble-Stat + - ECNT + * - The Continuous Ranked :raw-html:`
` + Probability Skill Score :raw-html:`
` + (normal dist.) + - CRPSS + - + - Ensemble-Stat + - ECNT + * - The Continuous Ranked :raw-html:`
` + Probability Skill Score :raw-html:`
` + (empirical dist.) + - CRPSS_EMP + - + - Ensemble-Stat + - ECNT + * - Cross track error (nm) + - CRTK_ERR + - TC-Pairs + - TCMPR From 43f5718e1f00487c181cb0cb77f986020a441af4 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 10:41:46 -0600 Subject: [PATCH 124/770] fixing crtk_err spacing #1049 --- docs/Users_Guide/statistics_list.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 3df8d70e4b..00fb4a3a08 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -331,5 +331,6 @@ METplus Database of Statistics - ECNT * - Cross track error (nm) - CRTK_ERR + - - TC-Pairs - TCMPR From 3db5d9c56f069531253ed8d037bf834af58ea647 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 10:47:56 -0600 Subject: [PATCH 125/770] fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 00fb4a3a08..ebcbbb6ee5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -275,14 +275,15 @@ METplus Database of Statistics complexity divided by the :raw-html:`
` observation complexity or :raw-html:`
` its reciprocal (unitless) - - COMPLEXITY_RATIO + - COMPLEXITY :raw-html:`
` + _RATIO - - MODE - MODE ascii object * - Minimum distance between :raw-html:`
` the convex hulls of two :raw-html:`
` objects (in grid units) - - CONVEX_HULL + - CONVEX_HULL :raw-html:`
` _DIST - - MODE From a18f054868978dbd57959d655d01bfaba791a049 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 11:15:04 -0600 Subject: [PATCH 126/770] CSI to CURVATURE_Y #1049 --- docs/Users_Guide/statistics_list.rst | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ebcbbb6ee5..893f672bbe 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -335,3 +335,46 @@ METplus Database of Statistics - - TC-Pairs - TCMPR + * - Critical Success Index + - CSI + - + - Point-Stat :raw-html:`
` + MODE :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + MODE :raw-html:`
` + MBRCTCS + * - Cloud top pressure (hPa) :raw-html:`
` + Total column precip. :raw-html:`
` + water (km/m**2) :raw-html:`
` + (microwave only) + - CTOP_PRS :raw-html:`
` + TC_PWAT + - + - GSI + - GSI diagnostic radiance :raw-html:`
` + MPR output + * - Radius of curvature + - CURVATURE + - + - MODE + - MODE ascii object + * - Ratio of the curvature + - CURVATURE :raw-html:`
` + _RATIO + - + - MODE + - MODE ascii object + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE_X + - + - MODE + - MODE ascii object + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE_Y + - + - MODE + - MODE ascii object + From 2b696d6146791621029d1807b119cfe277c429dd Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 11:25:02 -0600 Subject: [PATCH 127/770] CURVATURE_X & Y spacing #1049 --- docs/Users_Guide/statistics_list.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 893f672bbe..791bbfb58d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -367,13 +367,15 @@ METplus Database of Statistics - MODE ascii object * - Center of curvature :raw-html:`
` (in grid coordinates) - - CURVATURE_X + - CURVATURE :raw-html:`
` + _X - - MODE - MODE ascii object * - Center of curvature :raw-html:`
` (in grid coordinates) - - CURVATURE_Y + - CURVATURE :raw-html:`
` + _Y - - MODE - MODE ascii object From 7a80e6cee4d313a0c8f1fb148b073a1390518677 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 11:50:01 -0600 Subject: [PATCH 128/770] DEV_CAT to DURATION_DIFF #1049 --- docs/Users_Guide/statistics_list.rst | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 791bbfb58d..3db4760fdc 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -379,4 +379,38 @@ METplus Database of Statistics - - MODE - MODE ascii object + * - Development methodology :raw-html:`
` + category + - DEV_CAT + - + - TC-Gen + - GENMPR + * - Absolute value + - DIR_ABSERR + - + - Point-Stat + - VCNT + * - Signed angle between :raw-html:`
` + the directions of the :raw-html:`
` + average forecast and :raw-html:`
` + observed wing vectors + - DIR_ERR + - + - Point-Stat + - VCNT + * - Difference in object :raw-html:`
` + direction of movement + - DIRECTION :raw-html:`
` + _DIFF + - + - MTD + - MTD 3D pair attribute output + * - Difference in the :raw-html:`
` + lifetimes of the :raw-html:`
` + two objects + - DURATION :raw-html:`
` + _DIFF + - + - MTD + - MTD 3D pair attribute output From e59386aaab07d011dba09d77b56b8f0210908e4d Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 12:14:55 -0600 Subject: [PATCH 129/770] EC_VALUE to F #1049 --- docs/Users_Guide/statistics_list.rst | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 3db4760fdc..68015e5e44 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -413,4 +413,71 @@ METplus Database of Statistics - - MTD - MTD 3D pair attribute output + * - Expected correct rate :raw-html:`
` + used for MCTS HSS_EC + - EC_VALUE + - + - Point-Stat + - MCTC + * - Extreme Dependency Index :raw-html:`
` + including normal and :raw-html:`
` + bootstrap upper and :raw-html:`
` + lower confidence limits + - EDI + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Extreme Dependency Score :raw-html:`
` + including normal and :raw-html:`
` + bootstrap upper and :raw-html:`
` + lower confidence limits + - EDS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Mean of absolute value :raw-html:`
` + of forecast minus :raw-html:`
` + observed gradients + - EGBAR + - + - Grid-Stat + - GRAD + * - Object end time + - END_TIME + - + - MTD + - MTD 3D attribute output + * - Difference in object :raw-html:`
` + ending time steps + - END_TIME :raw-html:`
` + _DELTA + - + - MTD + - MTD 3D pair attribute output + * - The unperturbed :raw-html:`
` + ensemble mean value + - ENS_MEAN + - + - Ensemble-Stat + - ORANK + * - The PERTURBED ensemble :raw-html:`
` + mean (e.g. with :raw-html:`
` + Observation Error). + - ENS_MEAN :raw-html:`
` + _OERR + - + - Ensemble-Stat + - ORANK + * - Standard deviation of :raw-html:`
` + the error + - ESTDEV + - + - Point-Stat :raw-html:`
` + Ensemble-Stat + - CNT :raw-html:`
` + SSVAR From bc500a0ec5d177fd44e2c9c727ea3cfde3be19a2 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 13:49:31 -0600 Subject: [PATCH 130/770] F_RATE TO FBS #1049 --- docs/Users_Guide/statistics_list.rst | 72 +++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 68015e5e44..fbbd0e6312 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -479,5 +479,75 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Ensemble-Stat - CNT :raw-html:`
` - SSVAR + SSVAR + * - Forecast rate/event :raw-html:`
` + frequency + - F_RATE + - + - Point-Stat :raw-html:`
` + Grid-Stat + - FHO :raw-html:`
` + NBRCNT + * - Mean forecast wind speed + - F_SPEED_BAR + - + - Point-Stat + - VL1L2 + * - Mean(f-c) + - FABAR + - + - Point-Stat + - SAL1L2 + * - False alarm ratio + - FAR + - + - Point-Stat :raw-html:`
` + MODE :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + MODE :raw-html:`
` + NBRCTCS + * - Forecast mean + - FBAR + - + - Point-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Ensemble-Stat + - CNT :raw-html:`
` + SL1L2 :raw-html:`
` + VCNT :raw-html:`
` + SSVAR + * - Mean forecast normal upper:raw-html:`
` + and lower confidence:raw-html:`
` + limits + - FBAR_NCL + - + - Ensemble-Stat + - SSVAR + * - Length (speed) of the:raw-html:`
` + average forecast:raw-html:`
` + wind vector + - FBAR_SPEED + - + - Point-Stat + - VCNT + * - Frequency Bias + - FBIAS + - + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Wavelet-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + DMAP :raw-html:`
` + NBRCTCS :raw-html:`
` + ISC :raw-html:`
` + MODE + * - Fractions Brier Score + - FBS + - + - Grid-Stat + - NBRCNT From 1d8240933e30b78939760edb4d85637461bb46ec Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 13:58:03 -0600 Subject: [PATCH 131/770] Fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index fbbd0e6312..77231acd38 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -518,15 +518,15 @@ METplus Database of Statistics SL1L2 :raw-html:`
` VCNT :raw-html:`
` SSVAR - * - Mean forecast normal upper:raw-html:`
` - and lower confidence:raw-html:`
` + * - Mean forecast normal upper :raw-html:`
` + and lower confidence :raw-html:`
` limits - FBAR_NCL - - Ensemble-Stat - SSVAR - * - Length (speed) of the:raw-html:`
` - average forecast:raw-html:`
` + * - Length (speed) of the :raw-html:`
` + average forecast :raw-html:`
` wind vector - FBAR_SPEED - From 750816dccfa80335610bb37454e5c41447ddb29a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 2 Nov 2021 14:30:50 -0600 Subject: [PATCH 132/770] fcst_clus thru fcst_conv_radius #1049 --- docs/Users_Guide/statistics_list.rst | 80 +++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 77231acd38..9c6afa19e0 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -489,7 +489,8 @@ METplus Database of Statistics - FHO :raw-html:`
` NBRCNT * - Mean forecast wind speed - - F_SPEED_BAR + - F_SPEED :raw-html:`
` + _BAR - - Point-Stat - VL1L2 @@ -550,4 +551,79 @@ METplus Database of Statistics - - Grid-Stat - NBRCNT - + * - Number of forecast :raw-html:`
` + clusters + - fcst_clus + - + - MODE + - MODE netCDF dimensions + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the cluster forecast :raw-html:`
` + objects + - fcst_clus_hull + - + - MODE + - MODE netCDF dimensions + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Latitude + - fcst_clus :raw-html:`
` + _hull_lat + - + - MODE + - MODE netCDF variables + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Longitude + - fcst_clus :raw-html:`
` + _hull _lon + - + - MODE + - MODE netCDF variables + * - Number of Forecast :raw-html:`
` + Cluster Convex Hull Points + - fcst_clus :raw-html:`
` + _hull_npts + - + - MODE + - MODE netCDF variables + * - Forecast Cluster Convex :raw-html:`
` + Hull Starting Index + - fcst_clus :raw-html:`
` + _hull_start + - + - MODE + - MODE netCDF variables + * - Forecast Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_clus :raw-html:`
` + _hull_x + - + - MODE + - MODE netCDF variables + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_clus :raw-html:`
` + _hull_y + - + - MODE + - MODE netCDF variables + * - Cluster forecast object id :raw-html:`
` + number for each grid point + - fcst_clus :raw-html:`
` + _id + - + - MODE + - MODE netCDF variables + * - Forecast convolution :raw-html:`
` + threshold + - fcst_conv :raw-html:`
` + _threshold + - + - MODE + - MODE netCDF variables + * - Forecast convolution radius + - fcst_conv :raw-html:`
` + _radius + - + - MODE + - MODE netCDF variables From 9154a840b47d61706ef6f6ccec4d188884591cba Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 3 Nov 2021 11:51:11 -0600 Subject: [PATCH 133/770] removing CTOP_PRS #1049 --- docs/Users_Guide/statistics_list.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 9c6afa19e0..230d80a7b3 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -344,16 +344,6 @@ METplus Database of Statistics - CTS :raw-html:`
` MODE :raw-html:`
` MBRCTCS - * - Cloud top pressure (hPa) :raw-html:`
` - Total column precip. :raw-html:`
` - water (km/m**2) :raw-html:`
` - (microwave only) - - CTOP_PRS :raw-html:`
` - TC_PWAT - - - - GSI - - GSI diagnostic radiance :raw-html:`
` - MPR output * - Radius of curvature - CURVATURE - From fb37a8bc8878dcb540ab98ef8392ced4a986fd08 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 3 Nov 2021 12:00:37 -0600 Subject: [PATCH 134/770] fixing the order of tools for FBAR and FBIAS #1049 --- docs/Users_Guide/statistics_list.rst | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 230d80a7b3..fe16fcc81e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -501,14 +501,12 @@ METplus Database of Statistics * - Forecast mean - FBAR - - - Point-Stat :raw-html:`
` - Point-Stat :raw-html:`
` + - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` - Ensemble-Stat - - CNT :raw-html:`
` + - SSVAR :raw-html:`
` + CNT :raw-html:`
` SL1L2 :raw-html:`
` - VCNT :raw-html:`
` - SSVAR + VCNT * - Mean forecast normal upper :raw-html:`
` and lower confidence :raw-html:`
` limits @@ -527,15 +525,14 @@ METplus Database of Statistics - FBIAS - - Point-Stat :raw-html:`
` - Grid-Stat :raw-html:`
` - Grid-Stat :raw-html:`
` Wavelet-Stat :raw-html:`
` - MODE + MODE :raw-html:`
` + Grid-Stat - CTS :raw-html:`
` - DMAP :raw-html:`
` - NBRCTCS :raw-html:`
` ISC :raw-html:`
` - MODE + MODE :raw-html:`
` + DMAP :raw-html:`
` + NBRCTCS * - Fractions Brier Score - FBS - From c466d2ebc6b693abd2ecb5db3c7cee126b8d0e89 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 3 Nov 2021 12:22:58 -0600 Subject: [PATCH 135/770] fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index fe16fcc81e..1329b1f5f4 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -548,7 +548,8 @@ METplus Database of Statistics define the hull of all :raw-html:`
` of the cluster forecast :raw-html:`
` objects - - fcst_clus_hull + - fcst_clus :raw-html:`
` + _hull - - MODE - MODE netCDF dimensions From 54dafa7d1798ec643d697c97cf54bfa20925f1bb Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 3 Nov 2021 14:00:22 -0600 Subject: [PATCH 136/770] adding grid-stat to all point-stat entries #1049 --- docs/Users_Guide/statistics_list.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 1329b1f5f4..e800052dc7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -19,7 +19,8 @@ METplus Database of Statistics * - Accuracy - ACC - Categorical - - Point-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` MODE - CTS :raw-html:`
` MCTS :raw-html:`
` @@ -187,6 +188,7 @@ METplus Database of Statistics _i - - Point-Stat + Grid-Stat :raw-html:`
` - PJC * - Total great circle distance :raw-html:`
` travelled by the 2D spatial :raw-html:`
` @@ -248,6 +250,7 @@ METplus Database of Statistics - CLIMO_MEAN - - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` Ensemble-Stat - MPR :raw-html:`
` ORANK @@ -256,6 +259,7 @@ METplus Database of Statistics - CLIMO_STDEV - - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` Ensemble-Stat - MPR :raw-html:`
` ORANK @@ -379,6 +383,7 @@ METplus Database of Statistics - DIR_ABSERR - - Point-Stat + Grid-Stat :raw-html:`
` - VCNT * - Signed angle between :raw-html:`
` the directions of the :raw-html:`
` @@ -387,6 +392,7 @@ METplus Database of Statistics - DIR_ERR - - Point-Stat + Grid-Stat :raw-html:`
` - VCNT * - Difference in object :raw-html:`
` direction of movement @@ -408,6 +414,7 @@ METplus Database of Statistics - EC_VALUE - - Point-Stat + Grid-Stat :raw-html:`
` - MCTC * - Extreme Dependency Index :raw-html:`
` including normal and :raw-html:`
` @@ -467,6 +474,7 @@ METplus Database of Statistics - ESTDEV - - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` Ensemble-Stat - CNT :raw-html:`
` SSVAR @@ -483,11 +491,13 @@ METplus Database of Statistics _BAR - - Point-Stat + Grid-Stat :raw-html:`
` - VL1L2 * - Mean(f-c) - FABAR - - Point-Stat + Grid-Stat :raw-html:`
` - SAL1L2 * - False alarm ratio - FAR @@ -503,6 +513,7 @@ METplus Database of Statistics - - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` @@ -520,6 +531,7 @@ METplus Database of Statistics - FBAR_SPEED - - Point-Stat + Grid-Stat :raw-html:`
` - VCNT * - Frequency Bias - FBIAS From ee5e27457a6aace261ef31fc7a81f258d0f4a815 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 3 Nov 2021 15:47:35 -0600 Subject: [PATCH 137/770] convert file back to unix format via dos2unix and added change that was lost in merge --- metplus/wrappers/cyclone_plotter_wrapper.py | 1336 +++++++++---------- 1 file changed, 668 insertions(+), 668 deletions(-) diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index 5c177f6fc9..7a5870e4b5 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -1,668 +1,668 @@ - -"""!@namespace ExtraTropicalCyclonePlotter -A Python class that generates plots of extra tropical cyclone forecast data, - replicating the NCEP tropical and extra tropical cyclone tracks and - verification plots http://www.emc.ncep.noaa.gov/mmb/gplou/emchurr/glblgen/ -""" - -import os -import time -import datetime -import re -import sys -from collections import namedtuple - - -# handle if module can't be loaded to run wrapper -WRAPPER_CANNOT_RUN = False -EXCEPTION_ERR = '' -try: - import pandas as pd - import matplotlib.pyplot as plt - import matplotlib.ticker as mticker - import cartopy.crs as ccrs - import cartopy.feature as cfeature - import cartopy - from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER - - ##If the script is run on a limited-internet access machine, the CARTOPY_DIR environment setting - ##will need to be set in the user-specific system configuration file. Review the Installation section - ##of the User's Guide for more details. - if os.getenv('CARTOPY_DIR'): - cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir')) - -except Exception as err_msg: - WRAPPER_CANNOT_RUN = True - EXCEPTION_ERR = err_msg - -import produtil.setup - -from ..util import met_util as util -from ..util import do_string_sub -from . import CommandBuilder - - -class CyclonePlotterWrapper(CommandBuilder): - """! Generate plots of extra tropical storm forecast tracks. - Reads input from ATCF files generated from MET TC-Pairs - """ - - def __init__(self, config, instance=None, config_overrides={}): - self.app_name = 'cyclone_plotter' - - super().__init__(config, - instance=instance, - config_overrides=config_overrides) - - if WRAPPER_CANNOT_RUN: - self.log_error("There was a problem importing modules: " - f"{EXCEPTION_ERR}\n") - return - - self.input_data = self.config.getdir('CYCLONE_PLOTTER_INPUT_DIR') - self.output_dir = self.config.getdir('CYCLONE_PLOTTER_OUTPUT_DIR') - self.init_date = self.config.getraw('config', - 'CYCLONE_PLOTTER_INIT_DATE') - self.init_hr = self.config.getraw('config', 'CYCLONE_PLOTTER_INIT_HR') - - init_time_fmt = self.config.getstr('config', 'INIT_TIME_FMT', '') - - if init_time_fmt: - clock_time = datetime.datetime.strptime( - self.config.getstr('config', - 'CLOCK_TIME'), - '%Y%m%d%H%M%S' - ) - - init_beg = self.config.getraw('config', 'INIT_BEG') - if init_beg: - init_beg_dt = util.get_time_obj(init_beg, - init_time_fmt, - clock_time, - logger=self.logger) - self.init_date = do_string_sub(self.init_date, init=init_beg_dt) - self.init_hr = do_string_sub(self.init_hr, init=init_beg_dt) - - self.model = self.config.getstr('config', 'CYCLONE_PLOTTER_MODEL') - self.title = self.config.getstr('config', - 'CYCLONE_PLOTTER_PLOT_TITLE') - self.gen_ascii = ( - self.config.getbool('config', - 'CYCLONE_PLOTTER_GENERATE_TRACK_ASCII') - ) - # Create a set to keep track of unique storm_ids for each track file. - self.unique_storm_id = set() - # Data structure to separate data based on storm id. - self.storm_id_dict = {} - - # Data/info which we want to retrieve from the track files. - self.columns_of_interest = ['AMODEL', 'STORM_ID', 'INIT', - 'LEAD', 'VALID', 'ALAT', 'ALON'] - self.circle_marker_size = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE') - ) - self.annotation_font_size = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_ANNOTATION_FONT_SIZE') - ) - - self.legend_font_size = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_LEGEND_FONT_SIZE') - ) - - # Map centered on Pacific Ocean - self.central_longitude = 180.0 - - self.cross_marker_size = (self.config.getint('config', - 'CYCLONE_PLOTTER_CROSS_MARKER_SIZE') - ) - self.resolution_dpi = ( - self.config.getint('config', - 'CYCLONE_PLOTTER_RESOLUTION_DPI') - ) - - self.add_watermark = self.config.getbool('config', - 'CYCLONE_PLOTTER_ADD_WATERMARK', - True) - # Set the marker symbols, '+' for a small cross, '.' for a small circle. - # NOTE: originally, 'o' was used. 'o' is also a circle, but it creates a - # very large filled circle on the plots that appears too large. - self.cross_marker = '+' - self.circle_marker = '.' - - # Map extent, global if CYCLONE_PLOTTER_GLOBAL_PLOT is True - self.is_global_extent = (self.config.getbool('config', - 'CYCLONE_PLOTTER_GLOBAL_PLOT') - ) - self.logger.debug(f"global_extent value: {self.is_global_extent}") - - # User-defined map extent, lons and lats - if self.is_global_extent: - self.logger.debug("Global extent") - else: - self.logger.debug("Getting lons and lats that define the plot's extent") - west_lon = (self.config.getstr('config', - 'CYCLONE_PLOTTER_WEST_LON') - ) - east_lon = (self.config.getstr('config', - 'CYCLONE_PLOTTER_EAST_LON') - ) - north_lat = (self.config.getstr('config', - 'CYCLONE_PLOTTER_NORTH_LAT') - ) - south_lat = (self.config.getstr('config', - 'CYCLONE_PLOTTER_SOUTH_LAT') - ) - - # Check for unconfigured lons and lats needed for defining the extent - if not west_lon: - self.logger.error("Missing CYCLONE_PLOTTER_WEST_LON in config file. ") - sys.exit("Missing the CYCLONE_PLOTTER_WEST_LON please check config file") - else: - self.west_lon = (float(west_lon)) - if not east_lon: - self.logger.error("Missing CYCLONE_PLOTTER_EAST_LON in config file. ") - sys.exit("Missing the CYCLONE_PLOTTER_EAST_LON please check config file") - else: - self.east_lon = (float(east_lon)) - if not south_lat: - self.logger.error("Missing CYCLONE_PLOTTER_SOUTH_LAT in config file. ") - sys.exit("Missing the CYCLONE_PLOTTER_SOUTH_LAT please check config file") - else: - self.south_lat = float(south_lat) - if not north_lat: - self.logger.error("Missing CYCLONE_PLOTTER_NORTH_LAT in config file. ") - sys.exit("Missing the CYCLONE_PLOTTER_NORTH_LAT please check config file") - else: - self.north_lat = float(north_lat) - - self.extent_region = [self.west_lon, self.east_lon, self.south_lat, self.north_lat] - self.logger.debug(f"extent region: {self.extent_region}") - - - def run_all_times(self): - """! Calls the defs needed to create the cyclone plots - run_all_times() is required by CommandBuilder. - - """ - self.sanitized_df = self.retrieve_data() - self.create_plot() - - - def retrieve_data(self): - """! Retrieve data from track files. - Returns: - sanitized_df: a pandas dataframe containing the - "sanitized" longitudes, as well as some markers and - lead group information needed for generating - scatter plots. - - """ - self.logger.debug("Begin retrieving data...") - all_tracks_list = [] - - # Store the data in the track list. - if os.path.isdir(self.input_data): - self.logger.debug("Get data from all files in the directory " + - self.input_data) - # Get the list of all files (full file path) in this directory - all_input_files = util.get_files(self.input_data, ".*.tcst", - self.logger) - - # read each file into pandas then concatenate them together - df_list = [pd.read_csv(file, delim_whitespace=True) for file in all_input_files] - combined = pd.concat(df_list, ignore_index=True) - - # check for empty dataframe, set error message and exit - if combined.empty: - self.logger.error("No data found in specified files. Please check your config file settings.") - sys.exit("No data found.") - - # if there are any NaN values in the ALAT, ALON, STORM_ID, LEAD, INIT, AMODEL, or VALID column, - # drop that row of data (axis=0). We need all these columns to contain valid data in order - # to create a meaningful plot. - combined_df = combined.copy(deep=True) - combined_df = combined.dropna(axis=0, how='any', - subset=self.columns_of_interest) - - # Retrieve and create the columns of interest - self.logger.debug(f"Number of rows of data: {combined_df.shape[0]}") - combined_subset = combined_df[self.columns_of_interest] - df = combined_subset.copy(deep=True) - df.allows_duplicate_labels = False - # INIT, LEAD, VALID correspond to the column headers from the MET - # TC tool output. INIT_YMD, INIT_HOUR, VALID_DD, and VALID_HOUR are - # new columns (for a new dataframe) created from these MET columns. - df['INIT'] = df['INIT'].astype(str) - df['INIT_YMD'] = (df['INIT'].str[:8]).astype(int) - df['INIT_HOUR'] = (df['INIT'].str[9:11]).astype(int) - df['LEAD'] = df['LEAD']/10000 - df['LEAD'] = df['LEAD'].astype(int) - df['VALID_DD'] = (df['VALID'].str[6:8]).astype(int) - df['VALID_HOUR'] = (df['VALID'].str[9:11]).astype(int) - df['VALID'] = df['VALID'].astype(int) - - # Subset the dataframe to include only the data relevant to the user's criteria as - # specified in the configuration file. - init_date = int(self.init_date) - init_hh = int(self.init_hr) - model_name = self.model - - if model_name: - self.logger.debug("Subsetting based on " + str(init_date) + " " + str(init_hh) + - ", and model:" + model_name ) - mask = df[(df['AMODEL'] == model_name) & (df['INIT_YMD'] >= init_date) & - (df['INIT_HOUR'] >= init_hh)] - else: - # no model specified, just subset on init date and init hour - mask = df[(df['INIT_YMD'] >= init_date) & - (df['INIT_HOUR'] >= init_hh)] - self.logger.debug("Subsetting based on " + str(init_date) + ", and "+ str(init_hh)) - - user_criteria_df = mask - # reset the index so things are ordered properly in the new dataframe - user_criteria_df.reset_index(inplace=True) - - # Aggregate the ALON values based on unique storm id to facilitate "sanitizing" the - # longitude values (to handle lons that cross the International Date Line). - unique_storm_ids_set = set(user_criteria_df['STORM_ID']) - self.unique_storm_ids = list(unique_storm_ids_set) - nunique = len(self.unique_storm_ids) - self.logger.debug(f" {nunique} unique storm ids identified") - - # Use named tuples to store the relevant storm track information (their index value in the dataframe, - # track id, and ALON values and later on the SLON (sanitized ALON values). - TrackPt = namedtuple("TrackPt", "indices track alons alats") - - # named tuple holding "sanitized" longitudes - SanTrackPt = namedtuple("SanTrackPt", "indices track alons slons") - - # Keep track of the unique storm tracks by their storm_id - storm_track_dict = {} - - for cur_unique in self.unique_storm_ids: - idx_list = user_criteria_df.index[user_criteria_df['STORM_ID'] == cur_unique].tolist() - alons = [] - alats = [] - indices = [] - - for idx in idx_list: - alons.append(user_criteria_df.loc[idx, 'ALON']) - alats.append(user_criteria_df.loc[idx, 'ALAT']) - indices.append(idx) - - # create the track_pt tuple and add it to the storm track dictionary - track_pt = TrackPt(indices, cur_unique, alons, alats) - storm_track_dict[cur_unique] = track_pt - - # create a new dataframe to contain the sanitized lons (i.e. the original ALONs that have - # been cleaned up when crossing the International Date Line) - sanitized_df = user_criteria_df.copy(deep=True) - - # Now we have a dictionary that helps in aggregating the data based on - # storm tracks (via storm id) and will contain the "sanitized" lons - sanitized_storm_tracks = {} - for key in storm_track_dict: - # "Sanitize" the longitudes to shift the lons that cross the International Date Line. - # Create a new SanTrackPt named tuple and add that to a new dictionary - # that keeps track of the sanitized data based on the storm id - # sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) - sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) - sanitized_track_pt = SanTrackPt(storm_track_dict[key].indices, storm_track_dict[key].track, - storm_track_dict[key].alons, sanitized_lons) - sanitized_storm_tracks[key] = sanitized_track_pt - - # fill in the sanitized dataframe, sanitized_df - for key in sanitized_storm_tracks: - # now use the indices of the storm tracks to correctly assign the sanitized - # lons to the appropriate row in the dataframe to maintain the row ordering of - # the original dataframe - idx_list = sanitized_storm_tracks[key].indices - - for i, idx in enumerate(idx_list): - sanitized_df.loc[idx,'SLON'] = sanitized_storm_tracks[key].slons[i] - - # Set some useful values used for plotting. - # Set the IS_FIRST value to True if this is the first - # point in the storm track, False - # otherwise - if i == 0: - sanitized_df.loc[idx, 'IS_FIRST'] = True - else: - sanitized_df.loc[idx, 'IS_FIRST'] = False - - # Set the lead group to the character '0' if the valid hour is 0 or 12, - # or to the charcter '6' if the valid hour is 6 or 18. Set the marker - # to correspond to the valid hour: 'o' (open circle) for 0 or 12 valid hour, - # or '+' (small plus/cross) for 6 or 18. - if sanitized_df.loc[idx, 'VALID_HOUR'] == 0 or sanitized_df.loc[idx, 'VALID_HOUR'] == 12: - sanitized_df.loc[idx, 'LEAD_GROUP'] ='0' - sanitized_df.loc[idx, 'MARKER'] = self.circle_marker - elif sanitized_df.loc[idx, 'VALID_HOUR'] == 6 or sanitized_df.loc[idx, 'VALID_HOUR'] == 18: - sanitized_df.loc[idx, 'LEAD_GROUP'] = '6' - sanitized_df.loc[idx, 'MARKER'] = self.cross_marker - - # If the user has specified a region of interest rather than the - # global extent, subset the data even further to points that are within a bounding box. - if not self.is_global_extent: - self.logger.debug(f"Subset the data based on the region of interest.") - subset_by_region_df = self.subset_by_region(sanitized_df) - final_df = subset_by_region_df.copy(deep=True) - else: - final_df = sanitized_df.copy(deep=True) - - # Write output ASCII file (csv) summarizing the information extracted from the input - # which is used to generate the plot. - if self.gen_ascii: - self.logger.debug(f" output dir: {self.output_dir}") - util.mkdir_p(self.output_dir) - ascii_track_parts = [self.init_date, '.csv'] - ascii_track_output_name = ''.join(ascii_track_parts) - final_df_filename = os.path.join(self.output_dir, ascii_track_output_name) - - # Make sure that the dataframe is sorted by STORM_ID, INIT_YMD, INIT_HOUR, and LEAD - # to ensure that the line plot is connecting the points in the correct order. - final_sorted_df = final_df.sort_values(by=['STORM_ID', 'INIT_YMD', 'INIT_HOUR', 'LEAD'], ignore_index=True) - final_df.reset_index(drop=True,inplace=True) - final_sorted_df.to_csv(final_df_filename) - else: - # The user's specified directory isn't valid, log the error and exit. - self.logger.error("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory, check config file.") - sys.exit("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory.") - - return final_sorted_df - - - def create_plot(self): - """ - Create the plot, using Cartopy - - """ - - # Use PlateCarree projection for scatter plots - # and Geodetic projection for line plots. - cm_lon = self.central_longitude - prj = ccrs.PlateCarree(central_longitude=cm_lon) - ax = plt.axes(projection=prj) - - # for transforming the annotations (matplotlib to cartopy workaround from Stack Overflow) - transform = ccrs.PlateCarree()._as_mpl_transform(ax) - - # Add land, coastlines, and ocean - ax.add_feature(cfeature.LAND) - ax.coastlines() - ax.add_feature(cfeature.OCEAN) - - # keep map zoomed out to full world (ie global extent) if CYCLONE_PLOTTER_GLOBAL_PLOT is - # yes or True, otherwise use the lons and lats defined in the config file to - # create a polygon (rectangular box) defining the region of interest. - if self.is_global_extent: - ax.set_global() - self.logger.debug("Generating a plot of the global extent") - else: - self.logger.debug(f"Generating a plot of the user-defined extent:{self.west_lon}, {self.east_lon}, " - f"{self.south_lat}, {self.north_lat}") - extent_list = [self.west_lon, self.east_lon, self.south_lat, self.north_lat] - self.logger.debug(f"Setting map extent to: {self.west_lon}, {self.east_lon}, {self.south_lat}, {self.north_lat}") - # Bounding box will not necessarily be centered about the 180 degree longitude, so - # DO NOT explicitly set the central longitude. - ax.set_extent(extent_list, ccrs.PlateCarree()) - - # Add grid lines for longitude and latitude - gl = ax.gridlines(crs=ccrs.PlateCarree(), - draw_labels=True, linewidth=1, color='gray', - alpha=0.5, linestyle='--') - - gl.top_labels = False - gl.left_labels = True - gl.xlines = True - gl.xformatter = LONGITUDE_FORMATTER - gl.yformatter = LATITUDE_FORMATTER - gl.xlabel_style = {'size': 9, 'color': 'blue'} - gl.xlabel_style = {'color': 'black', 'weight': 'normal'} - - # Plot title - plt.title(self.title + "\nFor forecast with initial time = " + - self.init_date) - - # Optional: Create the NCAR watermark with a timestamp - # This will appear in the bottom right corner of the plot, below - # the x-axis. NOTE: The timestamp is in the user's local time zone - # and not in UTC time. - if self.add_watermark: - ts = time.time() - st = datetime.datetime.fromtimestamp(ts).strftime( - '%Y-%m-%d %H:%M:%S') - watermark = 'DTC METplus\nplot created at: ' + st - plt.text(60, -130, watermark, fontsize=5, alpha=0.25) - - # Make sure the output directory exists, and create it if it doesn't. - util.mkdir_p(self.output_dir) - - # get the points for the scatter plots (and the relevant information for annotations, etc.) - points_list = self.get_plot_points() - - # Legend labels - lead_group_0_legend = "Indicates a position at 00 or 12 UTC" - lead_group_6_legend = "Indicates a position at 06 or 18 UTC" - - # to be consistent with the NOAA website, use red for annotations, markers, and lines. - pt_color = 'red' - cross_marker_size = self.cross_marker_size - circle_marker_size = self.circle_marker_size - - # Get all the lat and lon (i.e. x and y) points for the '+' and 'o' marker types - # to be used in generating the scatter plots (one for the 0/12 hr and one for the 6/18 hr lead - # groups). Also collect ALL the lons and lats, which will be used to generate the - # line plot (the last plot that goes on top of all the scatter plots). - cross_lons = [] - cross_lats = [] - cross_annotations = [] - circle_lons = [] - circle_lats = [] - circle_annotations = [] - - for idx,pt in enumerate(points_list): - if pt.marker == self.cross_marker: - cross_lons.append(pt.lon) - cross_lats.append(pt.lat) - cross_annotations.append(pt.annotation) - # cross_marker = pt.marker - elif pt.marker == self.circle_marker: - circle_lons.append(pt.lon) - circle_lats.append(pt.lat) - circle_annotations.append(pt.annotation) - # circle_marker = pt.marker - - # Now generate the scatter plots for the lead group 0/12 hr ('+' marker) and the - # lead group 6/18 hr ('.' marker). - plt.scatter(circle_lons, circle_lats, s=self.circle_marker_size, c=pt_color, - marker=self.circle_marker, zorder=2, label=lead_group_0_legend, transform=ccrs.PlateCarree()) - plt.scatter(cross_lons, cross_lats, s=self.cross_marker_size, c=pt_color, - marker=self.cross_marker, zorder=2, label=lead_group_6_legend, transform=ccrs.PlateCarree()) - - # annotations for the scatter plots - counter = 0 - for x,y in zip(circle_lons, circle_lats): - plt.annotate(circle_annotations[counter], (x,y+1), xycoords=transform, color=pt_color, - fontsize=self.annotation_font_size) - counter += 1 - - counter = 0 - for x, y in zip(cross_lons, cross_lats): - plt.annotate(cross_annotations[counter], (x, y + 1), xycoords=transform, color=pt_color, - fontsize=self.annotation_font_size) - counter += 1 - - # Dummy point to add the additional label explaining the labelling of the first - # point in the storm track - plt.scatter(0, 0, zorder=2, marker=None, c='', - label="Date (dd/hhz) is the first " + - "time storm was able to be tracked in model") - - # Settings for the legend box location. - ax.legend(loc='lower left', bbox_to_anchor=(0, -0.4), - fancybox=True, shadow=True, scatterpoints=1, - prop={'size':self.legend_font_size}) - - # Generate the line plot - # First collect all the lats and lons for each storm track. Then for each storm track, - # generate a line plot. - pts_by_track_dict = self.get_points_by_track() - - for key in pts_by_track_dict: - lons = [] - lats = [] - for idx, pt in enumerate(pts_by_track_dict[key]): - lons.append(pt.lon) - lats.append(pt.lat) - - # Create the line plot for the current storm track, use the Geodetic coordinate reference system - # to correctly connect adjacent points that have been sanitized and cross the - # International Date line or the Prime Meridian. - plt.plot(lons, lats, linestyle='-', color=pt_color, linewidth=.4, transform=ccrs.Geodetic(), zorder=3) - - # Write the plot to the output directory - out_filename_parts = [self.init_date, '.png'] - output_plot_name = ''.join(out_filename_parts) - plot_filename = os.path.join(self.output_dir, output_plot_name) - if self.resolution_dpi > 0: - plt.savefig(plot_filename, dpi=self.resolution_dpi) - else: - # use Matplotlib's default if no resolution is set in config file - plt.savefig(plot_filename) - - - def get_plot_points(self): - """ - Get the lon and lat points to be plotted, along with any other plotting-relevant - information like the marker, whether this is a first point (to be used in - annotating the first point using the valid day date and valid hour), etc. - - :return: A list of named tuples that represent the points to plot with corresponding - plotting information - """ - - # Create a named tuple to store the point information - PlotPt = namedtuple("PlotPt", "storm_id lon lat is_first marker valid_dd valid_hour annotation") - - points_list = [] - storm_id = self.sanitized_df['STORM_ID'] - lons = self.sanitized_df['SLON'] - lats = self.sanitized_df['ALAT'] - is_first_list = self.sanitized_df['IS_FIRST'] - marker_list = self.sanitized_df['MARKER'] - valid_dd_list = self.sanitized_df['VALID_DD'] - valid_hour_list = self.sanitized_df['VALID_HOUR'] - annotation_list = [] - - for idx, cur_lon in enumerate(lons): - if is_first_list[idx] is True: - annotation = str(valid_dd_list[idx]).zfill(2) + '/' + \ - str(valid_hour_list[idx]).zfill(2) + 'z' - else: - annotation = None - - annotation_list.append(annotation) - cur_pt = PlotPt(storm_id, lons[idx], lats[idx], is_first_list[idx], marker_list[idx], - valid_dd_list[idx], valid_hour_list[idx], annotation) - points_list.append(cur_pt) - - return points_list - - - def get_points_by_track(self): - """ - Get all the lats and lons for each storm track. Used to generate the line - plot of the storm tracks. - - Args: - - :return: - points_by_track: Points aggregated by storm track. - Returns a dictionary where the key is the storm_id - and values are the points (lon,lat) stored in a named tuple - """ - track_dict = {} - LonLat = namedtuple("LonLat", "lon lat") - for cur_unique in self.unique_storm_ids: - # retrieve the ALAT and ALON values that correspond to the rows for a unique storm id. - # i.e. Get the index value(s) corresponding to this unique storm id - idx_list = self.sanitized_df.index[self.sanitized_df['STORM_ID'] == cur_unique].tolist() - sanitized_lons_and_lats = [] - indices = [] - for idx in idx_list: - cur_lonlat = LonLat(self.sanitized_df.loc[idx, 'SLON'], self.sanitized_df.loc[idx, 'ALAT']) - sanitized_lons_and_lats.append(cur_lonlat) - indices.append(idx) - - # update the track dictionary - track_dict[cur_unique] = sanitized_lons_and_lats - - return track_dict - - - def subset_by_region(self, sanitized_df): - """ - Args: - @param: sanitized_df the pandas dataframe containing - the "sanitized" longitudes and other useful - plotting information - - Returns: - :return: - """ - self.logger.debug("Subsetting by region...") - - # Copy the sanitized_df dataframe - sanitized_by_region_df = sanitized_df.copy(deep=True) - - # Iterate over ALL the rows and if any point is within the polygon, - # save it's index so we can create a new dataframe with just the - # relevant data. - for index, row in sanitized_by_region_df.iterrows(): - if (self.west_lon <= row['ALON'] <= self.east_lon) and (self.south_lat <= row['ALAT'] <= self.north_lat): - sanitized_by_region_df.loc[index,'INSIDE'] = True - else: - sanitized_by_region_df.loc[index,'INSIDE'] = False - - # Now filter the input dataframe based on the whether points are inside - # the specified boundaries. - masked = sanitized_by_region_df[sanitized_by_region_df['INSIDE'] == True] - masked.reset_index(drop=True,inplace=True) - - if len(masked) == 0: - sys.exit("No data in region specified, please check your lon and lat values in the config file.") - - return masked - - - @staticmethod - def sanitize_lonlist(lon_list): - """ - Solution from Stack Overflow for "sanitizing" longitudes that cross the International Date Line - https://stackoverflow.com/questions/67730660/plotting-line-across-international-dateline-with-cartopy - - Args: - @param lon_list: A list of longitudes (float) that correspond to a storm track - - Returns: - new_list: a list of "sanitized" lons that are "corrected" for crossing the - International Date Line - """ - - new_list = [] - oldval = 0 - # used to compare adjacent longitudes in a storm track - treshold = 10 - for ix, ea in enumerate(lon_list): - diff = oldval - ea - if (ix > 0): - if (diff > treshold): - ea = ea + 360 - oldval = ea - new_list.append(ea) - - return new_list \ No newline at end of file + +"""!@namespace ExtraTropicalCyclonePlotter +A Python class that generates plots of extra tropical cyclone forecast data, + replicating the NCEP tropical and extra tropical cyclone tracks and + verification plots http://www.emc.ncep.noaa.gov/mmb/gplou/emchurr/glblgen/ +""" + +import os +import time +import datetime +import re +import sys +from collections import namedtuple + + +# handle if module can't be loaded to run wrapper +WRAPPER_CANNOT_RUN = False +EXCEPTION_ERR = '' +try: + import pandas as pd + import matplotlib.pyplot as plt + import matplotlib.ticker as mticker + import cartopy.crs as ccrs + import cartopy.feature as cfeature + import cartopy + from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER + + ##If the script is run on a limited-internet access machine, the CARTOPY_DIR environment setting + ##will need to be set in the user-specific system configuration file. Review the Installation section + ##of the User's Guide for more details. + if os.getenv('CARTOPY_DIR'): + cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir')) + +except Exception as err_msg: + WRAPPER_CANNOT_RUN = True + EXCEPTION_ERR = err_msg + +import produtil.setup + +from ..util import met_util as util +from ..util import do_string_sub +from . import CommandBuilder + + +class CyclonePlotterWrapper(CommandBuilder): + """! Generate plots of extra tropical storm forecast tracks. + Reads input from ATCF files generated from MET TC-Pairs + """ + + def __init__(self, config, instance=None, config_overrides=None): + self.app_name = 'cyclone_plotter' + + super().__init__(config, + instance=instance, + config_overrides=config_overrides) + + if WRAPPER_CANNOT_RUN: + self.log_error("There was a problem importing modules: " + f"{EXCEPTION_ERR}\n") + return + + self.input_data = self.config.getdir('CYCLONE_PLOTTER_INPUT_DIR') + self.output_dir = self.config.getdir('CYCLONE_PLOTTER_OUTPUT_DIR') + self.init_date = self.config.getraw('config', + 'CYCLONE_PLOTTER_INIT_DATE') + self.init_hr = self.config.getraw('config', 'CYCLONE_PLOTTER_INIT_HR') + + init_time_fmt = self.config.getstr('config', 'INIT_TIME_FMT', '') + + if init_time_fmt: + clock_time = datetime.datetime.strptime( + self.config.getstr('config', + 'CLOCK_TIME'), + '%Y%m%d%H%M%S' + ) + + init_beg = self.config.getraw('config', 'INIT_BEG') + if init_beg: + init_beg_dt = util.get_time_obj(init_beg, + init_time_fmt, + clock_time, + logger=self.logger) + self.init_date = do_string_sub(self.init_date, init=init_beg_dt) + self.init_hr = do_string_sub(self.init_hr, init=init_beg_dt) + + self.model = self.config.getstr('config', 'CYCLONE_PLOTTER_MODEL') + self.title = self.config.getstr('config', + 'CYCLONE_PLOTTER_PLOT_TITLE') + self.gen_ascii = ( + self.config.getbool('config', + 'CYCLONE_PLOTTER_GENERATE_TRACK_ASCII') + ) + # Create a set to keep track of unique storm_ids for each track file. + self.unique_storm_id = set() + # Data structure to separate data based on storm id. + self.storm_id_dict = {} + + # Data/info which we want to retrieve from the track files. + self.columns_of_interest = ['AMODEL', 'STORM_ID', 'INIT', + 'LEAD', 'VALID', 'ALAT', 'ALON'] + self.circle_marker_size = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_CIRCLE_MARKER_SIZE') + ) + self.annotation_font_size = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_ANNOTATION_FONT_SIZE') + ) + + self.legend_font_size = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_LEGEND_FONT_SIZE') + ) + + # Map centered on Pacific Ocean + self.central_longitude = 180.0 + + self.cross_marker_size = (self.config.getint('config', + 'CYCLONE_PLOTTER_CROSS_MARKER_SIZE') + ) + self.resolution_dpi = ( + self.config.getint('config', + 'CYCLONE_PLOTTER_RESOLUTION_DPI') + ) + + self.add_watermark = self.config.getbool('config', + 'CYCLONE_PLOTTER_ADD_WATERMARK', + True) + # Set the marker symbols, '+' for a small cross, '.' for a small circle. + # NOTE: originally, 'o' was used. 'o' is also a circle, but it creates a + # very large filled circle on the plots that appears too large. + self.cross_marker = '+' + self.circle_marker = '.' + + # Map extent, global if CYCLONE_PLOTTER_GLOBAL_PLOT is True + self.is_global_extent = (self.config.getbool('config', + 'CYCLONE_PLOTTER_GLOBAL_PLOT') + ) + self.logger.debug(f"global_extent value: {self.is_global_extent}") + + # User-defined map extent, lons and lats + if self.is_global_extent: + self.logger.debug("Global extent") + else: + self.logger.debug("Getting lons and lats that define the plot's extent") + west_lon = (self.config.getstr('config', + 'CYCLONE_PLOTTER_WEST_LON') + ) + east_lon = (self.config.getstr('config', + 'CYCLONE_PLOTTER_EAST_LON') + ) + north_lat = (self.config.getstr('config', + 'CYCLONE_PLOTTER_NORTH_LAT') + ) + south_lat = (self.config.getstr('config', + 'CYCLONE_PLOTTER_SOUTH_LAT') + ) + + # Check for unconfigured lons and lats needed for defining the extent + if not west_lon: + self.logger.error("Missing CYCLONE_PLOTTER_WEST_LON in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_WEST_LON please check config file") + else: + self.west_lon = (float(west_lon)) + if not east_lon: + self.logger.error("Missing CYCLONE_PLOTTER_EAST_LON in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_EAST_LON please check config file") + else: + self.east_lon = (float(east_lon)) + if not south_lat: + self.logger.error("Missing CYCLONE_PLOTTER_SOUTH_LAT in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_SOUTH_LAT please check config file") + else: + self.south_lat = float(south_lat) + if not north_lat: + self.logger.error("Missing CYCLONE_PLOTTER_NORTH_LAT in config file. ") + sys.exit("Missing the CYCLONE_PLOTTER_NORTH_LAT please check config file") + else: + self.north_lat = float(north_lat) + + self.extent_region = [self.west_lon, self.east_lon, self.south_lat, self.north_lat] + self.logger.debug(f"extent region: {self.extent_region}") + + + def run_all_times(self): + """! Calls the defs needed to create the cyclone plots + run_all_times() is required by CommandBuilder. + + """ + self.sanitized_df = self.retrieve_data() + self.create_plot() + + + def retrieve_data(self): + """! Retrieve data from track files. + Returns: + sanitized_df: a pandas dataframe containing the + "sanitized" longitudes, as well as some markers and + lead group information needed for generating + scatter plots. + + """ + self.logger.debug("Begin retrieving data...") + all_tracks_list = [] + + # Store the data in the track list. + if os.path.isdir(self.input_data): + self.logger.debug("Get data from all files in the directory " + + self.input_data) + # Get the list of all files (full file path) in this directory + all_input_files = util.get_files(self.input_data, ".*.tcst", + self.logger) + + # read each file into pandas then concatenate them together + df_list = [pd.read_csv(file, delim_whitespace=True) for file in all_input_files] + combined = pd.concat(df_list, ignore_index=True) + + # check for empty dataframe, set error message and exit + if combined.empty: + self.logger.error("No data found in specified files. Please check your config file settings.") + sys.exit("No data found.") + + # if there are any NaN values in the ALAT, ALON, STORM_ID, LEAD, INIT, AMODEL, or VALID column, + # drop that row of data (axis=0). We need all these columns to contain valid data in order + # to create a meaningful plot. + combined_df = combined.copy(deep=True) + combined_df = combined.dropna(axis=0, how='any', + subset=self.columns_of_interest) + + # Retrieve and create the columns of interest + self.logger.debug(f"Number of rows of data: {combined_df.shape[0]}") + combined_subset = combined_df[self.columns_of_interest] + df = combined_subset.copy(deep=True) + df.allows_duplicate_labels = False + # INIT, LEAD, VALID correspond to the column headers from the MET + # TC tool output. INIT_YMD, INIT_HOUR, VALID_DD, and VALID_HOUR are + # new columns (for a new dataframe) created from these MET columns. + df['INIT'] = df['INIT'].astype(str) + df['INIT_YMD'] = (df['INIT'].str[:8]).astype(int) + df['INIT_HOUR'] = (df['INIT'].str[9:11]).astype(int) + df['LEAD'] = df['LEAD']/10000 + df['LEAD'] = df['LEAD'].astype(int) + df['VALID_DD'] = (df['VALID'].str[6:8]).astype(int) + df['VALID_HOUR'] = (df['VALID'].str[9:11]).astype(int) + df['VALID'] = df['VALID'].astype(int) + + # Subset the dataframe to include only the data relevant to the user's criteria as + # specified in the configuration file. + init_date = int(self.init_date) + init_hh = int(self.init_hr) + model_name = self.model + + if model_name: + self.logger.debug("Subsetting based on " + str(init_date) + " " + str(init_hh) + + ", and model:" + model_name ) + mask = df[(df['AMODEL'] == model_name) & (df['INIT_YMD'] >= init_date) & + (df['INIT_HOUR'] >= init_hh)] + else: + # no model specified, just subset on init date and init hour + mask = df[(df['INIT_YMD'] >= init_date) & + (df['INIT_HOUR'] >= init_hh)] + self.logger.debug("Subsetting based on " + str(init_date) + ", and "+ str(init_hh)) + + user_criteria_df = mask + # reset the index so things are ordered properly in the new dataframe + user_criteria_df.reset_index(inplace=True) + + # Aggregate the ALON values based on unique storm id to facilitate "sanitizing" the + # longitude values (to handle lons that cross the International Date Line). + unique_storm_ids_set = set(user_criteria_df['STORM_ID']) + self.unique_storm_ids = list(unique_storm_ids_set) + nunique = len(self.unique_storm_ids) + self.logger.debug(f" {nunique} unique storm ids identified") + + # Use named tuples to store the relevant storm track information (their index value in the dataframe, + # track id, and ALON values and later on the SLON (sanitized ALON values). + TrackPt = namedtuple("TrackPt", "indices track alons alats") + + # named tuple holding "sanitized" longitudes + SanTrackPt = namedtuple("SanTrackPt", "indices track alons slons") + + # Keep track of the unique storm tracks by their storm_id + storm_track_dict = {} + + for cur_unique in self.unique_storm_ids: + idx_list = user_criteria_df.index[user_criteria_df['STORM_ID'] == cur_unique].tolist() + alons = [] + alats = [] + indices = [] + + for idx in idx_list: + alons.append(user_criteria_df.loc[idx, 'ALON']) + alats.append(user_criteria_df.loc[idx, 'ALAT']) + indices.append(idx) + + # create the track_pt tuple and add it to the storm track dictionary + track_pt = TrackPt(indices, cur_unique, alons, alats) + storm_track_dict[cur_unique] = track_pt + + # create a new dataframe to contain the sanitized lons (i.e. the original ALONs that have + # been cleaned up when crossing the International Date Line) + sanitized_df = user_criteria_df.copy(deep=True) + + # Now we have a dictionary that helps in aggregating the data based on + # storm tracks (via storm id) and will contain the "sanitized" lons + sanitized_storm_tracks = {} + for key in storm_track_dict: + # "Sanitize" the longitudes to shift the lons that cross the International Date Line. + # Create a new SanTrackPt named tuple and add that to a new dictionary + # that keeps track of the sanitized data based on the storm id + # sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) + sanitized_lons = self.sanitize_lonlist(storm_track_dict[key].alons) + sanitized_track_pt = SanTrackPt(storm_track_dict[key].indices, storm_track_dict[key].track, + storm_track_dict[key].alons, sanitized_lons) + sanitized_storm_tracks[key] = sanitized_track_pt + + # fill in the sanitized dataframe, sanitized_df + for key in sanitized_storm_tracks: + # now use the indices of the storm tracks to correctly assign the sanitized + # lons to the appropriate row in the dataframe to maintain the row ordering of + # the original dataframe + idx_list = sanitized_storm_tracks[key].indices + + for i, idx in enumerate(idx_list): + sanitized_df.loc[idx,'SLON'] = sanitized_storm_tracks[key].slons[i] + + # Set some useful values used for plotting. + # Set the IS_FIRST value to True if this is the first + # point in the storm track, False + # otherwise + if i == 0: + sanitized_df.loc[idx, 'IS_FIRST'] = True + else: + sanitized_df.loc[idx, 'IS_FIRST'] = False + + # Set the lead group to the character '0' if the valid hour is 0 or 12, + # or to the charcter '6' if the valid hour is 6 or 18. Set the marker + # to correspond to the valid hour: 'o' (open circle) for 0 or 12 valid hour, + # or '+' (small plus/cross) for 6 or 18. + if sanitized_df.loc[idx, 'VALID_HOUR'] == 0 or sanitized_df.loc[idx, 'VALID_HOUR'] == 12: + sanitized_df.loc[idx, 'LEAD_GROUP'] ='0' + sanitized_df.loc[idx, 'MARKER'] = self.circle_marker + elif sanitized_df.loc[idx, 'VALID_HOUR'] == 6 or sanitized_df.loc[idx, 'VALID_HOUR'] == 18: + sanitized_df.loc[idx, 'LEAD_GROUP'] = '6' + sanitized_df.loc[idx, 'MARKER'] = self.cross_marker + + # If the user has specified a region of interest rather than the + # global extent, subset the data even further to points that are within a bounding box. + if not self.is_global_extent: + self.logger.debug(f"Subset the data based on the region of interest.") + subset_by_region_df = self.subset_by_region(sanitized_df) + final_df = subset_by_region_df.copy(deep=True) + else: + final_df = sanitized_df.copy(deep=True) + + # Write output ASCII file (csv) summarizing the information extracted from the input + # which is used to generate the plot. + if self.gen_ascii: + self.logger.debug(f" output dir: {self.output_dir}") + util.mkdir_p(self.output_dir) + ascii_track_parts = [self.init_date, '.csv'] + ascii_track_output_name = ''.join(ascii_track_parts) + final_df_filename = os.path.join(self.output_dir, ascii_track_output_name) + + # Make sure that the dataframe is sorted by STORM_ID, INIT_YMD, INIT_HOUR, and LEAD + # to ensure that the line plot is connecting the points in the correct order. + final_sorted_df = final_df.sort_values(by=['STORM_ID', 'INIT_YMD', 'INIT_HOUR', 'LEAD'], ignore_index=True) + final_df.reset_index(drop=True,inplace=True) + final_sorted_df.to_csv(final_df_filename) + else: + # The user's specified directory isn't valid, log the error and exit. + self.logger.error("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory, check config file.") + sys.exit("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory.") + + return final_sorted_df + + + def create_plot(self): + """ + Create the plot, using Cartopy + + """ + + # Use PlateCarree projection for scatter plots + # and Geodetic projection for line plots. + cm_lon = self.central_longitude + prj = ccrs.PlateCarree(central_longitude=cm_lon) + ax = plt.axes(projection=prj) + + # for transforming the annotations (matplotlib to cartopy workaround from Stack Overflow) + transform = ccrs.PlateCarree()._as_mpl_transform(ax) + + # Add land, coastlines, and ocean + ax.add_feature(cfeature.LAND) + ax.coastlines() + ax.add_feature(cfeature.OCEAN) + + # keep map zoomed out to full world (ie global extent) if CYCLONE_PLOTTER_GLOBAL_PLOT is + # yes or True, otherwise use the lons and lats defined in the config file to + # create a polygon (rectangular box) defining the region of interest. + if self.is_global_extent: + ax.set_global() + self.logger.debug("Generating a plot of the global extent") + else: + self.logger.debug(f"Generating a plot of the user-defined extent:{self.west_lon}, {self.east_lon}, " + f"{self.south_lat}, {self.north_lat}") + extent_list = [self.west_lon, self.east_lon, self.south_lat, self.north_lat] + self.logger.debug(f"Setting map extent to: {self.west_lon}, {self.east_lon}, {self.south_lat}, {self.north_lat}") + # Bounding box will not necessarily be centered about the 180 degree longitude, so + # DO NOT explicitly set the central longitude. + ax.set_extent(extent_list, ccrs.PlateCarree()) + + # Add grid lines for longitude and latitude + gl = ax.gridlines(crs=ccrs.PlateCarree(), + draw_labels=True, linewidth=1, color='gray', + alpha=0.5, linestyle='--') + + gl.top_labels = False + gl.left_labels = True + gl.xlines = True + gl.xformatter = LONGITUDE_FORMATTER + gl.yformatter = LATITUDE_FORMATTER + gl.xlabel_style = {'size': 9, 'color': 'blue'} + gl.xlabel_style = {'color': 'black', 'weight': 'normal'} + + # Plot title + plt.title(self.title + "\nFor forecast with initial time = " + + self.init_date) + + # Optional: Create the NCAR watermark with a timestamp + # This will appear in the bottom right corner of the plot, below + # the x-axis. NOTE: The timestamp is in the user's local time zone + # and not in UTC time. + if self.add_watermark: + ts = time.time() + st = datetime.datetime.fromtimestamp(ts).strftime( + '%Y-%m-%d %H:%M:%S') + watermark = 'DTC METplus\nplot created at: ' + st + plt.text(60, -130, watermark, fontsize=5, alpha=0.25) + + # Make sure the output directory exists, and create it if it doesn't. + util.mkdir_p(self.output_dir) + + # get the points for the scatter plots (and the relevant information for annotations, etc.) + points_list = self.get_plot_points() + + # Legend labels + lead_group_0_legend = "Indicates a position at 00 or 12 UTC" + lead_group_6_legend = "Indicates a position at 06 or 18 UTC" + + # to be consistent with the NOAA website, use red for annotations, markers, and lines. + pt_color = 'red' + cross_marker_size = self.cross_marker_size + circle_marker_size = self.circle_marker_size + + # Get all the lat and lon (i.e. x and y) points for the '+' and 'o' marker types + # to be used in generating the scatter plots (one for the 0/12 hr and one for the 6/18 hr lead + # groups). Also collect ALL the lons and lats, which will be used to generate the + # line plot (the last plot that goes on top of all the scatter plots). + cross_lons = [] + cross_lats = [] + cross_annotations = [] + circle_lons = [] + circle_lats = [] + circle_annotations = [] + + for idx,pt in enumerate(points_list): + if pt.marker == self.cross_marker: + cross_lons.append(pt.lon) + cross_lats.append(pt.lat) + cross_annotations.append(pt.annotation) + # cross_marker = pt.marker + elif pt.marker == self.circle_marker: + circle_lons.append(pt.lon) + circle_lats.append(pt.lat) + circle_annotations.append(pt.annotation) + # circle_marker = pt.marker + + # Now generate the scatter plots for the lead group 0/12 hr ('+' marker) and the + # lead group 6/18 hr ('.' marker). + plt.scatter(circle_lons, circle_lats, s=self.circle_marker_size, c=pt_color, + marker=self.circle_marker, zorder=2, label=lead_group_0_legend, transform=ccrs.PlateCarree()) + plt.scatter(cross_lons, cross_lats, s=self.cross_marker_size, c=pt_color, + marker=self.cross_marker, zorder=2, label=lead_group_6_legend, transform=ccrs.PlateCarree()) + + # annotations for the scatter plots + counter = 0 + for x,y in zip(circle_lons, circle_lats): + plt.annotate(circle_annotations[counter], (x,y+1), xycoords=transform, color=pt_color, + fontsize=self.annotation_font_size) + counter += 1 + + counter = 0 + for x, y in zip(cross_lons, cross_lats): + plt.annotate(cross_annotations[counter], (x, y + 1), xycoords=transform, color=pt_color, + fontsize=self.annotation_font_size) + counter += 1 + + # Dummy point to add the additional label explaining the labelling of the first + # point in the storm track + plt.scatter(0, 0, zorder=2, marker=None, c='', + label="Date (dd/hhz) is the first " + + "time storm was able to be tracked in model") + + # Settings for the legend box location. + ax.legend(loc='lower left', bbox_to_anchor=(0, -0.4), + fancybox=True, shadow=True, scatterpoints=1, + prop={'size':self.legend_font_size}) + + # Generate the line plot + # First collect all the lats and lons for each storm track. Then for each storm track, + # generate a line plot. + pts_by_track_dict = self.get_points_by_track() + + for key in pts_by_track_dict: + lons = [] + lats = [] + for idx, pt in enumerate(pts_by_track_dict[key]): + lons.append(pt.lon) + lats.append(pt.lat) + + # Create the line plot for the current storm track, use the Geodetic coordinate reference system + # to correctly connect adjacent points that have been sanitized and cross the + # International Date line or the Prime Meridian. + plt.plot(lons, lats, linestyle='-', color=pt_color, linewidth=.4, transform=ccrs.Geodetic(), zorder=3) + + # Write the plot to the output directory + out_filename_parts = [self.init_date, '.png'] + output_plot_name = ''.join(out_filename_parts) + plot_filename = os.path.join(self.output_dir, output_plot_name) + if self.resolution_dpi > 0: + plt.savefig(plot_filename, dpi=self.resolution_dpi) + else: + # use Matplotlib's default if no resolution is set in config file + plt.savefig(plot_filename) + + + def get_plot_points(self): + """ + Get the lon and lat points to be plotted, along with any other plotting-relevant + information like the marker, whether this is a first point (to be used in + annotating the first point using the valid day date and valid hour), etc. + + :return: A list of named tuples that represent the points to plot with corresponding + plotting information + """ + + # Create a named tuple to store the point information + PlotPt = namedtuple("PlotPt", "storm_id lon lat is_first marker valid_dd valid_hour annotation") + + points_list = [] + storm_id = self.sanitized_df['STORM_ID'] + lons = self.sanitized_df['SLON'] + lats = self.sanitized_df['ALAT'] + is_first_list = self.sanitized_df['IS_FIRST'] + marker_list = self.sanitized_df['MARKER'] + valid_dd_list = self.sanitized_df['VALID_DD'] + valid_hour_list = self.sanitized_df['VALID_HOUR'] + annotation_list = [] + + for idx, cur_lon in enumerate(lons): + if is_first_list[idx] is True: + annotation = str(valid_dd_list[idx]).zfill(2) + '/' + \ + str(valid_hour_list[idx]).zfill(2) + 'z' + else: + annotation = None + + annotation_list.append(annotation) + cur_pt = PlotPt(storm_id, lons[idx], lats[idx], is_first_list[idx], marker_list[idx], + valid_dd_list[idx], valid_hour_list[idx], annotation) + points_list.append(cur_pt) + + return points_list + + + def get_points_by_track(self): + """ + Get all the lats and lons for each storm track. Used to generate the line + plot of the storm tracks. + + Args: + + :return: + points_by_track: Points aggregated by storm track. + Returns a dictionary where the key is the storm_id + and values are the points (lon,lat) stored in a named tuple + """ + track_dict = {} + LonLat = namedtuple("LonLat", "lon lat") + for cur_unique in self.unique_storm_ids: + # retrieve the ALAT and ALON values that correspond to the rows for a unique storm id. + # i.e. Get the index value(s) corresponding to this unique storm id + idx_list = self.sanitized_df.index[self.sanitized_df['STORM_ID'] == cur_unique].tolist() + sanitized_lons_and_lats = [] + indices = [] + for idx in idx_list: + cur_lonlat = LonLat(self.sanitized_df.loc[idx, 'SLON'], self.sanitized_df.loc[idx, 'ALAT']) + sanitized_lons_and_lats.append(cur_lonlat) + indices.append(idx) + + # update the track dictionary + track_dict[cur_unique] = sanitized_lons_and_lats + + return track_dict + + + def subset_by_region(self, sanitized_df): + """ + Args: + @param: sanitized_df the pandas dataframe containing + the "sanitized" longitudes and other useful + plotting information + + Returns: + :return: + """ + self.logger.debug("Subsetting by region...") + + # Copy the sanitized_df dataframe + sanitized_by_region_df = sanitized_df.copy(deep=True) + + # Iterate over ALL the rows and if any point is within the polygon, + # save it's index so we can create a new dataframe with just the + # relevant data. + for index, row in sanitized_by_region_df.iterrows(): + if (self.west_lon <= row['ALON'] <= self.east_lon) and (self.south_lat <= row['ALAT'] <= self.north_lat): + sanitized_by_region_df.loc[index,'INSIDE'] = True + else: + sanitized_by_region_df.loc[index,'INSIDE'] = False + + # Now filter the input dataframe based on the whether points are inside + # the specified boundaries. + masked = sanitized_by_region_df[sanitized_by_region_df['INSIDE'] == True] + masked.reset_index(drop=True,inplace=True) + + if len(masked) == 0: + sys.exit("No data in region specified, please check your lon and lat values in the config file.") + + return masked + + + @staticmethod + def sanitize_lonlist(lon_list): + """ + Solution from Stack Overflow for "sanitizing" longitudes that cross the International Date Line + https://stackoverflow.com/questions/67730660/plotting-line-across-international-dateline-with-cartopy + + Args: + @param lon_list: A list of longitudes (float) that correspond to a storm track + + Returns: + new_list: a list of "sanitized" lons that are "corrected" for crossing the + International Date Line + """ + + new_list = [] + oldval = 0 + # used to compare adjacent longitudes in a storm track + treshold = 10 + for ix, ea in enumerate(lon_list): + diff = oldval - ea + if (ix > 0): + if (diff > treshold): + ea = ea + 360 + oldval = ea + new_list.append(ea) + + return new_list From 9a4df62a9ffc5939896fe8b2312daeff4b01dc85 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 3 Nov 2021 15:52:12 -0600 Subject: [PATCH 138/770] adding fixing spacing #1049 --- docs/Users_Guide/statistics_list.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e800052dc7..8a2b1b77be 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -514,6 +514,7 @@ METplus Database of Statistics - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` + :raw-html:`
` - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` From c4d04de1090f510064059679304a2693c2876e3b Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 3 Nov 2021 15:57:47 -0600 Subject: [PATCH 139/770] adding fixing spacing take 2 #1049 --- docs/Users_Guide/statistics_list.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8a2b1b77be..66c29985be 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -490,8 +490,8 @@ METplus Database of Statistics - F_SPEED :raw-html:`
` _BAR - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat:raw-html:`
` + Grid-Stat - VL1L2 * - Mean(f-c) - FABAR @@ -514,7 +514,7 @@ METplus Database of Statistics - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` - :raw-html:`
` + :raw-html:`
` - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` From 8d444e2651820ed8ac2c22407a65397557903888 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:01:41 -0600 Subject: [PATCH 140/770] return None from function instead of exiting so that METplus clean up functionality will still run --- metplus/wrappers/cyclone_plotter_wrapper.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index 7a5870e4b5..9c012e562e 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -188,6 +188,8 @@ def run_all_times(self): """ self.sanitized_df = self.retrieve_data() + if not self.sanitized_df: + return None self.create_plot() @@ -218,7 +220,7 @@ def retrieve_data(self): # check for empty dataframe, set error message and exit if combined.empty: self.logger.error("No data found in specified files. Please check your config file settings.") - sys.exit("No data found.") + return None # if there are any NaN values in the ALAT, ALON, STORM_ID, LEAD, INIT, AMODEL, or VALID column, # drop that row of data (axis=0). We need all these columns to contain valid data in order @@ -370,7 +372,7 @@ def retrieve_data(self): else: # The user's specified directory isn't valid, log the error and exit. self.logger.error("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory, check config file.") - sys.exit("CYCLONE_PLOTTER_INPUT_DIR isn't a valid directory.") + return None return final_sorted_df From f9480e8ebfb3719e0151df724afacee8ced122df Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:02:17 -0600 Subject: [PATCH 141/770] fixed indentation --- metplus/wrappers/cyclone_plotter_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index 9c012e562e..05df085166 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -211,7 +211,7 @@ def retrieve_data(self): self.input_data) # Get the list of all files (full file path) in this directory all_input_files = util.get_files(self.input_data, ".*.tcst", - self.logger) + self.logger) # read each file into pandas then concatenate them together df_list = [pd.read_csv(file, delim_whitespace=True) for file in all_input_files] From dbdec0b35de01ce3ee4a4bce3ba7b1e5c330d4a3 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:03:53 -0600 Subject: [PATCH 142/770] fixed indentation - for loop should not be nested inside other for loop --- metplus/wrappers/cyclone_plotter_wrapper.py | 58 ++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index 05df085166..8a8e7234cd 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -316,35 +316,35 @@ def retrieve_data(self): storm_track_dict[key].alons, sanitized_lons) sanitized_storm_tracks[key] = sanitized_track_pt - # fill in the sanitized dataframe, sanitized_df - for key in sanitized_storm_tracks: - # now use the indices of the storm tracks to correctly assign the sanitized - # lons to the appropriate row in the dataframe to maintain the row ordering of - # the original dataframe - idx_list = sanitized_storm_tracks[key].indices - - for i, idx in enumerate(idx_list): - sanitized_df.loc[idx,'SLON'] = sanitized_storm_tracks[key].slons[i] - - # Set some useful values used for plotting. - # Set the IS_FIRST value to True if this is the first - # point in the storm track, False - # otherwise - if i == 0: - sanitized_df.loc[idx, 'IS_FIRST'] = True - else: - sanitized_df.loc[idx, 'IS_FIRST'] = False - - # Set the lead group to the character '0' if the valid hour is 0 or 12, - # or to the charcter '6' if the valid hour is 6 or 18. Set the marker - # to correspond to the valid hour: 'o' (open circle) for 0 or 12 valid hour, - # or '+' (small plus/cross) for 6 or 18. - if sanitized_df.loc[idx, 'VALID_HOUR'] == 0 or sanitized_df.loc[idx, 'VALID_HOUR'] == 12: - sanitized_df.loc[idx, 'LEAD_GROUP'] ='0' - sanitized_df.loc[idx, 'MARKER'] = self.circle_marker - elif sanitized_df.loc[idx, 'VALID_HOUR'] == 6 or sanitized_df.loc[idx, 'VALID_HOUR'] == 18: - sanitized_df.loc[idx, 'LEAD_GROUP'] = '6' - sanitized_df.loc[idx, 'MARKER'] = self.cross_marker + # fill in the sanitized dataframe, sanitized_df + for key in sanitized_storm_tracks: + # now use the indices of the storm tracks to correctly assign the sanitized + # lons to the appropriate row in the dataframe to maintain the row ordering of + # the original dataframe + idx_list = sanitized_storm_tracks[key].indices + + for i, idx in enumerate(idx_list): + sanitized_df.loc[idx,'SLON'] = sanitized_storm_tracks[key].slons[i] + + # Set some useful values used for plotting. + # Set the IS_FIRST value to True if this is the first + # point in the storm track, False + # otherwise + if i == 0: + sanitized_df.loc[idx, 'IS_FIRST'] = True + else: + sanitized_df.loc[idx, 'IS_FIRST'] = False + + # Set the lead group to the character '0' if the valid hour is 0 or 12, + # or to the charcter '6' if the valid hour is 6 or 18. Set the marker + # to correspond to the valid hour: 'o' (open circle) for 0 or 12 valid hour, + # or '+' (small plus/cross) for 6 or 18. + if sanitized_df.loc[idx, 'VALID_HOUR'] == 0 or sanitized_df.loc[idx, 'VALID_HOUR'] == 12: + sanitized_df.loc[idx, 'LEAD_GROUP'] ='0' + sanitized_df.loc[idx, 'MARKER'] = self.circle_marker + elif sanitized_df.loc[idx, 'VALID_HOUR'] == 6 or sanitized_df.loc[idx, 'VALID_HOUR'] == 18: + sanitized_df.loc[idx, 'LEAD_GROUP'] = '6' + sanitized_df.loc[idx, 'MARKER'] = self.cross_marker # If the user has specified a region of interest rather than the # global extent, subset the data even further to points that are within a bounding box. From 4ec50af4c393d3f85985ab515722e0ce14df3524 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:34:27 -0600 Subject: [PATCH 143/770] fixed check for failure in retrieve_data function --- metplus/wrappers/cyclone_plotter_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index 8a8e7234cd..b05e76ded2 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -188,7 +188,7 @@ def run_all_times(self): """ self.sanitized_df = self.retrieve_data() - if not self.sanitized_df: + if self.sanitized_df is None: return None self.create_plot() From 54f368a991285a9d5afd99a3f48aa88d3ea3d0c8 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 4 Nov 2021 10:09:39 -0600 Subject: [PATCH 144/770] feature 1223 error if file not found (#1238) --- metplus/util/met_util.py | 2 +- metplus/wrappers/tcmpr_plotter_wrapper.py | 2 +- .../read_ascii_storm.py | 165 +++++++++--------- 3 files changed, 83 insertions(+), 86 deletions(-) diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index 7ebd0b3862..4e5118b6a3 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -1516,7 +1516,7 @@ def get_files(filedir, filename_regex, logger=None): file_paths.append(filepath) else: continue - return file_paths + return sorted(file_paths) def prune_empty(output_dir, logger): """! Start from the output_dir, and recursively check diff --git a/metplus/wrappers/tcmpr_plotter_wrapper.py b/metplus/wrappers/tcmpr_plotter_wrapper.py index 4f574ec606..371b1a8417 100755 --- a/metplus/wrappers/tcmpr_plotter_wrapper.py +++ b/metplus/wrappers/tcmpr_plotter_wrapper.py @@ -283,7 +283,7 @@ def get_input_files(self): input_files = util.get_files(input_data, ".*.tcst") self.logger.debug(f"Number of files: {len(input_files)}") - return sorted(input_files) + return input_files def format_arg_string(self): arg_list = [] diff --git a/parm/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect/read_ascii_storm.py b/parm/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect/read_ascii_storm.py index a46a141ff2..1340e3b274 100644 --- a/parm/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect/read_ascii_storm.py +++ b/parm/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect/read_ascii_storm.py @@ -2,92 +2,89 @@ import pandas as pd import os import sys -import ntpath -######################################################################## +print(f'Python Script: {sys.argv[0]}') + +# input file specified on the command line +# load the data into the numpy array -print('Python Script:\t', sys.argv[0]) - - ## - ## input file specified on the command line - ## load the data into the numpy array - ## - -if len(sys.argv) == 2: - # Read the input file as the first argument - input_file = os.path.expandvars(sys.argv[1]) - try: - print("Input File:\t" + repr(input_file)) - - # Read and format the input 11-column observations: - # (1) string: Message_Type - # (2) string: Station_ID - # (3) string: Valid_Time(YYYYMMDD_HHMMSS) - # (4) numeric: Lat(Deg North) - # (5) numeric: Lon(Deg East) - # (6) numeric: Elevation(msl) - # (7) string: Var_Name(or GRIB_Code) - # (8) numeric: Level - # (9) numeric: Height(msl or agl) - # (10) string: QC_String - # (11) numeric: Observation_Value - - column_names = ["Message_Type","Station_ID","Valid_Time","Lat","Lon","Elevation","Var_Name","Level","Height","QC_String","Observation_Value"] - - # Create a blank dataframe based on the 11 column standard - point_frame = pd.DataFrame(columns=column_names,dtype='str') - - #Read in the Storm report, 8 columns not matching the 11 column standard - temp_data = pd.read_csv(input_file,names=['Time', 'Fscale', 'Location', 'County','Stat','Lat', 'Lon', 'Comment'], dtype=str ,skiprows=1) - - #Strip out any rows in the middle that are actually header rows - #Allows for concatenating storm reports together - temp_data = temp_data[temp_data["Time"] != "Time"] - - #Change some columns to floats and ints - temp_data[["Lat","Lon"]] = temp_data[["Lat","Lon"]].apply(pd.to_numeric) - - #Assign approprite columns to point_frame leaving missing as empty strings - point_frame["Lat"] = temp_data["Lat"] - point_frame["Lon"] = temp_data["Lon"] - #point_frame["Station_ID"] = temp_data["County"] - point_frame["Station_ID"] = "NA" - point_frame["Var_Name"] = "Fscale" - point_frame["Message_Type"] = "StormReport" - - #Assign 0.0 values to numeric point_frame columns that we don't have in the csv file - point_frame["Elevation"] = 0.0 - point_frame["Level"] = 0.0 - point_frame["Height"] = 0.0 - - #Change Comments into a "QC" string Tornado=1, Hail=2, Wind=3, Other=4 - point_frame["QC_String"] = "4" - mask = temp_data["Comment"].str.contains('TORNADO') - point_frame.loc[mask,"QC_String"] = "1" - mask = temp_data["Comment"].str.contains('HAIL') - point_frame.loc[mask,"QC_String"] = "2" - mask = temp_data["Comment"].str.contains('WIND') - point_frame.loc[mask,"QC_String"] = "3" - - #Time is HHMM in the csv file so we need to use a piece of the filename and - #this value to create a valid date string - file_without_path = ntpath.basename(input_file) - year_month_day = "20"+file_without_path[0:6] - point_frame["Valid_Time"] = year_month_day+"_"+temp_data["Time"]+"00" - - #Currently we are only interested in the fact that we have a report at that locaton - #and not its actual value so all values are 1.0 - point_frame["Observation_Value"] = 1.0 - - #Ascii2nc wants the final values in a list - point_data = point_frame.values.tolist() - - print("Data Length:\t" + repr(len(point_data))) - print("Data Type:\t" + repr(type(point_data))) - except NameError: - print("Can't find the input file") -else: - print("ERROR: read_ascii_storm.py -> Must specify exactly one input file.") +if len(sys.argv) < 2: + script_name = os.path.basename(sys.argv[0]) + print(f"ERROR: {script_name} -> Must specify exactly one input file.") sys.exit(1) +# Read the input file as the first argument +input_file = os.path.expandvars(sys.argv[1]) +print(f'Input File: {input_file}') + +if not os.path.exists(input_file): + print("ERROR: Could not find input file") + sys.exit(2) + +# Read and format the input 11-column observations +COLUMN_NAMES = ( + "Message_Type", # (1) string + "Station_ID", # (2) string + "Valid_Time", # (3) string (YYYYMMDD_HHMMSS) + "Lat", # (4) numeric (Deg North) + "Lon", # (5) numeric (Deg East) + "Elevation", # (6) numeric (msl) + "Var_Name", # (7) string (or GRIB_Code) + "Level", # (8) numeric + "Height", # (9) numeric (msl or agl) + "QC_String", # (10) string + "Observation_Value" # (11) numeric +) + +# Create a blank dataframe based on the 11 column standard +point_frame = pd.DataFrame(columns=COLUMN_NAMES,dtype='str') + +#Read in the Storm report, 8 columns not matching the 11 column standard +temp_data = pd.read_csv(input_file,names=['Time', 'Fscale', 'Location', 'County','Stat','Lat', 'Lon', 'Comment'], dtype=str ,skiprows=1) + +#Strip out any rows in the middle that are actually header rows +#Allows for concatenating storm reports together +temp_data = temp_data[temp_data["Time"] != "Time"] + +#Change some columns to floats and ints +temp_data[["Lat","Lon"]] = temp_data[["Lat","Lon"]].apply(pd.to_numeric) + +#Assign approprite columns to point_frame leaving missing as empty strings +point_frame["Lat"] = temp_data["Lat"] +point_frame["Lon"] = temp_data["Lon"] +#point_frame["Station_ID"] = temp_data["County"] +point_frame["Station_ID"] = "NA" +point_frame["Var_Name"] = "Fscale" +point_frame["Message_Type"] = "StormReport" + +#Assign 0.0 values to numeric point_frame columns that we don't have in the csv file +point_frame["Elevation"] = 0.0 +point_frame["Level"] = 0.0 +point_frame["Height"] = 0.0 + +#Change Comments into a "QC" string Tornado=1, Hail=2, Wind=3, Other=4 +point_frame["QC_String"] = "4" +mask = temp_data["Comment"].str.contains('TORNADO') +point_frame.loc[mask,"QC_String"] = "1" +mask = temp_data["Comment"].str.contains('HAIL') +point_frame.loc[mask,"QC_String"] = "2" +mask = temp_data["Comment"].str.contains('WIND') +point_frame.loc[mask,"QC_String"] = "3" + +#Time is HHMM in the csv file so we need to use a piece of the filename and +#this value to create a valid date string +file_without_path = os.path.basename(input_file) +year_month_day = "20"+file_without_path[0:6] +point_frame["Valid_Time"] = year_month_day+"_"+temp_data["Time"]+"00" + +#Currently we are only interested in the fact that we have a report at that locaton +#and not its actual value so all values are 1.0 +point_frame["Observation_Value"] = 1.0 + +#Ascii2nc wants the final values in a list +point_data = point_frame.values.tolist() + +print("Data Length:\t" + repr(len(point_data))) +print("Data Type:\t" + repr(type(point_data))) + ######################################################################## From b8673a3c64122ed8e3342096ff0ecb6539684386 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 10:46:05 -0600 Subject: [PATCH 145/770] adding fixing spacing take 3 #1049 --- docs/Users_Guide/statistics_list.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 66c29985be..2a5f40544d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -514,7 +514,6 @@ METplus Database of Statistics - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` - :raw-html:`
` - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` From a16581dd5efe44a9596a2b3b6a285453bf5c75ca Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 10:57:26 -0600 Subject: [PATCH 146/770] adding fixing spacing take 4 #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2a5f40544d..8957d2d743 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -490,7 +490,7 @@ METplus Database of Statistics - F_SPEED :raw-html:`
` _BAR - - - Point-Stat:raw-html:`
` + - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 * - Mean(f-c) @@ -514,6 +514,7 @@ METplus Database of Statistics - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` + - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` From 45909133d448e6081dc05db26011079eb9349a3e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 10:59:19 -0600 Subject: [PATCH 147/770] adding fixing spacing take 5 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8957d2d743..0234229876 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -531,8 +531,8 @@ METplus Database of Statistics wind vector - FBAR_SPEED - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat - VCNT * - Frequency Bias - FBIAS From 5f0ebdde24ea87380a170c08c423b9ac9f2c9a9f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 11:00:37 -0600 Subject: [PATCH 148/770] adding fixing spacing take 6 #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0234229876..4b8bf708b7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -496,8 +496,8 @@ METplus Database of Statistics * - Mean(f-c) - FABAR - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat - SAL1L2 * - False alarm ratio - FAR From 8adbc8a57c6634cff49b9636880e5c2a6fac551c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 11:14:57 -0600 Subject: [PATCH 149/770] adding fixing spacing take 7 #1049 --- docs/Users_Guide/statistics_list.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4b8bf708b7..a7ef0ca828 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -514,7 +514,7 @@ METplus Database of Statistics - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` - + . - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` @@ -529,7 +529,8 @@ METplus Database of Statistics * - Length (speed) of the :raw-html:`
` average forecast :raw-html:`
` wind vector - - FBAR_SPEED + - FBAR :raw-html:`
` + _SPEED - - Point-Stat :raw-html:`
` Grid-Stat From fe14c4f841c03938372bdf3cfba1aa3121528176 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 11:25:46 -0600 Subject: [PATCH 150/770] fixing spacing with a period take 7 #1049 --- docs/Users_Guide/statistics_list.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index a7ef0ca828..d871e772a5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -538,14 +538,15 @@ METplus Database of Statistics * - Frequency Bias - FBIAS - - - Point-Stat :raw-html:`
` - Wavelet-Stat :raw-html:`
` + - Wavelet-Stat :raw-html:`
` MODE :raw-html:`
` - Grid-Stat - - CTS :raw-html:`
` - ISC :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + . + - ISC :raw-html:`
` MODE :raw-html:`
` DMAP :raw-html:`
` + CTS :raw-html:`
` NBRCTCS * - Fractions Brier Score - FBS From a5d9ab9f13b3af08ccc49a9d7587800076e6ec7c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 12:09:57 -0600 Subject: [PATCH 151/770] first attempt fcst_ #1049 --- docs/Users_Guide/statistics_list.rst | 136 +++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index d871e772a5..87a5f052ef 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -630,3 +630,139 @@ METplus Database of Statistics - - MODE - MODE netCDF variables + * - Simple forecast object :raw-html:`
` + id number for each :raw-html:`
` + grid point + - fcst_obj :raw-html:`
` + _id + - + - MODE + - MODE netCDF variables + * - Forecast Object Raw :raw-html:`
` + Values + - fcst_obj :raw-html:`
` + _raw + - + - MODE + - MODE netCDF variables + * - Forecast raw values + - fcst_raw + - + - MODE + - MODE netCDF variables + * - Number of simple :raw-html:`
` + forecast objects + - fcst_simp + - + - MODE + - MODE netCDF dimensions + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of all of the simple :raw-html:`
` + forecast objects + - fcst_simp :raw-html:`
` + _bdy + - + - MODE + - MODE netCDF dimensions + * - Forecast Simple :raw-html:`
` + Boundary PoLatitude + - fcst_simp :raw-html:`
` + _bdy_lat + - + - MODE + - MODE netCDF variables + * - Forecast Simple :raw-html:`
` + Boundary PoLongitude + - fcst_simp :raw-html:`
` + _bdy_lon + - + - MODE + - MODE netCDF variables + * - Number of Forecast :raw-html:`
` + Simple Boundary Points + - fcst_simp :raw-html:`
` + _bdy_npts + - + - MODE + - MODE netCDF variables + * - Forecast Simple :raw-html:`
` + Boundary Starting Index + - fcst_simp :raw-html:`
` + _bdy_start + - + - MODE + - MODE netCDF variables + * - Forecast Simple :raw-html:`
` + Boundary PoX-Coordinate + - fcst_simp :raw-html:`
` + _bdy_x + - + - MODE + - MODE netCDF variables + * - Forecast Simple :raw-html:`
` + Boundary PoY-Coordinate + - fcst_simp :raw-html:`
` + _bdy_y + - + - MODE + - MODE netCDF variables + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the simple forecast :raw-html:`
` + objects + - fcst_simp :raw-html:`
` + _hull + - + - MODE + - MODE netCDF dimensions + * - Forecast Simple Convex :raw-html:`
` + Hull Point Latitude + - fcst_simp :raw-html:`
` + _hull_lat + - + - MODE + - MODE netCDF variables + * - Forecast Simple Convex :raw-html:`
` + Hull Point Longitude + - fcst_simp :raw-html:`
` + _hull_lon + - + - MODE + - MODE netCDF variables + * - Number of Forecast :raw-html:`
` + Simple Convex Hull Points + - fcst_simp :raw-html:`
` + _hull_npts + - + - MODE + - MODE netCDF variables + * - Forecast Simple Convex :raw-html:`
` + Hull Starting Index + - fcst_simp :raw-html:`
` + _hull_start + - + - MODE + - MODE netCDF variables + * - Forecast Simple Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_simp :raw-html:`
` + _hull_x + - + - MODE + - MODE netCDF variables + * - Forecast Simple Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_simp :raw-html:`
` + _hull_y + - + - MODE + - MODE netCDF variables + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst :raw-html:`
` + _thresh :raw-html:`
` + _length + - + - MODE + - MODE netCDF dimensions From 419fe14717ec78f50027e7adbb2d2a563e923a62 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 13:38:08 -0600 Subject: [PATCH 152/770] fixing typos #1049 --- docs/Users_Guide/statistics_list.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 87a5f052ef..2e4714effd 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -187,8 +187,8 @@ METplus Database of Statistics - CALIBRATION :raw-html:`
` _i - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat - PJC * - Total great circle distance :raw-html:`
` travelled by the 2D spatial :raw-html:`
` @@ -382,8 +382,8 @@ METplus Database of Statistics * - Absolute value - DIR_ABSERR - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat - VCNT * - Signed angle between :raw-html:`
` the directions of the :raw-html:`
` @@ -391,8 +391,8 @@ METplus Database of Statistics observed wing vectors - DIR_ERR - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat - VCNT * - Difference in object :raw-html:`
` direction of movement @@ -413,8 +413,8 @@ METplus Database of Statistics used for MCTS HSS_EC - EC_VALUE - - - Point-Stat - Grid-Stat :raw-html:`
` + - Point-Stat :raw-html:`
` + Grid-Stat - MCTC * - Extreme Dependency Index :raw-html:`
` including normal and :raw-html:`
` From ffb0e9f34c8d7aad204673cbccdfda90a4bab6b7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 14:00:15 -0600 Subject: [PATCH 153/770] thru FOBAR #1049 --- docs/Users_Guide/statistics_list.rst | 110 ++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2e4714effd..fd9acc83d7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -765,4 +765,112 @@ METplus Database of Statistics _length - - MODE - - MODE netCDF dimensions + - MODE netCDF dimensions + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst_thresh :raw-html:`
` + _length + - + - MODE + - MODE netCDF dimensions + * - Direction of the average :raw-html:`
` + forecast wind vector + - FDIR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Forecast energy squared :raw-html:`
` + for this scale + - FENERGY + - + - Wavelet-Stat + - ISC + * - Mean((f-c)²) + - FFABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average of forecast :raw-html:`
` + squared. [Mean(f²) :raw-html:`
` + Grid-Stat] + - FFBAR + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + SL1L2 + * - Mean of absolute value :raw-html:`
` + of forecast gradients + - FGBAR + - + - Grid-Stat + - GRAD + * - Ratio of forecast and :raw-html:`
` + observed gradients + - FGOG_RATIO + - + - Grid-Stat + - GRAD + * - Count of events in :raw-html:`
` + forecast category i and :raw-html:`
` + observation category j + - Fi_Oj + - + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTC + * - Forecast mean + - FMEAN + - + - MODE :raw-html:`
` + Grid-Stat :raw-html:`
` + Point-Stat + - MODE :raw-html:`
` + NBRCTCS :raw-html:`
` + CTS + * - Number of forecast no :raw-html:`
` + and observation no + - FN_ON + - + - MODE :raw-html:`
` + Grid-Stat :raw-html:`
` + Point-Stat + - MODE :raw-html:`
` + NBRCTC :raw-html:`
` + CTC + * - Number of forecast no :raw-html:`
` + and observation yes + - FN_OY + - + - MODE :raw-html:`
` + Grid-Stat :raw-html:`
` + Point-Stat + - MODE :raw-html:`
` + NBRCTC :raw-html:`
` + CTC + * - Attributes for pairs of :raw-html:`
` + simple forecast and :raw-html:`
` + observation objects + - FNNN_ONNN + - + - MODE + - MODE ascii object + * - Mean((f-c)*(o-c)) + - FOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average product of :raw-html:`
` + forecast and observation :raw-html:`
` + / Mean(f*o) + - FOBAR + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + SL1L2 From 6d7a98ae7bd270fbf3735492ed5bd10074feb5dd Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 15:15:04 -0600 Subject: [PATCH 154/770] thru end of F #1049 --- docs/Users_Guide/statistics_list.rst | 91 +++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index fd9acc83d7..e0db3644b1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -872,5 +872,94 @@ METplus Database of Statistics - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - - SSVAR :raw-html:`
` + - SSVAR :raw-html:`
` SL1L2 + * - Pratt’s Figure of Merit :raw-html:`
` + from observation to :raw-html:`
` + forecast + - FOM_FO + - + - Grid-Stat + - DMAP + * - Maximum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MAX + - + - Grid-Stat + - DMAP + * - Mean of FOM_FO and FOM_OF + - FOM_MEAN + - + - Grid-Stat + - DMAP + * - Minimum of FOM_FO and FOM_OF + - FOM_MIN + - + - Grid-Stat + - DMAP + * - Pratt’s Figure of Merit :raw-html:`
` + from forecast to :raw-html:`
` + observation + - FOM_OF + - + - Grid-Stat + - DMAP + * - Number of tied forecast :raw-html:`
` + ranks used in computing :raw-html:`
` + Kendall’s tau statistic + - FRANK_TIES + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Root mean square forecast :raw-html:`
` + wind speed + - FS_RMS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Fractions Skill Score :raw-html:`
` + including bootstrap upper :raw-html:`
` + and lower confidence limits + - FSS + - + - Grid-Stat + - NBRCNT + * - Standard deviation of the :raw-html:`
` + error including normal :raw-html:`
` + upper and lower :raw-html:`
` + confidence limits + - FSTDEV + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + VCNT + * - Number of forecast events + - FY + - + - Grid-Stat + - DMAP + * - Number of forecast yes :raw-html:`
` + and observation no + - FY_ON + - + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + CTC :raw-html:`
` + NBRCTC + * - Number of forecast yes :raw-html:`
` + and observation yes + - FY_OY + - + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + CTC :raw-html:`
` + NBRCTC From 728151b88e3b86d8b482ab69797e7a3f57ddc3a7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 15:42:10 -0600 Subject: [PATCH 155/770] g thru h #1049 --- docs/Users_Guide/statistics_list.rst | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e0db3644b1..6464e531eb 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -963,3 +963,72 @@ METplus Database of Statistics - MODE :raw-html:`
` CTC :raw-html:`
` NBRCTC + * - Distance between the :raw-html:`
` + forecast and Best track :raw-html:`
` + genesis events (km) + - GEN_DIST + - + - TC-Gen + - GENMPR + * - Forecast minus Best track genesis time in HHMMSS format + - GEN_TDIFF + - + - TC-Gen + - GENMPR + * - Gerrity Score and :raw-html:`
` + bootstrap confidence limits + - GER + - + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTS + * - Gilbert Skill Score + - GSS + - + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + MODE + * - Hit rate + - H_RATE + - + - Point-Stat :raw-html:`
` + Grid-Stat + - FHO + * - Hausdorff Distance + - HAUSDORFF + - + - Grid-Stat + - DMAP + * - Hanssen and Kuipers :raw-html:`
` + Discriminant + - HK + - + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + MCTS :raw-html:`
` + CTS :raw-html:`
` + NBRCTS + * - Heidke Skill Score + - HSS + - + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + MCTS :raw-html:`
` + CTS :raw-html:`
` + NBRCTS + * - Heidke Skill Score with :raw-html:`
` + user-specific expected :raw-html:`
` + correct and bootstrap :raw-html:`
` + confidence limits + - HSS_EC + - + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTS From e49a578fbba5465e79d3771a747f3158097b3d5f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 16:21:04 -0600 Subject: [PATCH 156/770] i thru intensity #s #1049 --- docs/Users_Guide/statistics_list.rst | 87 +++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6464e531eb..68cac3a8a1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1031,4 +1031,89 @@ METplus Database of Statistics - - Point-Stat :raw-html:`
` Grid-Stat - - MCTS + - MCTS + * - The Ignorance Score + - IGN + - + - Ensemble-Stat + - ECNT + * - Line number in ORANK file :raw-html:`
` + Index for the current :raw-html:`
` + matched pair + - INDEX + - + - Ensemble-Stat :raw-html:`
` + TC-Gen :raw-html:`
` + TC-Pairs :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + GENMPR :raw-html:`
` + TCMPR :raw-html:`
` + MPR + * - Best track genesis minus :raw-html:`
` + forecast initialization :raw-html:`
` + time in HHMMSS format + - INIT_TDIFF + - + - TC-Gen + - GENMPR + * - Forecaster initials + - INITIALS + - + - TC-Pairs + - PROBRIRW :raw-html:`
` + TCMPR + * - User-specified percentile :raw-html:`
` + intensity in time slice :raw-html:`
` + / inside object + - INTENSITY_* + - + - MTD + - MTD 2D & 3D attribute output + * - 10th percentile intensity :raw-html:`
` + in time slice / intensity :raw-html:`
` + inside object + - INTENSITY_10 + - + - MTD + - MTD 2D & 3D attribute output + * - 10th, 25th, 50th, 75th, :raw-html:`
` + and 90th percentiles :raw-html:`
` + of intensity of the raw :raw-html:`
` + field within the object + - INTENSITY :raw-html:`
` + _10, _25, :raw-html:`
` + _50, _75, :raw-html:`
` + _90 + - + - MODE + - MODE ascii object + * - 25th percentile intensity :raw-html:`
` + in time slice / :raw-html:`
` + inside object + - INTENSITY_25 + - + - MTD + - MTD 2D & 3D attribute output + * - 60th percentile intensity :raw-html:`
` + in time slice / :raw-html:`
` + inside object + - INTENSITY_50 + - + - MTD + - MTD 2D & 3D attribute output + * - 75th percentile intensity :raw-html:`
` + in time slice / :raw-html:`
` + inside object + - INTENSITY_75 + - + - MTD + - MTD 2D & 3D attribute output + * - 90th percentile intensity :raw-html:`
` + in time slice / :raw-html:`
` + inside object + - INTENSITY_90 + - + - MTD + - MTD 2D & 3D attribute output From 9c6209c26d712ea77dfcf02b2fe2502894019acc Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 4 Nov 2021 16:27:35 -0600 Subject: [PATCH 157/770] fixing typos #1049 --- docs/Users_Guide/statistics_list.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 68cac3a8a1..b524a080bd 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -970,7 +970,9 @@ METplus Database of Statistics - - TC-Gen - GENMPR - * - Forecast minus Best track genesis time in HHMMSS format + * - Forecast minus Best track :raw-html:`
` + genesis time in HHMMSS :raw-html:`
` + format - GEN_TDIFF - - TC-Gen From 5bba88db5b58d5e69319adee7b9993f2d0733446 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 5 Nov 2021 10:19:26 -0600 Subject: [PATCH 158/770] capturing example for Julie #1049 --- docs/Users_Guide/statistics_list.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b524a080bd..5188547509 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -542,7 +542,6 @@ METplus Database of Statistics MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` - . - ISC :raw-html:`
` MODE :raw-html:`
` DMAP :raw-html:`
` From 2997e0aad647b352a770832e981ceb1cc480b46b Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 5 Nov 2021 10:50:29 -0600 Subject: [PATCH 159/770] thru k #1049 --- docs/Users_Guide/statistics_list.rst | 78 ++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 5188547509..86967e8f0d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -542,6 +542,7 @@ METplus Database of Statistics MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` + . - ISC :raw-html:`
` MODE :raw-html:`
` DMAP :raw-html:`
` @@ -1118,3 +1119,80 @@ METplus Database of Statistics - - MTD - MTD 2D & 3D attribute output + * - The percentile of :raw-html:`
` + intensity chosen for use :raw-html:`
` + in the PERCENTILE :raw-html:`
` + _INTENSITY_RATIO column :raw-html:`
` + (variable units) :raw-html:`
` + - INTENSITY + _NN + - + - MODE + - MODE ascii object + * - Sum of the intensities of :raw-html:`
` + the raw field within the :raw-html:`
` + object (variable units) + - INTENSITY :raw-html:`
` + _SUM + - + - MODE + - MODE ascii object + * - Total interest for this :raw-html:`
` + object pair + - INTEREST + - + - MTD :raw-html:`
` + MODE + - MTD 3D pair attribute output :raw-html:`
` + MODE ascii object + * - Intersection area of two :raw-html:`
` + objects (in grid squares) + - INTERSEC :raw-html:`
` + TION_AREA + - + - MODE + - MODE ascii object + * - Ratio of intersection area :raw-html:`
` + to the lesser of the :raw-html:`
` + forecast and observation :raw-html:`
` + object areas (unitless) + - INTERSEC :raw-html:`
` + TION_OVER :raw-html:`
` + _AREA + - + - MODE + - MODE ascii object + * - “Volume” of object :raw-html:`
` + intersection + - INTERSEC :raw-html:`
` + TION_VOLUME + - + - MTD + - MTD 3D pair attribute output + * - The Interquartile Range :raw-html:`
` + including bootstrap upper :raw-html:`
` + and lower confidence limits + - IQR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - The intensity scale :raw-html:`
` + skill score + - ISC + - + - Wavelet-Stat + - ISC + * - The scale at which all :raw-html:`
` + information following :raw-html:`
` + applies + - ISCALE + - + - Wavelet-Stat + - ISC + * - Kendall’s tau statistic + - KT_CORR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT From d4e644efec20dd452a1420b1f7b4a0058e71da2a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 5 Nov 2021 11:03:05 -0600 Subject: [PATCH 160/770] fixing typos #1049 --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 86967e8f0d..951951ee24 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1120,10 +1120,10 @@ METplus Database of Statistics - MTD - MTD 2D & 3D attribute output * - The percentile of :raw-html:`
` - intensity chosen for use :raw-html:`
` - in the PERCENTILE :raw-html:`
` - _INTENSITY_RATIO column :raw-html:`
` - (variable units) :raw-html:`
` + intensity chosen for use :raw-html:`
` + in the PERCENTILE :raw-html:`
` + _INTENSITY_RATIO column :raw-html:`
` + (variable units) :raw-html:`
` - INTENSITY _NN - From a09530d5f7e2041ff0055dcfef8ed430919022a7 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Fri, 5 Nov 2021 11:25:53 -0600 Subject: [PATCH 161/770] thru L #1049 --- docs/Users_Guide/statistics_list.rst | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 951951ee24..363f036805 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1122,8 +1122,7 @@ METplus Database of Statistics * - The percentile of :raw-html:`
` intensity chosen for use :raw-html:`
` in the PERCENTILE :raw-html:`
` - _INTENSITY_RATIO column :raw-html:`
` - (variable units) :raw-html:`
` + _INTENSITY_RATIO column - INTENSITY _NN - @@ -1196,3 +1195,42 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - CNT + * - Dimension of the latitude + - lat + - + - MODE + - MODE netCDF dimensions & variables + * - Length of the :raw-html:`
` + enclosing rectangle + - LENGTH + - + - MODE + - MODE ascii object + * - Level of storm :raw-html:`
` + classification + - LEVEL + - + - TC-Pairs + - TCMPR + * - Likelihood when forecast :raw-html:`
` + is between the ith and :raw-html:`
` + i+1th probability :raw-html:`
` + thresholds repeated + - LIKELIHOOD :raw-html:`
` + _i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Logarithm of the Odds Ratio + - LODDS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Dimension of the longitude + - lon + - + - MODE + - MODE netCDF dimensions & variables From 2029c8d9c8494e4839a906c69c4c506bf6b614a8 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 10:55:11 -0700 Subject: [PATCH 162/770] thru MG #1049 --- docs/Users_Guide/statistics_list.rst | 85 ++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 363f036805..61ea5e1994 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1234,3 +1234,88 @@ METplus Database of Statistics - - MODE - MODE netCDF dimensions & variables + * - The Median Absolute :raw-html:`
` + Deviation + - MAD + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Mean absolute error + - MAE + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT :raw-html:`
` + SAL1L2 :raw-html:`
` + SL1L2 + * - Magnitude & :raw-html:`
` + Multiplicative bias + - MBIAS + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT + * - The Mean Error + - ME + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ECNT :raw-html:`
` + SSVAR :raw-html:`
` + CNT + * - The Mean Error of the :raw-html:`
` + PERTURBED ensemble mean + - ME_OERR + - + - Ensemble-Stat + - ECNT + * - The square of the :raw-html:`
` + mean error (bias) + - ME2 + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Mean-error Distance from :raw-html:`
` + observation to forecast + - MED_FO + - + - Grid-Stat + - DMAP + * - Maximum of MED_FO :raw-html:`
` + and MED_OF + - MED_MAX + - + - Grid-Stat + - DMAP + * - Mean of MED_FO :raw-html:`
` + and MED_OF + - MED_MEAN + - + - Grid-Stat + - DMAP + * - Minimum of MED_FO :raw-html:`
` + and MED_OF + - MED_MIN + - + - Grid-Stat + - DMAP + * - Mean-error Distance from :raw-html:`
` + forecast to observation + - MED_OF + - + - Grid-Stat + - DMAP + * - Mean of maximum of :raw-html:`
` + absolute values of :raw-html:`
` + forecast and observed :raw-html:`
` + gradients + - MGBAR + - + - Grid-Stat + - GRAD + From d960b0ecee3b4e6b8eedb2ecab24e64c3e27abba Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 11:20:31 -0700 Subject: [PATCH 163/770] thru N_ENS #1049 --- docs/Users_Guide/statistics_list.rst | 65 +++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 61ea5e1994..88a274df6d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1318,4 +1318,67 @@ METplus Database of Statistics - - Grid-Stat - GRAD - + * - Mean squared error + - MSE + - + - Ensemble-Stat :raw-html:`
` + Wavelet-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + ISC :raw-html:`
` + CNT + * - The mean squared error :raw-html:`
` + skill + - MSESS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Mean squared length of :raw-html:`
` + the vector difference :raw-html:`
` + between the forecast :raw-html:`
` + and observed winds + - MSVE + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Total number of :raw-html:`
` + probability intervals :raw-html:`
` + and current forecast run + - N_BIN + - + - Ensemble-Stat + - PHIST :raw-html:`
` + SSVAR + * - Dimension of the :raw-html:`
` + contingency table & the :raw-html:`
` + total number of :raw-html:`
` + categories in each :raw-html:`
` + dimension + - N_CAT + - + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTC :raw-html:`
` + MCTS + * - Number of cluster objects + - n_clus + - + - MODE + - MODE netCDF variables + * - Number of ensemble :raw-html:`
` + values / members + - N_ENS + - + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK :raw-html:`
` + RELP + * - Number of valid :raw-html:`
` + ensemble values + - N_ENS_VLD + - + - Ensemble-Stat + - ORANK From 7977ff35cb822a274c2fbc454176273169aa55a5 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 11:52:17 -0700 Subject: [PATCH 164/770] thru all N #1049 --- docs/Users_Guide/statistics_list.rst | 50 +++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 88a274df6d..f261698a3f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1381,4 +1381,52 @@ METplus Database of Statistics - N_ENS_VLD - - Ensemble-Stat - - ORANK + - ORANK + * - Number of simple :raw-html:`
` + forecast objects + - n_fcst_simp + - + - MODE + - MODE netCDF variables + * - Number of simple :raw-html:`
` + observation objects + - n_obs_simp + - + - MODE + - MODE netCDF variables + * - Number of Cost/Loss :raw-html:`
` + ratios + - N_PNT + - + - Point-Stat :raw-html:`
` + Grid-Stat + - ECLV + * - + - N_PROB + - + - Ensemble-Stat + - Number of probability thresholds + * - Number of possible ranks :raw-html:`
` + for observation + - N_RANK + - + - Ensemble-Stat + - RHIST + * - Number of probability :raw-html:`
` + thresholds + - N_THRESH + - + - TC-Pairs :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - PROBRIRW :raw-html:`
` + PJC :raw-html:`
` + PRC :raw-html:`
` + PSTD output format :raw-html:`
` + PTC + * - Total number of scales :raw-html:`
` + used in decomposition + - NSCALE + - + - Wavelet-Stat + - ISC From 62b7938ac83f5d613fe7c63efadc2af6f208e4b8 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 13:01:43 -0700 Subject: [PATCH 165/770] fixing FBIAS alignment #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index f261698a3f..b782798c64 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -545,9 +545,9 @@ METplus Database of Statistics . - ISC :raw-html:`
` MODE :raw-html:`
` - DMAP :raw-html:`
` CTS :raw-html:`
` - NBRCTCS + NBRCTCS :raw-html:`
` + DMAP * - Fractions Brier Score - FBS - From 2e04e64f683677dfca013dc040eaa74f46f6ab33 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 13:06:24 -0700 Subject: [PATCH 166/770] fixing ME and MSE alignment #1049 --- docs/Users_Guide/statistics_list.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b782798c64..4b5a694600 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1262,6 +1262,7 @@ METplus Database of Statistics - ME - - Ensemble-Stat :raw-html:`
` + . :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - ECNT :raw-html:`
` @@ -1327,7 +1328,8 @@ METplus Database of Statistics Grid-Stat - SSVAR :raw-html:`
` ISC :raw-html:`
` - CNT + CNT :raw-html:`
` + . * - The mean squared error :raw-html:`
` skill - MSESS From 6076a293f0002dc2fc3f6dca5206b014cd8ed7c6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 13:15:26 -0700 Subject: [PATCH 167/770] fixing ME alignment take 2 #1049 --- docs/Users_Guide/statistics_list.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4b5a694600..23e2e8babd 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1267,6 +1267,7 @@ METplus Database of Statistics Grid-Stat - ECNT :raw-html:`
` SSVAR :raw-html:`
` + . :raw-html:`
` CNT * - The Mean Error of the :raw-html:`
` PERTURBED ensemble mean From 41b4ef321152e021bd2d4e52d97d491d3ed7c474 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 14:15:01 -0700 Subject: [PATCH 168/770] thru OBS_E #1049 --- docs/Users_Guide/statistics_list.rst | 161 +++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 23e2e8babd..c8d145f56f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1433,3 +1433,164 @@ METplus Database of Statistics - - Wavelet-Stat - ISC + * - NBRCNT output format :raw-html:`
` + & observation rate + - O_RATE + - + - Point-Stat :raw-html:`
` + Grid-Stat + - NBRCNT :raw-html:`
` + FHO + * - Mean observed wind speed + - O_SPEED_BAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean(o-c) + - OABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average observed value :raw-html:`
` + observation mean :raw-html:`
` + Mean (o) :raw-html:`
` + & mean value + - OBAR + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` . + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + SL1L2 :raw-html:`
` + VCNT + * - Mean observation normal :raw-html:`
` + upper and lower :raw-html:`
` + confidence limits + - OBAR_NCL + - + - Ensemble-Stat + - SSVAR + * - Length (speed) of the :raw-html:`
` + average observed wind :raw-html:`
` + vector + - OBAR_SPEED + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Object category + - OBJECT_CAT + - + - MODE :raw-html:`
` + MTD + - MODE ascii object :raw-html:`
` + MTD 2D & 3D attribute output :raw-html:`
` + MTD 3D pair attribute output + * - Object number + - OBJECT_ID + - + - MODE :raw-html:`
` + MTD + - MODE ascii object :raw-html:`
` + MTD 2D & 3D attribute output :raw-html:`
` + MTD 3D pair attribute output + * - Observation value + - OBS + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + MPR :raw-html:`
` + . + * - Number of observed :raw-html:`
` + clusters + - obs_clus + - + - MODE + - MODE netCDF dimensions + * - Number of points used to :raw-html:`
` + define the hull of all of :raw-html:`
` + the cluster observation :raw-html:`
` + objects + - obs_clus :raw-html:`
` + _hull + - + - MODE + - MODE netCDF dimensions + * - Observation Cluster Convex :raw-html:`
` + Hull Point Latitude + - obs_clus + _hull_lat + - + - MODE + - MODE netCDF variables + * - Observation Cluster Convex :raw-html:`
` + Hull Point Longitude + - obs_clus :raw-html:`
` + _hull_lon + - + - MODE + - MODE netCDF variables + * - Number of Observation :raw-html:`
` + Cluster Convex Hull Points + - obs_clus :raw-html:`
` + _hull_npts + - + - MODE + - MODE netCDF variables + * - Observation Cluster Convex :raw-html:`
` + Hull Starting Index + - obs_clus :raw-html:`
` + _hull_start + - + - MODE + - MODE netCDF variables + * - Observation Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - obs_clus :raw-html:`
` + _hull_x + - + - MODE + - MODE netCDF variables + * - Observation Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - obs_clus :raw-html:`
` + _hull_y + - + - MODE + - MODE netCDF variables + * - Cluster observation object :raw-html:`
` + id number for each :raw-html:`
` + grid point + - obs_clus_id + - + - MODE + - MODE netCDF variables + * - Observation convolution :raw-html:`
` + threshold + - obs_conv + _threshold + - + - MODE + - MODE netCDF variables + * - Observation convolution :raw-html:`
` + radius + - obs_conv :raw-html:`
` + _radius + - + - MODE + - MODE netCDF variables + * - Elevation of the :raw-html:`
` + observation + - OBS_ELV + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + MPR :raw-html:`
` + . From ec4e1fb9f044cf70d72acf3786df297e3f494c24 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 14:25:01 -0700 Subject: [PATCH 169/770] fixing alignment #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c8d145f56f..bcd08b7ad5 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1523,7 +1523,7 @@ METplus Database of Statistics - MODE netCDF dimensions * - Observation Cluster Convex :raw-html:`
` Hull Point Latitude - - obs_clus + - obs_clus :raw-html:`
` _hull_lat - - MODE @@ -1572,7 +1572,7 @@ METplus Database of Statistics - MODE netCDF variables * - Observation convolution :raw-html:`
` threshold - - obs_conv + - obs_conv :raw-html:`
` _threshold - - MODE From eff34e989f0c504b3ba01ccc830c97bbbb554fb3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 14:35:02 -0700 Subject: [PATCH 170/770] fixing alignment n_thresh #1049 --- docs/Users_Guide/statistics_list.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index bcd08b7ad5..e2324c52e8 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1421,7 +1421,9 @@ METplus Database of Statistics - - TC-Pairs :raw-html:`
` Point-Stat :raw-html:`
` - Grid-Stat + Grid-Stat :raw-html:`
` + . :raw-html:`
` + . - PROBRIRW :raw-html:`
` PJC :raw-html:`
` PRC :raw-html:`
` From 26611c1a04278d411f99031e5a6ae1ff75d94f53 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 16:17:30 -0700 Subject: [PATCH 171/770] thru OBS_thresh #1049 --- docs/Users_Guide/statistics_list.rst | 175 +++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e2324c52e8..9789feefe8 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1596,3 +1596,178 @@ METplus Database of Statistics - ORANK :raw-html:`
` MPR :raw-html:`
` . + * - Latitude of the :raw-html:`
` + observation + - OBS_LAT + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + MPR :raw-html:`
` + . + * - Longitude of the :raw-html:`
` + observation + - OBS_LON + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + MPR :raw-html:`
` . + * - Level of the observation + - OBS_LVL + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + MPR :raw-html:`
` + . + * - Simple observation object :raw-html:`
` + id number for each :raw-html:`
` + grid point + - obs_obj_id + - + - MODE + - MODE netCDF variables + * - Observation Object Raw :raw-html:`
` + Values + - obs_obj_raw + - + - MODE + - MODE netCDF variables + * - Quality control flag for :raw-html:`
` + observation + - OBS_QC + - + - Point-Stat :raw-html:`
` + Grid-Stat + - MPR + * - Observation Raw Values + - obs_raw + - + - MODE + - MODE netCDF variables + * - Station Identifier + - OBS_SID + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ORANK :raw-html:`
` + MPR :raw-html:`
` . + * - Number of simple :raw-html:`
` + observation objects + - obs_simp + - + - MODE + - MODE netCDF dimensions + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of the simple observation :raw-html:`
` + objects + - obs_simp :raw-html:`
` + _bdy + - + - MODE + - MODE netCDF dimensions + * - Observation Simple :raw-html:`
` + Boundary Point Latitude + - obs_simp :raw-html:`
` + _bdy_lat + - + - MODE + - MODE netCDF variables + * - Observation Simple :raw-html:`
` + Boundary Point Longitude + - obs_simp :raw-html:`
` + _bdy_lon + - + - MODE + - MODE netCDF variables + * - Observation Simple :raw-html:`
` + Boundary Starting Index + - obs_simp :raw-html:`
` + _bdy_start + - + - MODE + - MODE netCDF variables + * - Number of Observation :raw-html:`
` + Simple Boundary Points + - obs_simp :raw-html:`
` + _bdy_npts + - + - MODE + - MODE netCDF variables + * - Observation Simple Boundary :raw-html:`
` + Point X-Coordinate + - obs_simp :raw-html:`
` + _bdy_x + - + - MODE + - MODE netCDF variables + * - Observation Simple Boundary :raw-html:`
` + Point Y-Coordinate + - obs_simp :raw-html:`
` + _bdy_y + - + - MODE + - MODE netCDF variables + * - Number of points used to :raw-html:`
` + define the hull of the :raw-html:`
` + simple observation objects + - obs_simp :raw-html:`
` + _hull + - + - MODE + - MODE netCDF dimensions + * - Observation Simple Convex :raw-html:`
` + Hull Point Latitude + - obs_simp :raw-html:`
` + _hull_lat + - + - MODE + - MODE netCDF variables + * - Observation Simple Convex :raw-html:`
` + Hull Point Longitude + - obs_simp :raw-html:`
` + _hull_lon + - + - MODE + - MODE netCDF variables + * - Number of Observation :raw-html:`
` + Simple Convex Hull Points + - obs_simp :raw-html:`
` + _hull_npts + - + - MODE + - MODE netCDF variables + * - Observation Simple Convex :raw-html:`
` + Hull Starting Index + - obs_simp :raw-html:`
` + _hull_start + - + - MODE + - MODE netCDF variables + * - Observation Simple Convex :raw-html:`
` + Hull Point X-Coordinate + - obs_simp :raw-html:`
` + _hull_x + - + - MODE + - MODE netCDF variables + * - Observation Simple Convex :raw-html:`
` + Hull Point Y-Coordinate + - obs_simp :raw-html:`
` + _hull_y + - + - MODE + - MODE netCDF variables + * - Number of thresholds :raw-html:`
` + applied to the observations + - obs_thresh :raw-html:`
` + _length + - + - MODE + - MODE netCDF dimensions From cd8108dfa79a966dbd3084a2c2c1ddf1ebce95e0 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 16:47:55 -0700 Subject: [PATCH 172/770] thru O #1049 --- docs/Users_Guide/statistics_list.rst | 124 +++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 9789feefe8..8646909347 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1771,3 +1771,127 @@ METplus Database of Statistics - - MODE - MODE netCDF dimensions + * - Odds Ratio + - ODDS + - + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + CTS :raw-html:`
` + NBRCTS + * - Direction of the average :raw-html:`
` + observed wind vector + - ODIR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Observed energy squared :raw-html:`
` + for this scale + - OENERGY + - + - Wavelet-Stat + - ISC + * - Mean of absolute value :raw-html:`
` + of observed gradients + - OGBAR + - + - Grid-Stat + - GRAD + * - Number of observation no :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds + - ON_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PTC + * - Number of observation no :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds + - ON_TP_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Mean((o-c)²) + - OOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average of observation squared & Mean(o²) + - OOBAR + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + SL1L2 :raw-html:`
` . + * - Operational methodology :raw-html:`
` + category (FYOY, FYON, :raw-html:`
` + FNOY, or DISCARD) + - OPS_CAT + - + - TC-Gen + - GENMPR + * - Number of tied observation :raw-html:`
` + ranks used in computing :raw-html:`
` + Kendall’s tau statistic + - ORANK_TIES + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Odds Ratio Skill Score + - ORSS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Root mean square observed :raw-html:`
` + wind speed + - OS_RMS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Standard deviation + - OSTDEV + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + VCNT + * - Number of observation :raw-html:`
` + events + - OY + - + - Grid-Stat + - DMAP + * - Number of observation yes :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds + - OY_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PTC + * - Number of observation yes :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds :raw-html:`
` + as a proportion of the :raw-html:`
` + total OY (repeated) + - OY_TP_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC From c3a95b495b5bef630b72a384c2d2d33b806e72e8 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Mon, 8 Nov 2021 16:53:21 -0700 Subject: [PATCH 173/770] fixing OOBAR formating #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8646909347..6a00437e26 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1823,7 +1823,8 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - SAL1L2 - * - Average of observation squared & Mean(o²) + * - Average of observation :raw-html:`
` + squared & Mean(o²) - OOBAR - - Ensemble-Stat :raw-html:`
` From be5dd7ffc4ad40bf5e704781c2eb2dbcf8e58ccd Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 10:09:07 -0700 Subject: [PATCH 174/770] thru PR_CORR #1049 --- docs/Users_Guide/statistics_list.rst | 82 ++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6a00437e26..6787ae7bb9 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1896,3 +1896,85 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - PJC + * - Ratio of the nth percentile :raw-html:`
` + (INTENSITY_NN column) of :raw-html:`
` + intensity of the two :raw-html:`
` + objects defined as the :raw-html:`
` + lesser of the forecast :raw-html:`
` + intensity divided by the :raw-html:`
` + observation intensity or :raw-html:`
` + its reciprocal (unitless) + - PERCENTILE :raw-html:`
` + _INTENSITY :raw-html:`
` + _RATIO + - + - MODE + - MODE ascii object + * - Probability Integral :raw-html:`
` + Transform + - PIT + - + - Ensemble-Stat + - ORANK + * - Probability of false :raw-html:`
` + detection + - PODF + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS + * - Probability of detecting no + - PODN + - + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + MODE + * - Probability of detecting :raw-html:`
` + yes + - PODY + - + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + MODE + * - Probability of detecting :raw-html:`
` + yes when forecast is :raw-html:`
` + greater than the ith :raw-html:`
` + probability thresholds + - PODY_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PRC + * - Probability of false :raw-html:`
` + detection + - POFD + - + - MODE :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + NBRCTCS + * - Probability of false :raw-html:`
` + detection when forecast is :raw-html:`
` + greater than the ith :raw-html:`
` + probability thresholds + - POFD_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PRC + * - Pearson correlation :raw-html:`
` + coefficient + - PR_CORR + - + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + . From 59bd5a383e5da9ecc6dfcdfbebb0dbf6bb770734 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 10:18:43 -0700 Subject: [PATCH 175/770] commented lines out with line total info #1049 --- docs/Users_Guide/statistics_list.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6787ae7bb9..48d2431cb9 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2,6 +2,13 @@ METplus Database of Statistics ****************************** + +# Statistics no more that 32 characters +# METplus Name no more than 17 characters +# Statistic Type no more than 19 characters +# Metplus Line Type currently unlimited (approx 33 characters) + + .. role:: raw-html(raw) :format: html From 29fda8ea38241300bee92e1544daef58f0a45002 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 10:29:40 -0700 Subject: [PATCH 176/770] commented lines out with line total info take 2 #1049 --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 48d2431cb9..8907a3606f 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,10 +3,10 @@ METplus Database of Statistics ****************************** -# Statistics no more that 32 characters -# METplus Name no more than 17 characters -# Statistic Type no more than 19 characters -# Metplus Line Type currently unlimited (approx 33 characters) +.. Statistics - no more that 32 characters + METplus Name - no more than 17 characters + Statistic Type - no more than 19 characters + Metplus Line Type - currently unlimited (approx 33 characters) .. role:: raw-html(raw) From b45f4fabe9392361e91344a7698b97b9ab84f8f1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 11:14:34 -0700 Subject: [PATCH 177/770] thru R #1049 --- docs/Users_Guide/statistics_list.rst | 166 ++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8907a3606f..34f30d844c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -3,7 +3,8 @@ METplus Database of Statistics ****************************** -.. Statistics - no more that 32 characters +.. Number of characters per line: + Statistics - no more that 32 characters METplus Name - no more than 17 characters Statistic Type - no more than 19 characters Metplus Line Type - currently unlimited (approx 33 characters) @@ -1985,3 +1986,166 @@ METplus Database of Statistics - SSVAR :raw-html:`
` CNT :raw-html:`
` . + * - The ith probability :raw-html:`
` + value (repeated) + - PROB_i + - + - TC-Pairs + - PROBRIRW + * - Rank of the observation + - RANK + - + - Ensemble-Stat + - ORANK + * - Count of observations :raw-html:`
` + with the i-th rank + - RANK_i + - + - Ensemble-Stat + - RHIST + * - Number of ranks used in :raw-html:`
` + computing Kendall’s tau :raw-html:`
` + statistic + - RANKS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Refinement when forecast :raw-html:`
` + is between the ith and :raw-html:`
` + i+1th probability :raw-html:`
` + thresholds (repeated) + - REFINEMENT :raw-html:`
` + _i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Reliability + - RELIABILITY + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD output format + * - Number of times the i-th :raw-html:`
` + ensemble member’s value :raw-html:`
` + was closest to the :raw-html:`
` + observation (repeated). :raw-html:`
` + When n members tie, :raw-html:`
` + 1/n is assigned to each :raw-html:`
` + member. + - RELP_i + - + - Ensemble-Stat + - RELP + * - Resolution + - RESOLUTION + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD output format + * - Start of RI time window :raw-html:`
` + in HH format + - RI_BEG + - + - TC-Pairs + - PROBRIRW + * - End of RI time window :raw-html:`
` + in HH format + - RI_END + - + - TC-Pairs + - PROBRIRW + * - Width of RI time window :raw-html:`
` + in HH format + - RI_WINDOW + - + - TC-Pairs + - PROBRIRW + * - Root mean squared error + - RMSE + - + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Ensemble-Stat :raw-html:`
` + . + - CNT :raw-html:`
` + . :raw-html:`
` + ECNT :raw-html:`
` + SSVAR + * - The Root Mean Square Error :raw-html:`
` + of the PERTURBED ensemble :raw-html:`
` + mean (e.g. with :raw-html:`
` + Observation Error) + - RMSE_OERR + - + - Ensemble-Stat + - ECNT + * - Root mean squared forecast :raw-html:`
` + anomaly (f-c) + - RMSFA + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Root mean squared :raw-html:`
` + observation anomaly (o-c) :raw-html:`
` + including bootstrap upper :raw-html:`
` + & lower confidence limits + - RMSOA + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Square root of MSVE + - RMSVE + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Area under the receiver :raw-html:`
` + operating characteristic :raw-html:`
` + curve + - ROC_AUC + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD outpu format + * - Mean of the Brier Scores :raw-html:`
` + for each RPS threshold + - RPS + - + - Ensemble-Stat + - RPS + * - Mean of the reliabilities :raw-html:`
` + for each RPS threshold + - RPS_REL + - + - Ensemble-Stat + - RPS Reliability + * - Mean of the resolutions :raw-html:`
` + for each RPS threshold + - RPS_RES + - + - Ensemble-Stat + - RPS Resolution + * - Mean of the uncertainties :raw-html:`
` + for each RPS threshold + - RPS_UNC + - + - Ensemble-Stat + - RPS Uncertainty + * - Ranked Probability Skill :raw-html:`
` + Score relative to external :raw-html:`
` + climatology + - RPSS + - + - Ensemble-Stat + - RPS + * - Ranked Probability Skill :raw-html:`
` + Score relative to sample :raw-html:`
` + climatology + - RPSS_SMPL + - + - Ensemble-Stat + - RPS From 0210c4d75b324837f2eed3690a0b1849fa4b9139 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 11:34:42 -0700 Subject: [PATCH 178/770] thru SPEED #1049 --- docs/Users_Guide/statistics_list.rst | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 34f30d844c..cea879d681 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2149,3 +2149,74 @@ METplus Database of Statistics - - Ensemble-Stat - RPS + * - S1 score + - S1 + - + - Grid-Stat + - GRAD + * - S1 score with respect to :raw-html:`
` + observed gradient + - S1_OG + - + - Grid-Stat + - GRAD + * - Symmetric Extremal :raw-html:`
` + Dependency Index + - SEDI + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Symmetric Extreme :raw-html:`
` + Dependency Score + - SEDS + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Scatter Index + - SI + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Spearman’s rank :raw-html:`
` + correlation coefficient + - SP_CORR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Spatial distance between :raw-html:`
` + (𝑥,𝑦)(x,y) coordinates of :raw-html:`
` + object spacetime centroid + - SPACE :raw-html:`
` + _CENTROID :raw-html:`
` + _DIST + - + - MTD + - MTD 3D pair attribute output + * - Absolute value of SPEED_ERR + - SPEED :raw-html:`
` + _ABSERR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Difference in object speeds + - SPEED_DELTA + - + - MTD + - MTD 3D pair attribute output + * - Difference between the :raw-html:`
` + length of the average :raw-html:`
` + forecast wind vector and :raw-html:`
` + the average observed wind :raw-html:`
` + vector (in the sense F - O) + - SPEED_ERR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT From b572e67c4ba640597b42199615260cd67435065c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 11:55:52 -0700 Subject: [PATCH 179/770] thru S #1049 --- docs/Users_Guide/statistics_list.rst | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index cea879d681..4755a78be7 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2220,3 +2220,56 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VCNT + * - The square root or the :raw-html:`
` + spread (standard deviation) :raw-html:`
` + of the mean of the variance :raw-html:`
` + of the unperturbed ensemble :raw-html:`
` + member values at each :raw-html:`
` + observation location + - SPREAD + - + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK + * - The square root or the :raw-html:`
` + spread (standard deviation) :raw-html:`
` + of the mean of the variance :raw-html:`
` + of the PERTURBED ensemble :raw-html:`
` + member values at each :raw-html:`
` + observation location + - SPREAD_OERR + - + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK + * - The square root of the sum :raw-html:`
` + of unperturbed ensemble :raw-html:`
` + variance and the :raw-html:`
` + observation error variance + - SPREAD_PLUS :raw-html:`
` + _OERR + - + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK + * - Object start time + - START_TIME + - + - MTD + - MTD 3D attribute output + * - Difference in object :raw-html:`
` + starting time steps + - START_TIME :raw-html:`
` + _DELTA + - + - MTD + - MTD 3D pair attribute output + * - Symmetric difference of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - SYMMETRIC :raw-html:`
` + _DIFF + - + - MODE + - MODE ascii object + From fa88025240d396e257f458129e2c508b859fd17e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 13:45:08 -0700 Subject: [PATCH 180/770] thru T #1049 --- docs/Users_Guide/statistics_list.rst | 68 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4755a78be7..114aa79e1b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2272,4 +2272,70 @@ METplus Database of Statistics - - MODE - MODE ascii object - + * - The ith probability :raw-html:`
` + threshold value (repeated) + - THRESH_i + - + - TC-Pairs :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + . :raw-html:`
` + . + - PROBRIRW :raw-html:`
` + PJC :raw-html:`
` + PRC :raw-html:`
` + PSTD output format :raw-html:`
` + PTC + * - Last probability :raw-html:`
` + threshold value + - THRESH_n + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC :raw-html:`
` + PRC :raw-html:`
` + PTC + * - The dimensions of the tile + - TILE_DIM + - + - Wavelet-Stat + - ISC + * - Horizontal coordinate of :raw-html:`
` + the lower left corner of :raw-html:`
` + the tile + - TILE_XLL + - + - Wavelet-Stat + - ISC + * - Vertical coordinate of :raw-html:`
` + the lower left corner :raw-html:`
` + of the tile + - TILE_YLL + - + - Wavelet-Stat + - ISC + * - Difference in t index of :raw-html:`
` + object spacetime centroid + - TIME :raw-html:`
` + _CENTROID :raw-html:`
` + _DELTA + - + - MTD + - MTD 3D pair attribute output + * - Time index of slice + - TIME_INDEX + - + - MTD + - MTD 2D attribute output + * - Track error of adeck :raw-html:`
` + relative to bdeck (nm) + - TK_ERR + - + - TC-Pairs + - PROBRIRW + * - Track error of adeck :raw-html:`
` + relative to bdeck (nm) + - TK_ERR + - + - TC-Pairs + - TCMPR From 0c864410ec3c2d883d7a47e266b4c6b44d9c18a3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 14:13:19 -0700 Subject: [PATCH 181/770] thru U #1049 --- docs/Users_Guide/statistics_list.rst | 88 ++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 114aa79e1b..a45a4596c1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1411,12 +1411,7 @@ METplus Database of Statistics - - Point-Stat :raw-html:`
` Grid-Stat - - ECLV - * - - - N_PROB - - - - Ensemble-Stat - - Number of probability thresholds + - ECLV * - Number of possible ranks :raw-html:`
` for observation - N_RANK @@ -2339,3 +2334,84 @@ METplus Database of Statistics - - TC-Pairs - TCMPR + * - Mean(uf-uc) + - UFABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uf) + - UFBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Uniform Fractions Skill :raw-html:`
` + Score including bootstrap :raw-html:`
` + upper and lower :raw-html:`
` + confidence limits + - UFSS + - + - Grid-Stat + - NBRCNT + * - Uncertainty + - UNCERTAINTY + - + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD outpu format + * - Union area of two objects :raw-html:`
` + (in grid squares) + - UNION_AREA + - + - MODE + - MODE ascii object + * - Mean(uo-uc) + - UOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uo) + - UOBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean((uf-uc)²+(vf-vc)²) + - UVFFABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uf²+vf²) + - UVFFBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean((uf-uc)*(uo-uc)+ :raw-html:`
` + (vf-vc)*(vo-vc)) + - UVFOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uf*uo+vf*vo) + - UVFOBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean((uo-uc)²+(vo-vc)²) + - UVOOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uo²+vo²) + - UVOOBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 From 8968d0da5431eb45fc9f6f1b30e299f19517c825 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 9 Nov 2021 14:50:21 -0700 Subject: [PATCH 182/770] thru V #1049 --- docs/Users_Guide/statistics_list.rst | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index a45a4596c1..b955cb8c36 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2412,6 +2412,93 @@ METplus Database of Statistics * - Mean(uo²+vo²) - UVOOBAR - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Economic value of the :raw-html:`
` + base rate + - VALUE_BASER + - + - Point-Stat :raw-html:`
` + Grid-Stat + - ECLV + * - Relative value for the :raw-html:`
` + ith Cost/Loss ratio + - VALUE_i + - + - Point-Stat :raw-html:`
` + Grid-Stat + - ECLV + * - Maximum variance + - VAR_MAX + - + - Ensemble-Stat + - SSVAR + * - Average variance + - VAR_MEAN + - + - Ensemble-Stat + - SSVAR + * - Minimum variance + - VAR_MIN + - + - Ensemble-Stat + - SSVAR + * - Direction of the vector :raw-html:`
` + difference between the :raw-html:`
` + average forecast and :raw-html:`
` + average wind vectors + - VDIFF_DIR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Length (speed) of the :raw-html:`
` + vector difference between :raw-html:`
` + the average forecast and :raw-html:`
` + average observed wind :raw-html:`
` + vectors + - VDIFF_SPEED + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Mean(vf-vc) + - VFABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(vf) + - VFBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean(vo-vc) + - VOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(vo) + - VOBAR + - - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 + * - Integer count of the :raw-html:`
` + number of 3D “cells” :raw-html:`
` + in an object + - VOLUME + - + - MTD + - MTD 3D attribute output + * - Forecast object volume :raw-html:`
` + divided by observation :raw-html:`
` + object volume + - VOLUME :raw-html:`
` + _RATIO + - + - MTD + - MTD 3D pair attribute output From a120226b6556f388ebd5c0ec15975d491efb9b0c Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 10 Nov 2021 11:15:56 -0700 Subject: [PATCH 183/770] thru V #1049 --- docs/Users_Guide/statistics_list.rst | 83 ++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index b955cb8c36..616b3c9760 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2502,3 +2502,86 @@ METplus Database of Statistics - - MTD - MTD 3D pair attribute output + * - Economic value of the base rate + - VALUE_BASER + - + - Point-Stat :raw-html:`
` Grid-Stat + - ECLV + * - Relative value for the ith Cost/Loss ratio + - VALUE_i + - + - Point-Stat :raw-html:`
` Grid-Stat + - ECLV + * - Maximum variance + - VAR_MAX + - + - Ensemble-Stat + - SSVAR + * - Average variance + - VAR_MEAN + - + - Ensemble-Stat + - SSVAR + * - Minimum variance + - VAR_MIN + - + - Ensemble-Stat + - SSVAR + * - Direction of the vector :raw-html:`
` + difference between the :raw-html:`
` + average forecast and :raw-html:`
` + average wind vectors + - VDIFF_DIR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Length (speed) of the :raw-html:`
` + vector difference between :raw-html:`
` + the average forecast and :raw-html:`
` + average observed wind :raw-html:`
` + vectors + - VDIFF_SPEED + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Mean(vf-vc) + - VFABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(vf) + - VFBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean(vo-vc) + - VOABAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(vo) + - VOBAR + - + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Integer count of the :raw-html:`
` + number of 3D “cells” :raw-html:`
` + in an object + - VOLUME + - + - MTD + - MTD 3D attribute output + * - Forecast object volume :raw-html:`
` + divided by observation :raw-html:`
` + object volume + - VOLUME :raw-html:`
` + _RATIO + - + - MTD + - MTD 3D pair attribute output From 4d177222f74e112edb81e7175884ee74cc80063e Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 10 Nov 2021 11:16:16 -0700 Subject: [PATCH 184/770] thru Z #1049 --- docs/Users_Guide/statistics_list.rst | 132 +++++++++++++-------------- 1 file changed, 61 insertions(+), 71 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 616b3c9760..ee293d06af 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2502,86 +2502,76 @@ METplus Database of Statistics - - MTD - MTD 3D pair attribute output - * - Economic value of the base rate - - VALUE_BASER - - - - Point-Stat :raw-html:`
` Grid-Stat - - ECLV - * - Relative value for the ith Cost/Loss ratio - - VALUE_i + * - HU or TS watch or :raw-html:`
` + warning in effect + - WATCH_WARN - - - Point-Stat :raw-html:`
` Grid-Stat - - ECLV - * - Maximum variance - - VAR_MAX + - TC-Pairs + - TCMPR + * - Width of the enclosing :raw-html:`
` + rectangle (in grid units) + - WIDTH - - - Ensemble-Stat - - SSVAR - * - Average variance - - VAR_MEAN + - MODE + - MODE ascii object + * - x component of :raw-html:`
` + object velocity + - X_DOT - - - Ensemble-Stat - - SSVAR - * - Minimum variance - - VAR_MIN + - MTD + - MTD 3D attribute output + * - X component position :raw-html:`
` + error (nm) + - X_ERR - - - Ensemble-Stat - - SSVAR - * - Direction of the vector :raw-html:`
` - difference between the :raw-html:`
` - average forecast and :raw-html:`
` - average wind vectors - - VDIFF_DIR + - TC-Pairs + - PROBRIRW + * - X component position :raw-html:`
` + error (nm) + - X_ERR - - - Point-Stat :raw-html:`
` - Grid-Stat - - VCNT - * - Length (speed) of the :raw-html:`
` - vector difference between :raw-html:`
` - the average forecast and :raw-html:`
` - average observed wind :raw-html:`
` - vectors - - VDIFF_SPEED + - TC-Pairs + - TCMPR + * - y component of :raw-html:`
` + object velocity + - Y_DOT - - - Point-Stat :raw-html:`
` - Grid-Stat - - VCNT - * - Mean(vf-vc) - - VFABAR + - MTD + - MTD 3D attribute output + * - Y component position :raw-html:`
` + error (nm) + - Y_ERR - - - Point-Stat :raw-html:`
` - Grid-Stat - - VAL1L2 - * - Mean(vf) - - VFBAR + - TC-Pairs + - PROBRIRW :raw-html:`
` + TCMPR + * - Zhu’s Measure from :raw-html:`
` + observation to forecast + - ZHU_FO - - - Point-Stat :raw-html:`
` - Grid-Stat - - VL1L2 - * - Mean(vo-vc) - - VOABAR + - Grid-Stat + - DMAP + * - Maximum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MAX - - - Point-Stat :raw-html:`
` - Grid-Stat - - VAL1L2 - * - Mean(vo) - - VOBAR + - Grid-Stat + - DMAP + * - Mean of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MEAN - - - Point-Stat :raw-html:`
` - Grid-Stat - - VL1L2 - * - Integer count of the :raw-html:`
` - number of 3D “cells” :raw-html:`
` - in an object - - VOLUME + - Grid-Stat + - DMAP + * - Minimum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MIN - - - MTD - - MTD 3D attribute output - * - Forecast object volume :raw-html:`
` - divided by observation :raw-html:`
` - object volume - - VOLUME :raw-html:`
` - _RATIO + - Grid-Stat + - DMAP + * - Zhu’s Measure from :raw-html:`
` + forecast to observation + - ZHU_OF - - - MTD - - MTD 3D pair attribute output + - Grid-Stat + - DMAP From 4fed843885f7d264ddc7bd70af7cb5eef85ce6ea Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Thu, 11 Nov 2021 12:24:19 -0700 Subject: [PATCH 185/770] Update statistics_list.rst Tara is testing editing in UI --- docs/Users_Guide/statistics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ee293d06af..2742895720 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -36,7 +36,7 @@ METplus Database of Statistics MODE output format: Accuracy * - Asymptotic Fractions Skill Score - AFSS - - + - Neighborhood - Grid-Stat - NBRCNT * - Along track error (nm) From e52474107446fbc92a26b90587cf2486531a44bf Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Thu, 11 Nov 2021 12:39:00 -0700 Subject: [PATCH 186/770] Update statistics_list.rst Updates through the A's --- docs/Users_Guide/statistics_list.rst | 33 ++++++++++++---------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2742895720..0381c6e673 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -4,10 +4,10 @@ METplus Database of Statistics .. Number of characters per line: - Statistics - no more that 32 characters + Statistic Name - no more that 32 characters METplus Name - no more than 17 characters Statistic Type - no more than 19 characters - Metplus Line Type - currently unlimited (approx 33 characters) + METplus Line Type - currently unlimited (approx 33 characters) .. role:: raw-html(raw) @@ -41,19 +41,19 @@ METplus Database of Statistics - NBRCNT * - Along track error (nm) - ALTK_ERR - - + - Continuous - TC-Pairs - TCMPR * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) - ANGLE_DIFF - - + - Diagnostic Attr. - MODE - MODE * - Anomaly Correlation :raw-html:`
` including mean error - ANOM_CORR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` @@ -64,7 +64,7 @@ METplus Database of Statistics error including bootstrap upper :raw-html:`
` and lower confidence limits - ANOM_CORR :raw-html:`
` _UNCNTR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` @@ -72,20 +72,15 @@ METplus Database of Statistics - CNT * - Object area (in grid squares) - AREA - - + - Diagnostics Attr. - MODE :raw-html:`
` MTD - MODE ascii object * - Forecast object area :raw-html:`
` divided by the observation :raw-html:`
` - object area (unitless) :raw-html:`
` - NOTE: Prior to met-10.0.0, :raw-html:`
` - defined as the lesser of :raw-html:`
` - the two object areas :raw-html:`
` - divided by the greater :raw-html:`
` - of the two + object area (unitless) - AREA_RATIO - - + - Diagnostics Attr. - MODE - MODE ascii object * - Area of the object :raw-html:`
` @@ -95,7 +90,7 @@ METplus Database of Statistics definition threshold :raw-html:`
` criteria (in grid squares) - AREA_THRESH - - + - Diagnostics Attr. - MODE - MODE ascii object * - Absolute value of :raw-html:`
` @@ -104,25 +99,25 @@ METplus Database of Statistics ratios of two objects :raw-html:`
` (unitless) - ASPECT_DIFF - - + - Diagnostics Attr. - MODE - MODE ascii object * - Object axis angle :raw-html:`
` (in degrees) - AXIS_ANG - - + - Diagnostics Attr. - MODE :raw-html:`
` MTD - Attribute output * - Difference in spatial :raw-html:`
` axis plane angles - AXIS_DIFF - - + - Diagnostics Attr. - MTD - Attribute output * - Baddeley’s Delta Metric - BADDELEY - - + - Distance Map - Grid-Stat - DMAP * - Bias Adjusted Gilbert :raw-html:`
` From 1822794e3e71233cc16f9496fbf501b8e653554f Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Thu, 11 Nov 2021 12:59:45 -0700 Subject: [PATCH 187/770] Update statistics_list.rst Cleaning up the A's --- docs/Users_Guide/statistics_list.rst | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0381c6e673..597748423b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -47,7 +47,7 @@ METplus Database of Statistics * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) - ANGLE_DIFF - - Diagnostic Attr. + - Diagnostic Attr - MODE - MODE * - Anomaly Correlation :raw-html:`
` @@ -61,8 +61,7 @@ METplus Database of Statistics - CNT * - Uncentered Anomaly :raw-html:`
` Correlation excluding mean :raw-html:`
` - error including bootstrap upper :raw-html:`
` - and lower confidence limits + error - ANOM_CORR :raw-html:`
` _UNCNTR - Continuous - Point-Stat :raw-html:`
` @@ -72,7 +71,7 @@ METplus Database of Statistics - CNT * - Object area (in grid squares) - AREA - - Diagnostics Attr. + - Diagnostic Attr - MODE :raw-html:`
` MTD - MODE ascii object @@ -80,17 +79,15 @@ METplus Database of Statistics divided by the observation :raw-html:`
` object area (unitless) - AREA_RATIO - - Diagnostics Attr. + - Diagnostic Attr - MODE - MODE ascii object * - Area of the object :raw-html:`
` - containing data values :raw-html:`
` - in the raw field :raw-html:`
` that meet the object :raw-html:`
` definition threshold :raw-html:`
` criteria (in grid squares) - AREA_THRESH - - Diagnostics Attr. + - Diagnostic Attr - MODE - MODE ascii object * - Absolute value of :raw-html:`
` @@ -99,20 +96,20 @@ METplus Database of Statistics ratios of two objects :raw-html:`
` (unitless) - ASPECT_DIFF - - Diagnostics Attr. + - Diagnostic Attr - MODE - MODE ascii object * - Object axis angle :raw-html:`
` (in degrees) - AXIS_ANG - - Diagnostics Attr. + - Diagnostic Attr - MODE :raw-html:`
` MTD - Attribute output * - Difference in spatial :raw-html:`
` axis plane angles - AXIS_DIFF - - Diagnostics Attr. + - Diagnostic Attr - MTD - Attribute output * - Baddeley’s Delta Metric @@ -123,14 +120,14 @@ METplus Database of Statistics * - Bias Adjusted Gilbert :raw-html:`
` Skill Score - BAGSS - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` NBRCTS * - Base Rate - BASER - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Wavelet-Stat :raw-html:`
` From 7747692ba974de0b41701fa00b94c28330c3f3a1 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Thu, 11 Nov 2021 13:26:41 -0700 Subject: [PATCH 188/770] Update statistics_list.rst Standardizing MODE and MTD entries --- docs/Users_Guide/statistics_list.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 597748423b..0ea9af39bc 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -33,7 +33,7 @@ METplus Database of Statistics - CTS :raw-html:`
` MCTS :raw-html:`
` NBRCTS :raw-html:`
` - MODE output format: Accuracy + MODE cts * - Asymptotic Fractions Skill Score - AFSS - Neighborhood @@ -42,7 +42,7 @@ METplus Database of Statistics * - Along track error (nm) - ALTK_ERR - Continuous - - TC-Pairs + - TC-Pairs, TC-Stat - TCMPR * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) @@ -74,14 +74,14 @@ METplus Database of Statistics - Diagnostic Attr - MODE :raw-html:`
` MTD - - MODE ascii object + - MODE obj * - Forecast object area :raw-html:`
` divided by the observation :raw-html:`
` object area (unitless) - AREA_RATIO - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Area of the object :raw-html:`
` that meet the object :raw-html:`
` definition threshold :raw-html:`
` @@ -89,7 +89,7 @@ METplus Database of Statistics - AREA_THRESH - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Absolute value of :raw-html:`
` the difference :raw-html:`
` between the aspect :raw-html:`
` @@ -98,20 +98,20 @@ METplus Database of Statistics - ASPECT_DIFF - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Object axis angle :raw-html:`
` (in degrees) - AXIS_ANG - Diagnostic Attr - MODE :raw-html:`
` MTD - - Attribute output + - MTD obj * - Difference in spatial :raw-html:`
` axis plane angles - AXIS_DIFF - Diagnostic Attr - MTD - - Attribute output + - MTD obj * - Baddeley’s Delta Metric - BADDELEY - Distance Map @@ -134,14 +134,14 @@ METplus Database of Statistics MODE - CTS :raw-html:`
` ECLV :raw-html:`
` - MODE :raw-html:`
` + MODE cts :raw-html:`
` NBRCTCS :raw-html:`
` PSTD :raw-html:`
` PJC * - Bias-corrected mean :raw-html:`
` squared error - BCMSE - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Ensemble-Stat @@ -151,9 +151,9 @@ METplus Database of Statistics the boundaries of two objects - BOUNDARY :raw-html:`
` _DIST - - + - Diagnostic Attr - MODE - - Attribute output + - MODE obj * - Brier Score - BRIER - From 03b4f8ed0ef9816acdf4cb159a7189f64ca39c62 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Thu, 11 Nov 2021 13:39:29 -0700 Subject: [PATCH 189/770] Update statistics_list.rst Updating B's and C's --- docs/Users_Guide/statistics_list.rst | 102 +++++++++++++-------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0ea9af39bc..a4f0f3ccf3 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -156,27 +156,27 @@ METplus Database of Statistics - MODE obj * - Brier Score - BRIER - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PSTD * - Climatological Brier Score - BRIERCL - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PSTD * - Brier Skill Score relative :raw-html:`
` to sample climatology - BSS - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PSTD * - Brier Skill Score relative :raw-html:`
` to external climatology - BSS_SMPL - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PSTD @@ -186,7 +186,7 @@ METplus Database of Statistics thresholds (repeated) - CALIBRATION :raw-html:`
` _i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PJC @@ -196,59 +196,59 @@ METplus Database of Statistics of the 3D object - CDIST :raw-html:`
` _TRAVELLED - - + - Diagnostic Attr - MTD - - MTD 3D + - MTD 3D obj * - Distance between two :raw-html:`
` objects centroids :raw-html:`
` (in grid units) - CENTROID :raw-html:`
` _DIST - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Latitude of centroid :raw-html:`
` Location of the centroid - CENTROID :raw-html:`
` _LAT - - + - Diagnostic Attr - MTD :raw-html:`
` MODE - - MTD 2D & 3D attribute output :raw-html:`
` - MODE ascii object + - MTD 2D & 3D obj :raw-html:`
` + MODE obj * - Longitude of centroid :raw-html:`
` Location of the centroid - CENTROID :raw-html:`
` _LON - - + - Diagnostic Attr - MTD :raw-html:`
` MODE - - MTD 2D & 3D attribute output :raw-html:`
` - MODE ascii object - * - t coordinate of centroid + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Time coordinate of centroid - CENTROID_T - - + - Diagnostic Attr - MTD - - MTD 3D attribute output - * - x coordinate of centroid :raw-html:`
` + - MTD 3D obj + * - X coordinate of centroid :raw-html:`
` Location of the centroid - CENTROID_X - - + - Diagnostic Attr - MTD :raw-html:`
` MODE - - MTD 2D & 3D attribute output :raw-html:`
` + - MTD 2D & 3D obj :raw-html:`
` MODE ascii object - * - y coordinate of centroid :raw-html:`
` + * - Y coordinate of centroid :raw-html:`
` Location of the centroid - CENTROID_Y - - + - Diagnostic Attr - MTD :raw-html:`
` MODE - - MTD 2D & 3D attribute output :raw-html:`
` + - MTD 2D & 3D obj :raw-html:`
` MODE ascii object * - Climatological mean value - CLIMO_MEAN - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Ensemble-Stat @@ -257,7 +257,7 @@ METplus Database of Statistics * - Climatological standard :raw-html:`
` deviation value - CLIMO_STDEV - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Ensemble-Stat @@ -270,9 +270,9 @@ METplus Database of Statistics by the area of the :raw-html:`
` complex hull (unitless) - COMPLEXITY - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Ratio of complexities of :raw-html:`
` two objects defined as :raw-html:`
` the lesser of the forecast :raw-html:`
` @@ -281,98 +281,98 @@ METplus Database of Statistics its reciprocal (unitless) - COMPLEXITY :raw-html:`
` _RATIO - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Minimum distance between :raw-html:`
` the convex hulls of two :raw-html:`
` objects (in grid units) - CONVEX_HULL :raw-html:`
` _DIST - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - The Continuous Ranked :raw-html:`
` Probability Score :raw-html:`
` (normal dist.) - CRPS - - + - Ensemble - Ensemble-Stat - ECNT * - The Continuous Ranked :raw-html:`
` Probability Score :raw-html:`
` (empirical dist.) - CRPS_EMP - - + - Ensemble - Ensemble-Stat - ECNT * - Climatological Continuous :raw-html:`
` Ranked Probability Score :raw-html:`
` (normal dist.) - CRPSCL - - + - Ensemble - Ensemble-Stat - ECNT * - Climatological Continuous :raw-html:`
` Ranked Probability Score :raw-html:`
` (empirical dist.) - CRPSCL_EMP - - + - Ensemble - Ensemble-Stat - ECNT * - The Continuous Ranked :raw-html:`
` Probability Skill Score :raw-html:`
` (normal dist.) - CRPSS - - + - Ensemble - Ensemble-Stat - ECNT * - The Continuous Ranked :raw-html:`
` Probability Skill Score :raw-html:`
` (empirical dist.) - CRPSS_EMP - - + - Ensemble - Ensemble-Stat - ECNT * - Cross track error (nm) - CRTK_ERR - - - - TC-Pairs - - TCMPR + - Continuous + - TC-Pairs, TC-Stat + - TCMPR, TCST * - Critical Success Index - CSI - - + - Categorical - Point-Stat :raw-html:`
` - MODE :raw-html:`
` + MODE cts :raw-html:`
` Grid-Stat - CTS :raw-html:`
` MODE :raw-html:`
` MBRCTCS * - Radius of curvature - CURVATURE - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Ratio of the curvature - CURVATURE :raw-html:`
` _RATIO - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Center of curvature :raw-html:`
` (in grid coordinates) - CURVATURE :raw-html:`
` _X - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Center of curvature :raw-html:`
` (in grid coordinates) - CURVATURE :raw-html:`
` _Y - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Development methodology :raw-html:`
` category - DEV_CAT From 605df225a31b5596c368cb3c36d1dbb2e86b365e Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Thu, 11 Nov 2021 13:47:31 -0700 Subject: [PATCH 190/770] Update statistics_list.rst Testing adding TC-Stat and TCST to an entry --- docs/Users_Guide/statistics_list.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index a4f0f3ccf3..da194f04cf 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -42,8 +42,10 @@ METplus Database of Statistics * - Along track error (nm) - ALTK_ERR - Continuous - - TC-Pairs, TC-Stat - - TCMPR + - TC-Pairs :raw-html:`
` + TC-Stat + - TCMPR :raw-html:`
` + TCST * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) - ANGLE_DIFF From 7094e0a613d354d894119900327837b6da143644 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 11 Nov 2021 16:29:32 -0700 Subject: [PATCH 191/770] feature 1252 allow dictionary value for time_summary.width (#1253) --- internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py | 8 ++++++++ metplus/wrappers/command_builder.py | 9 +++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py index 641c4d3599..8db28911a4 100644 --- a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py +++ b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py @@ -88,6 +88,14 @@ def ascii2nc_wrapper(metplus_config, config_path=None, config_overrides=None): 'grib_code = [11, 204, 211];obs_var = [];' 'type = ["min", "max", "range", "mean", "stdev", "median", "p80"];' 'vld_freq = 0;vld_thresh = 0.0;}')}), + # width as dictionary + ({'ASCII2NC_TIME_SUMMARY_WIDTH': '{ beg = -21600; end = 0; }'}, + {'METPLUS_TIME_SUMMARY_DICT': + ('time_summary = {flag = FALSE;raw_data = FALSE;beg = "000000";' + 'end = "235959";step = 300;width = { beg = -21600; end = 0; };' + 'grib_code = [11, 204, 211];obs_var = [];' + 'type = ["min", "max", "range", "mean", "stdev", "median", "p80"];' + 'vld_freq = 0;vld_thresh = 0.0;}')}), ({'ASCII2NC_TIME_SUMMARY_GRIB_CODES': '12, 203, 212'}, {'METPLUS_TIME_SUMMARY_DICT': diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 64bdc9c255..9e22aa6642 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -2012,10 +2012,11 @@ def handle_time_summary_dict(self, c_dict, remove_bracket_list=None): 'step', 'TIME_SUMMARY_STEP') - self.set_met_config_int(tmp_dict, - f'{app}_TIME_SUMMARY_WIDTH', - 'width', - 'TIME_SUMMARY_WIDTH') + self.set_met_config_string(tmp_dict, + f'{app}_TIME_SUMMARY_WIDTH', + 'width', + 'TIME_SUMMARY_WIDTH', + remove_quotes=True) self.set_met_config_list(tmp_dict, [f'{app}_TIME_SUMMARY_GRIB_CODES', From 934ec59b8c077fb1449cd2619741a04aa775477d Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Fri, 12 Nov 2021 15:33:03 -0700 Subject: [PATCH 192/770] Update statistics_list.rst Clean up of a few A-Cs and then update of Ds --- docs/Users_Guide/statistics_list.rst | 30 ++++++++++++---------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index da194f04cf..e63f6b672c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -339,8 +339,10 @@ METplus Database of Statistics * - Cross track error (nm) - CRTK_ERR - Continuous - - TC-Pairs, TC-Stat - - TCMPR, TCST + - TC-Pairs :raw-html:`
` + TC-Stat + - TCMPR :raw-html:`
` + TCST * - Critical Success Index - CSI - Categorical @@ -375,24 +377,18 @@ METplus Database of Statistics - Diagnostic Attr - MODE - MODE obj - * - Development methodology :raw-html:`
` - category - - DEV_CAT - - - - TC-Gen - - GENMPR - * - Absolute value + * - Absolute value of DIR_ERR (see below) - DIR_ABSERR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT * - Signed angle between :raw-html:`
` the directions of the :raw-html:`
` average forecast and :raw-html:`
` - observed wing vectors + observed wind vectors - DIR_ERR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT @@ -400,21 +396,21 @@ METplus Database of Statistics direction of movement - DIRECTION :raw-html:`
` _DIFF - - + - Diagnostic Attr - MTD - - MTD 3D pair attribute output + - MTD 3D obj * - Difference in the :raw-html:`
` lifetimes of the :raw-html:`
` two objects - DURATION :raw-html:`
` _DIFF - - + - Diagnostic Attr - MTD - - MTD 3D pair attribute output + - MTD 3D obj * - Expected correct rate :raw-html:`
` used for MCTS HSS_EC - EC_VALUE - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - MCTC From badb0d7cf28519cadab319078375da7e27f68c1a Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Fri, 12 Nov 2021 15:43:33 -0700 Subject: [PATCH 193/770] Update statistics_list.rst A few clean-ups and Es --- docs/Users_Guide/statistics_list.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e63f6b672c..129dc0cba2 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -377,7 +377,8 @@ METplus Database of Statistics - Diagnostic Attr - MODE - MODE obj - * - Absolute value of DIR_ERR (see below) + * - Absolute value of :raw-html:`
` + DIR_ERR (see below) - DIR_ABSERR - Continuous - Point-Stat :raw-html:`
` @@ -419,7 +420,7 @@ METplus Database of Statistics bootstrap upper and :raw-html:`
` lower confidence limits - EDI - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` @@ -429,7 +430,7 @@ METplus Database of Statistics bootstrap upper and :raw-html:`
` lower confidence limits - EDS - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` @@ -438,25 +439,25 @@ METplus Database of Statistics of forecast minus :raw-html:`
` observed gradients - EGBAR - - + - Continuous - Grid-Stat - GRAD * - Object end time - END_TIME - - + - Diagnostic Attr - MTD - - MTD 3D attribute output + - MTD 3D obj * - Difference in object :raw-html:`
` ending time steps - END_TIME :raw-html:`
` _DELTA - - + - Diagnostic Attr - MTD - - MTD 3D pair attribute output + - MTD 3D pair * - The unperturbed :raw-html:`
` ensemble mean value - ENS_MEAN - - + - Diagnostic Fld - Ensemble-Stat - ORANK * - The PERTURBED ensemble :raw-html:`
` @@ -464,13 +465,13 @@ METplus Database of Statistics Observation Error). - ENS_MEAN :raw-html:`
` _OERR - - + - Diagnostic Fld - Ensemble-Stat - ORANK * - Standard deviation of :raw-html:`
` the error - ESTDEV - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Ensemble-Stat From 03ff5dc745969af6048470b0c57980baaa40d4fa Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Fri, 12 Nov 2021 16:31:04 -0700 Subject: [PATCH 194/770] Update statistics_list.rst Halfway through Fs... --- docs/Users_Guide/statistics_list.rst | 183 ++++++++++----------------- 1 file changed, 70 insertions(+), 113 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 129dc0cba2..fe329abe2b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -480,7 +480,7 @@ METplus Database of Statistics * - Forecast rate/event :raw-html:`
` frequency - F_RATE - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - FHO :raw-html:`
` @@ -488,19 +488,19 @@ METplus Database of Statistics * - Mean forecast wind speed - F_SPEED :raw-html:`
` _BAR - - + - Continous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 - * - Mean(f-c) + * - Mean Forecast Anomaly - FABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - SAL1L2 * - False alarm ratio - FAR - - + - Categorical - Point-Stat :raw-html:`
` MODE :raw-html:`
` Grid-Stat @@ -509,39 +509,30 @@ METplus Database of Statistics NBRCTCS * - Forecast mean - FBAR - - + - Categorical - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` - . - SSVAR :raw-html:`
` CNT :raw-html:`
` SL1L2 :raw-html:`
` VCNT - * - Mean forecast normal upper :raw-html:`
` - and lower confidence :raw-html:`
` - limits - - FBAR_NCL - - - - Ensemble-Stat - - SSVAR * - Length (speed) of the :raw-html:`
` average forecast :raw-html:`
` wind vector - FBAR :raw-html:`
` _SPEED - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT * - Frequency Bias - FBIAS - - + - Categorical - Wavelet-Stat :raw-html:`
` MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` - . - ISC :raw-html:`
` MODE :raw-html:`
` CTS :raw-html:`
` @@ -549,253 +540,219 @@ METplus Database of Statistics DMAP * - Fractions Brier Score - FBS - - + - Continuous - Grid-Stat - NBRCNT * - Number of forecast :raw-html:`
` clusters - fcst_clus - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Number of points used to :raw-html:`
` define the hull of all :raw-html:`
` of the cluster forecast :raw-html:`
` objects - fcst_clus :raw-html:`
` _hull - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Forecast Cluster Convex :raw-html:`
` Hull Point Latitude - fcst_clus :raw-html:`
` _hull_lat - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Cluster Convex :raw-html:`
` Hull Point Longitude - fcst_clus :raw-html:`
` _hull _lon - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Number of Forecast :raw-html:`
` Cluster Convex Hull Points - fcst_clus :raw-html:`
` _hull_npts - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Cluster Convex :raw-html:`
` Hull Starting Index - fcst_clus :raw-html:`
` _hull_start - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Cluster Convex :raw-html:`
` Hull Point X-Coordinate - fcst_clus :raw-html:`
` _hull_x - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Cluster Convex :raw-html:`
` Hull Point Y-Coordinate - fcst_clus :raw-html:`
` _hull_y - - - - MODE - - MODE netCDF variables - * - Cluster forecast object id :raw-html:`
` - number for each grid point - - fcst_clus :raw-html:`
` - _id - - - - MODE - - MODE netCDF variables - * - Forecast convolution :raw-html:`
` - threshold - - fcst_conv :raw-html:`
` - _threshold - - - - MODE - - MODE netCDF variables - * - Forecast convolution radius - - fcst_conv :raw-html:`
` - _radius - - - - MODE - - MODE netCDF variables - * - Simple forecast object :raw-html:`
` - id number for each :raw-html:`
` - grid point - - fcst_obj :raw-html:`
` - _id - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Object Raw :raw-html:`
` Values - fcst_obj :raw-html:`
` _raw - - - - MODE - - MODE netCDF variables - * - Forecast raw values - - fcst_raw - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Number of simple :raw-html:`
` forecast objects - fcst_simp - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Number of points used :raw-html:`
` to define the boundaries :raw-html:`
` of all of the simple :raw-html:`
` forecast objects - fcst_simp :raw-html:`
` _bdy - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Forecast Simple :raw-html:`
` - Boundary PoLatitude + Boundary Latitude - fcst_simp :raw-html:`
` _bdy_lat - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple :raw-html:`
` - Boundary PoLongitude + Boundary Longitude - fcst_simp :raw-html:`
` _bdy_lon - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Number of Forecast :raw-html:`
` Simple Boundary Points - fcst_simp :raw-html:`
` _bdy_npts - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple :raw-html:`
` Boundary Starting Index - fcst_simp :raw-html:`
` _bdy_start - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple :raw-html:`
` - Boundary PoX-Coordinate + Boundary X-Coordinate - fcst_simp :raw-html:`
` _bdy_x - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple :raw-html:`
` - Boundary PoY-Coordinate + Boundary Y-Coordinate - fcst_simp :raw-html:`
` _bdy_y - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Number of points used to :raw-html:`
` define the hull of all :raw-html:`
` of the simple forecast :raw-html:`
` objects - fcst_simp :raw-html:`
` _hull - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Forecast Simple Convex :raw-html:`
` Hull Point Latitude - fcst_simp :raw-html:`
` _hull_lat - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple Convex :raw-html:`
` Hull Point Longitude - fcst_simp :raw-html:`
` _hull_lon - - + - Diagnostic Attr - MODE - MODE netCDF variables * - Number of Forecast :raw-html:`
` Simple Convex Hull Points - fcst_simp :raw-html:`
` _hull_npts - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple Convex :raw-html:`
` Hull Starting Index - fcst_simp :raw-html:`
` _hull_start - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple Convex :raw-html:`
` Hull Point X-Coordinate - fcst_simp :raw-html:`
` _hull_x - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Forecast Simple Convex :raw-html:`
` Hull Point Y-Coordinate - fcst_simp :raw-html:`
` _hull_y - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE netCDF * - Number of thresholds :raw-html:`
` applied to the forecast - fcst :raw-html:`
` _thresh :raw-html:`
` _length - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Number of thresholds :raw-html:`
` applied to the forecast - fcst_thresh :raw-html:`
` _length - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE netCDF * - Direction of the average :raw-html:`
` forecast wind vector - FDIR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT * - Forecast energy squared :raw-html:`
` for this scale - FENERGY - - + - Diagnostic Attr - Wavelet-Stat - ISC - * - Mean((f-c)²) + * - Mean Forecast Anomaly Squared - FFABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - SAL1L2 * - Average of forecast :raw-html:`
` - squared. [Mean(f²) :raw-html:`
` - Grid-Stat] + squared. - FFBAR - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat From b021341ba7b6260057ba34d12b3b64b87374791a Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 15 Nov 2021 10:37:36 -0700 Subject: [PATCH 195/770] feature 1213 obs_quality_inc/exc (#1260) --- docs/Users_Guide/glossary.rst | 22 +++++++++- docs/Users_Guide/wrappers.rst | 44 +++++++++++++++++-- .../test_ensemble_stat_wrapper.py | 4 ++ .../point_stat/test_point_stat_wrapper.py | 6 ++- metplus/wrappers/ensemble_stat_wrapper.py | 15 +++++++ metplus/wrappers/point_stat_wrapper.py | 13 ++++-- parm/met_config/EnsembleStatConfig_wrapped | 8 +++- parm/met_config/PointStatConfig_wrapped | 9 +++- .../EnsembleStat/EnsembleStat.conf | 3 ++ .../met_tool_wrapper/PointStat/PointStat.conf | 4 +- 10 files changed, 114 insertions(+), 14 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index c924aded30..3484ae5beb 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -6661,11 +6661,19 @@ METplus Configuration Glossary | *Used by:* MODE - POINT_STAT_OBS_QUALITY - Specify the value for 'obs_quality' in the MET configuration file for PointStat. + POINT_STAT_OBS_QUALITY_INC + Specify the value for 'obs_quality_inc' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_OBS_QUALITY_EXC + Specify the value for 'obs_quality_exc' in the MET configuration file for PointStat. | *Used by:* PointStat + POINT_STAT_OBS_QUALITY + .. warning:: **DEPRECATED:** Please use :term:`POINT_STAT_OBS_QUALITY_INC` instead. + POINT_STAT_OUTPUT_FLAG_FHO Specify the value for 'output_flag.fho' in the MET configuration file for PointStat. @@ -8257,3 +8265,13 @@ METplus Configuration Glossary Specify the value for 'ens.file_type' in the MET configuration file for GenEnsProd. | *Used by:* GenEnsProd + + ENSEMBLE_STAT_OBS_QUALITY_INC + Specify the value for 'obs_quality_inc' in the MET configuration file for EnsembleStat. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_OBS_QUALITY_EXC + Specify the value for 'obs_quality_exc' in the MET configuration file for EnsembleStat. + + | *Used by:* EnsembleStat diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 8b07448f8a..18fc97d57e 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -274,6 +274,8 @@ METplus Configuration | :term:`ENSEMBLE_STAT_ENSEMBLE_FLAG_NMEP` | :term:`ENSEMBLE_STAT_ENSEMBLE_FLAG_RANK` | :term:`ENSEMBLE_STAT_ENSEMBLE_FLAG_WEIGHT` +| :term:`ENSEMBLE_STAT_OBS_QUALITY_INC` +| :term:`ENSEMBLE_STAT_OBS_QUALITY_EXC` | :term:`ENSEMBLE_STAT_MET_CONFIG_OVERRIDES` | :term:`ENSEMBLE_STAT_VERIFICATION_MASK_TEMPLATE` (optional) | :term:`ENS_VAR_NAME` (optional) @@ -829,6 +831,28 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`ENSEMBLE_STAT_OUTPUT_PREFIX` - output_prefix +**${METPLUS_OBS_QUALITY_INC}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`ENSEMBLE_STAT_OBS_QUALITY_INC` + - obs_quality_inc + +**${METPLUS_OBS_QUALITY_EXC}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`ENSEMBLE_STAT_OBS_QUALITY_EXC` + - obs_quality_exc + **${METPLUS_MET_CONFIG_OVERRIDES}** .. list-table:: @@ -4854,7 +4878,8 @@ Configuration | :term:`POINT_STAT_CLIMO_CDF_BINS` | :term:`POINT_STAT_CLIMO_CDF_CENTER_BINS` | :term:`POINT_STAT_CLIMO_CDF_WRITE_BINS` -| :term:`POINT_STAT_OBS_QUALITY` +| :term:`POINT_STAT_OBS_QUALITY_INC` +| :term:`POINT_STAT_OBS_QUALITY_EXC` | :term:`POINT_STAT_OUTPUT_FLAG_FHO` | :term:`POINT_STAT_OUTPUT_FLAG_CTC` | :term:`POINT_STAT_OUTPUT_FLAG_CTS` @@ -5196,7 +5221,18 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`POINT_STAT_CLIMO_CDF_WRITE_BINS` - climo_cdf.write_bins -**${METPLUS_OBS_QUALITY}** +**${METPLUS_OBS_QUALITY_INC}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`POINT_STAT_OBS_QUALITY_INC` + - obs_quality_inc + +**${METPLUS_OBS_QUALITY_EXC}** .. list-table:: :widths: 5 5 @@ -5204,8 +5240,8 @@ see :ref:`How METplus controls MET config file settings`. * - METplus Config(s) - MET Config File - * - :term:`POINT_STAT_OBS_QUALITY` - - obs_quality + * - :term:`POINT_STAT_OBS_QUALITY_EXC` + - obs_quality_exc **${METPLUS_OUTPUT_FLAG_DICT}** diff --git a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py index d0f525654f..750fe994af 100644 --- a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py +++ b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py @@ -540,6 +540,10 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, 'type = [{method = GAUSSIAN;width = 1;}];}' ) }), + ({'ENSEMBLE_STAT_OBS_QUALITY_INC': '2,3,4', }, + {'METPLUS_OBS_QUALITY_INC': 'obs_quality_inc = ["2", "3", "4"];'}), + ({'ENSEMBLE_STAT_OBS_QUALITY_EXC': '5,6,7', }, + {'METPLUS_OBS_QUALITY_EXC': 'obs_quality_exc = ["5", "6", "7"];'}), ] ) diff --git a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py index 7155d1d1b8..e8c2302f35 100755 --- a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py +++ b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py @@ -178,8 +178,12 @@ def test_met_dictionary_in_var_options(metplus_config): { 'METPLUS_CLIMO_CDF_DICT': 'climo_cdf = {cdf_bins = 1.0;center_bins = TRUE;write_bins = FALSE;}'}), + ({'POINT_STAT_OBS_QUALITY_INC': '2,3,4', }, + {'METPLUS_OBS_QUALITY_INC': 'obs_quality_inc = ["2", "3", "4"];'}), + ({'POINT_STAT_OBS_QUALITY_EXC': '5,6,7', }, + {'METPLUS_OBS_QUALITY_EXC': 'obs_quality_exc = ["5", "6", "7"];'}), ({'POINT_STAT_OBS_QUALITY': '1, 2, 3', }, - {'METPLUS_OBS_QUALITY': 'obs_quality = ["1", "2", "3"];'}), + {'METPLUS_OBS_QUALITY_INC': 'obs_quality_inc = ["1", "2", "3"];'}), ({'POINT_STAT_OUTPUT_FLAG_FHO': 'BOTH', }, {'METPLUS_OUTPUT_FLAG_DICT': 'output_flag = {fho = BOTH;}'}), diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index e171987be8..652170ffbe 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -61,6 +61,8 @@ class EnsembleStatWrapper(CompareGriddedWrapper): 'METPLUS_OUTPUT_FLAG_DICT', 'METPLUS_ENSEMBLE_FLAG_DICT', 'METPLUS_OUTPUT_PREFIX', + 'METPLUS_OBS_QUALITY_INC', + 'METPLUS_OBS_QUALITY_EXC', ] # handle deprecated env vars used pre v4.0.0 @@ -299,6 +301,19 @@ def create_c_dict(self): c_dict['MASK_POLY_TEMPLATE'] = self.read_mask_poly() + self.add_met_config( + name='obs_quality_inc', + data_type='list', + metplus_configs=['ENSEMBLE_STAT_OBS_QUALITY_INC', + 'ENSEMBLE_STAT_OBS_QUALITY_INCLUDE'] + ) + self.add_met_config( + name='obs_quality_exc', + data_type='list', + metplus_configs=['ENSEMBLE_STAT_OBS_QUALITY_EXC', + 'ENSEMBLE_STAT_OBS_QUALITY_EXCLUDE'] + ) + # old method of setting MET config values c_dict['ENS_THRESH'] = ( self.config.getstr('config', 'ENSEMBLE_STAT_ENS_THRESH', '1.0') diff --git a/metplus/wrappers/point_stat_wrapper.py b/metplus/wrappers/point_stat_wrapper.py index fb1887a356..a21df7caad 100755 --- a/metplus/wrappers/point_stat_wrapper.py +++ b/metplus/wrappers/point_stat_wrapper.py @@ -33,7 +33,8 @@ class PointStatWrapper(CompareGriddedWrapper): 'METPLUS_MASK_SID', 'METPLUS_OUTPUT_PREFIX', 'METPLUS_CLIMO_CDF_DICT', - 'METPLUS_OBS_QUALITY', + 'METPLUS_OBS_QUALITY_INC', + 'METPLUS_OBS_QUALITY_EXC', 'METPLUS_OUTPUT_FLAG_DICT', 'METPLUS_INTERP_DICT', 'METPLUS_CLIMO_MEAN_DICT', @@ -194,9 +195,15 @@ def create_c_dict(self): False) ) - self.add_met_config(name='obs_quality', + self.add_met_config(name='obs_quality_inc', data_type='list', - metplus_configs=['POINT_STAT_OBS_QUALITY']) + metplus_configs=['POINT_STAT_OBS_QUALITY_INC', + 'POINT_STAT_OBS_QUALITY_INCLUDE', + 'POINT_STAT_OBS_QUALITY']) + self.add_met_config(name='obs_quality_exc', + data_type='list', + metplus_configs=['POINT_STAT_OBS_QUALITY_EXC', + 'POINT_STAT_OBS_QUALITY_EXCLUDE']) self.handle_flags('output') diff --git a/parm/met_config/EnsembleStatConfig_wrapped b/parm/met_config/EnsembleStatConfig_wrapped index 5331c5583c..e9ef2d5667 100644 --- a/parm/met_config/EnsembleStatConfig_wrapped +++ b/parm/met_config/EnsembleStatConfig_wrapped @@ -95,7 +95,13 @@ obs = { ${METPLUS_MESSAGE_TYPE} sid_exc = []; obs_thresh = [ NA ]; -obs_quality = []; + +//obs_quality_inc = +${METPLUS_OBS_QUALITY_INC} + +//obs_quality_exc = +${METPLUS_OBS_QUALITY_EXC} + ${METPLUS_DUPLICATE_FLAG} obs_summary = NONE; obs_perc_value = 50; diff --git a/parm/met_config/PointStatConfig_wrapped b/parm/met_config/PointStatConfig_wrapped index 2a0d8d1907..2a654d6d23 100644 --- a/parm/met_config/PointStatConfig_wrapped +++ b/parm/met_config/PointStatConfig_wrapped @@ -63,8 +63,13 @@ obs = { // message_type = ${METPLUS_MESSAGE_TYPE} sid_exc = []; -//obs_quality = -${METPLUS_OBS_QUALITY} + +//obs_quality_inc = +${METPLUS_OBS_QUALITY_INC} + +//obs_quality_exc = +${METPLUS_OBS_QUALITY_EXC} + duplicate_flag = NONE; obs_summary = NONE; obs_perc_value = 50; diff --git a/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf b/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf index 261bff3325..c4c61ce8e7 100644 --- a/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf +++ b/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf @@ -169,6 +169,9 @@ ENSEMBLE_STAT_ENSEMBLE_FLAG_NMEP = FALSE ENSEMBLE_STAT_ENSEMBLE_FLAG_RANK = TRUE ENSEMBLE_STAT_ENSEMBLE_FLAG_WEIGHT = FALSE +#ENSEMBLE_STAT_OBS_QUALITY_INC = +#ENSEMBLE_STAT_OBS_QUALITY_EXC = + # Ensemble Variables and levels as specified in the ens field dictionary # of the MET configuration file. Specify as ENS_VARn_NAME, ENS_VARn_LEVELS, # (optional) ENS_VARn_OPTION diff --git a/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf b/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf index a290427772..36875e0a79 100644 --- a/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf +++ b/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf @@ -50,7 +50,9 @@ LOOP_ORDER = processes # or the value of the environment variable METPLUS_PARM_BASE if set POINT_STAT_CONFIG_FILE ={PARM_BASE}/met_config/PointStatConfig_wrapped -#POINT_STAT_OBS_QUALITY = 1, 2, 3 + +#POINT_STAT_OBS_QUALITY_INC = 1, 2, 3 +#POINT_STAT_OBS_QUALITY_EXC = POINT_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = NEAREST #POINT_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = From 9a6473a131ae64fa4b5606637b04d0284b48018e Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 15 Nov 2021 12:20:51 -0700 Subject: [PATCH 196/770] Feature 1203 ioda2nc (#1262) --- .github/parm/use_case_groups.json | 5 + docs/Contributors_Guide/create_wrapper.rst | 64 ++++- docs/Users_Guide/glossary.rst | 258 ++++++++++++++++- docs/Users_Guide/quicksearch.rst | 2 + docs/Users_Guide/wrappers.rst | 269 ++++++++++++++++++ .../met_tool_wrapper/IODA2NC/IODA2NC.py | 110 +++++++ .../met_tool_wrapper/IODA2NC/README.rst | 2 + .../gen_ens_prod/test_gen_ens_prod_wrapper.py | 54 ---- .../pytests/ioda2nc/test_ioda2nc_wrapper.py | 246 ++++++++++++++++ internal_tests/use_cases/all_use_cases.txt | 1 + metplus/util/doc_util.py | 1 + metplus/util/met_util.py | 2 +- metplus/wrappers/ascii2nc_wrapper.py | 10 +- metplus/wrappers/command_builder.py | 45 ++- metplus/wrappers/gen_ens_prod_wrapper.py | 4 - metplus/wrappers/ioda2nc_wrapper.py | 174 +++++++++++ metplus/wrappers/pb2nc_wrapper.py | 2 +- metplus/wrappers/runtime_freq_wrapper.py | 2 +- parm/met_config/Ascii2NcConfig_wrapped | 2 + parm/met_config/EnsembleStatConfig_wrapped | 2 + parm/met_config/GenEnsProdConfig_wrapped | 2 + parm/met_config/GridDiagConfig_wrapped | 2 + parm/met_config/GridStatConfig_wrapped | 4 +- parm/met_config/IODA2NCConfig_wrapped | 117 ++++++++ parm/met_config/MODEConfig_wrapped | 2 + parm/met_config/MTDConfig_wrapped | 2 + parm/met_config/PB2NCConfig_wrapped | 3 +- parm/met_config/PointStatConfig_wrapped | 3 +- parm/met_config/STATAnalysisConfig_wrapped | 4 +- parm/met_config/SeriesAnalysisConfig_wrapped | 4 +- parm/met_config/TCGenConfig_wrapped | 2 + parm/met_config/TCPairsConfig_wrapped | 2 + parm/met_config/TCRMWConfig_wrapped | 2 + parm/met_config/TCStatConfig_wrapped | 2 + .../met_tool_wrapper/IODA2NC/IODA2NC.conf | 83 ++++++ 35 files changed, 1409 insertions(+), 80 deletions(-) create mode 100644 docs/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.py create mode 100644 docs/use_cases/met_tool_wrapper/IODA2NC/README.rst create mode 100644 internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py create mode 100755 metplus/wrappers/ioda2nc_wrapper.py create mode 100644 parm/met_config/IODA2NCConfig_wrapped create mode 100644 parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 536774aac5..4cb2de818b 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -9,6 +9,11 @@ "index_list": "30-58", "run": false }, + { + "category": "met_tool_wrapper", + "index_list": "59", + "run": false + }, { "category": "air_quality_and_comp", "index_list": "0", diff --git a/docs/Contributors_Guide/create_wrapper.rst b/docs/Contributors_Guide/create_wrapper.rst index 63ffb66c0f..e051a04211 100644 --- a/docs/Contributors_Guide/create_wrapper.rst +++ b/docs/Contributors_Guide/create_wrapper.rst @@ -26,11 +26,13 @@ In metplus/util/doc_util.py, add entries to the LOWER_TO_WRAPPER_NAME dictionary so that the wrapper can be found in the PROCESS_LIST even if it is formatted differently. The key should be the wrapper name in all lower-case letters without any underscores. The value should be the class name -of the wrapper without the "Wrapper" suffix. Examples:: +of the wrapper without the "Wrapper" suffix. Add the new entry in the location +to preserve alphabetical order so it is easier for other developers to find +it. Examples:: - 'newtool': 'NewTool', 'ascii2nc': 'ASCII2NC', 'ensemblestat': 'EnsembleStat', + 'newtool': 'NewTool', The name of a tool can be formatted in different ways depending on the context. For example, the MET tool PCPCombine is written as Pcp-Combine in the MET @@ -59,6 +61,9 @@ Wrapper Components Open the wrapper file for editing the new class. +Naming +^^^^^^ + Rename the class to match the wrapper's class from the above sections. Most wrappers should be a sub-class of the CommandBuilder wrapper:: @@ -67,19 +72,28 @@ Most wrappers should be a sub-class of the CommandBuilder wrapper:: The text 'CommandBuilder' in parenthesis makes NewToolWrapper a subclass of CommandBuilder. +Find and replace can be used to rename all instances of the wrapper name in +the file. For example, to create IODA2NC wrapper from ASCII2NC, replace +**ascii2nc** with **ioda2nc** and **ASCII2NC** with **IODA2NC**. +To create EnsembleStat wrapper from GridStat, replace +**grid_stat** with **ensemble_stat** and +**GridStat** with **EnsembleStat**. + +Parent Class +^^^^^^^^^^^^ + If the new tool falls under one of the existing tool categories, then you can make the tool a subclass of one of those classes. This should only be done if the functions in the parent class are needed by the new wrapper. If you are unsure, then use CommandBuilder. -Refer to the :ref:`basic_components_of_wrappers` section of the Contributor's -Guide for more information on what should be added. - Init Function ^^^^^^^^^^^^^ Modify the init function to initialize NewTool from its base class to set the self.app_name variable to name of the application. +If the application is a MET tool, then set self.app_path to the full path +of the tool under **MET_BIN_DIR**. See the Basic Components :ref:`bc_init_function` section for more information:: def __init__(self, config, instance=None, config_overrides=None): @@ -90,6 +104,43 @@ See the Basic Components :ref:`bc_init_function` section for more information:: instance=instance, config_overrides=config_overrides) +Read Configuration Variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The create_c_dict function is called during the initialization step of each +wrapper. It is where values from the self.config object are read. +The values are stored in the **c_dict** variable that is referenced +throughout the wrapper execution via self.c_dict. + +The function should always start with a call to the parent class' +implementation of the function to read/set any variables that are common to +all wrappers:: + + c_dict = super().create_c_dict() + +The function should also always return the c_dict variable:: + + return c_dict + +File Input/Output +""""""""""""""""" + +METplus configuration variables that end with _DIR and _TEMPLATE are used +to define the criteria to search for input files. + +Allow Multiple Files +"""""""""""""""""""" + +If the application can take more than one file as input for a given category +(i.e. FCST, OBS, ENS, etc.) then ALLOW_MULTIPLE_FILES must be set to True:: + + c_dict['ALLOW_MULTIPLE_FILES'] = True + +This is set to False by default in CommandBuilder's create_c_dict function. +If it is set to False and a list of files are found for an input +(using wildcards or a list of files in the METplus config template variable) +then the wrapper will produce an error and not build the command. + Run Functions ^^^^^^^^^^^^^ @@ -182,6 +233,9 @@ Your use case/example configuration file is located in a directory structure lik Note the documentation file is in METplus/docs while the use case conf file is in METplus/parm +Refer to the :ref:`basic_components_of_wrappers` section of the Contributor's +Guide for more information on what should be added. + Documentation ------------- diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 3484ae5beb..3b5cee3f5f 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -497,7 +497,7 @@ METplus Configuration Glossary ASCII2NC_CONFIG_FILE Path to optional configuration file read by ascii2nc. To utilize a configuration file, set this to - {PARM_BASE}/parm/met_config/Ascii2NcConfig_wrapped. + {PARM_BASE}/met_config/Ascii2NcConfig_wrapped. If unset, no config file will be used. | *Used by:* ASCII2NC @@ -640,7 +640,7 @@ METplus Configuration Glossary | *Used by:* ASCII2NC ASCII2NC_FILE_WINDOW_END - Used to control the upper bound of the window around the valid time to determine if an ASCII2NC input file should be used for processing. Overrides :term:`OBS_FILE_WINDOW_BEGIN`. See 'Use Windows to Find Valid Files' section for more information. + Used to control the upper bound of the window around the valid time to determine if an ASCII2NC input file should be used for processing. Overrides :term:`OBS_FILE_WINDOW_END`. See 'Use Windows to Find Valid Files' section for more information. | *Used by:* ASCII2NC @@ -8266,6 +8266,260 @@ METplus Configuration Glossary | *Used by:* GenEnsProd + LOG_IODA2NC_VERBOSITY + Overrides the log verbosity for IODA2NC only. + If not set, the verbosity level is controlled by :term:`LOG_MET_VERBOSITY`. + + | *Used by:* IODA2NC + + IODA2NC_CUSTOM_LOOP_LIST + Sets custom string loop list for a specific wrapper. + See :term:`CUSTOM_LOOP_LIST`. + + | *Used by:* IODA2NC + + IODA2NC_FILE_WINDOW_BEG + Used to control the lower bound of the window around the valid time to + determine if an IODA2NC input file should be used for processing. + Overrides :term:`OBS_FILE_WINDOW_BEGIN`. + See 'Use Windows to Find Valid Files' section for more information. + + | *Used by:* IODA2NC + + IODA2NC_FILE_WINDOW_END + Used to control the upper bound of the window around the valid time to + determine if an IODA2NC input file should be used for processing. + Overrides :term:`OBS_FILE_WINDOW_END`. + See 'Use Windows to Find Valid Files' section for more information. + + | *Used by:* IODA2NC + + IODA2NC_SKIP_IF_OUTPUT_EXISTS + If True, do not run IODA2NC if output file already exists. Set to False to overwrite files. + + | *Used by:* IODA2NC + + IODA2NC_INPUT_DIR + Directory containing input data to IODA2NC. + This variable is optional because you can specify the full path to the + input files using :term:`IODA2NC_INPUT_TEMPLATE`. + + | *Used by:* IODA2NC + + IODA2NC_INPUT_TEMPLATE + Filename template of the input file used by IODA2NC. + See also :term:`IODA2NC_INPUT_DIR`. + + | *Used by:* IODA2NC + + IODA2NC_OUTPUT_DIR + Directory to write output data generated by IODA2NC. + This variable is optional because you can specify the full path to the + output files using :term:`IODA2NC_OUTPUT_TEMPLATE`. + + | *Used by:* IODA2NC + + IODA2NC_OUTPUT_TEMPLATE + Filename template of the output file generated by IODA2NC. + See also :term:`IODA2NC_OUTPUT_DIR`. + + | *Used by:* IODA2NC + + IODA2NC_VALID_BEG + Used to set the command line argument -valid_beg that controls the + lower bound of valid times of data to use. + Filename template notation can be used, i.e. {valid?fmt=%Y%m%d_%H%M%S} + + | *Used by:* IODA2NC + + IODA2NC_VALID_END + Used to set the command line argument -valid_end that controls the + upper bound of valid times of data to use. + Filename template notation can be used, i.e. + {valid?fmt=%Y%m%d_%H%M%S?shift=1d} (valid time shifted forward one day) + + | *Used by:* IODA2NC + + IODA2NC_NMSG + Used to set the command line argument -nmsg for ioda2nc. + + | *Used by:* IODA2NC + + IODA2NC_CONFIG_FILE + Path to wrapped MET configuration file read by ioda2nc. + If unset, {PARM_BASE}/met_config/IODA2NCConfig_wrapped will be used. + + | *Used by:* IODA2NC + + IODA2NC_MESSAGE_TYPE + Specify the value for 'message_type' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_MESSAGE_TYPE_MAP + Specify the value for 'message_type_map' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_MESSAGE_TYPE_GROUP_MAP + Specify the value for 'message_type_group_map' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_STATION_ID + Specify the value for 'station_id' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_OBS_WINDOW_BEG + Specify the value for 'obs_window.beg' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_OBS_WINDOW_END + Specify the value for 'obs_window.end' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_MASK_GRID + Specify the value for 'mask.grid' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_MASK_POLY + Specify the value for 'mask.poly' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_ELEVATION_RANGE_BEG + Specify the value for 'elevation_range.beg' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_ELEVATION_RANGE_END + Specify the value for 'elevation_range.end' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_LEVEL_RANGE_BEG + Specify the value for 'level_range.beg' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_LEVEL_RANGE_END + Specify the value for 'level_range.end' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_OBS_VAR + Specify the value for 'obs_var' in the MET configuration file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_OBS_NAME_MAP + Specify the value for 'obs_name_map' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_METADATA_MAP + Specify the value for 'metadata_map' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_MISSING_THRESH + Specify the value for 'missing_thresh' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_QUALITY_MARK_THRESH + Specify the value for 'quality_mark_thresh' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_FLAG + Specify the value for 'time_summary.flag' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_RAW_DATA + Specify the value for 'time_summary.raw_data' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_BEG + Specify the value for 'time_summary.beg' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_END + Specify the value for 'time_summary.end' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_STEP + Specify the value for 'time_summary.step' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_WIDTH + Specify the value for 'time_summary.width' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_GRIB_CODE + Specify the value for 'time_summary.grib_code' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_OBS_VAR + Specify the value for 'time_summary.obs_var' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_TYPE + Specify the value for 'time_summary.type' in the MET configuration file + for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_VLD_FREQ + Specify the value for 'time_summary.vld_freq' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_TIME_SUMMARY_VLD_THRESH + Specify the value for 'time_summary.vld_thresh' in the MET configuration + file for IODA2NC. + + | *Used by:* IODA2NC + + IODA2NC_MET_CONFIG_OVERRIDES + Override any variables in the MET configuration file that are not + supported by the wrapper. This should be set to the full variable name + and value that you want to override, including the equal sign and the + ending semi-colon. The value is directly appended to the end of the + wrapped MET config file. + + Example: + IODA2NC_MET_CONFIG_OVERRIDES = desc = "override_desc"; model = "override_model"; + + See :ref:`Overriding Unsupported MET config file settings` for more information + + | *Used by:* IODA2NC + ENSEMBLE_STAT_OBS_QUALITY_INC Specify the value for 'obs_quality_inc' in the MET configuration file for EnsembleStat. diff --git a/docs/Users_Guide/quicksearch.rst b/docs/Users_Guide/quicksearch.rst index b758b1fd8c..4249e3b38c 100644 --- a/docs/Users_Guide/quicksearch.rst +++ b/docs/Users_Guide/quicksearch.rst @@ -20,6 +20,7 @@ Use Cases by MET Tool: | `GenEnsProd <../search.html?q=GenEnsProdToolUseCase&check_keywords=yes&area=default>`_ | `GridStat <../search.html?q=GridStatToolUseCase&check_keywords=yes&area=default>`_ | `GridDiag <../search.html?q=GridDiagToolUseCase&check_keywords=yes&area=default>`_ + | `IODA2NC <../search.html?q=IODA2NCToolUseCase&check_keywords=yes&area=default>`_ | `MODE <../search.html?q=MODEToolUseCase&check_keywords=yes&area=default>`_ | `MTD <../search.html?q=MTDToolUseCase&check_keywords=yes&area=default>`_ | `PB2NC <../search.html?q=PB2NCToolUseCase&check_keywords=yes&area=default>`_ @@ -45,6 +46,7 @@ Use Cases by MET Tool: | **GenEnsProd**: *GenEnsProdToolUseCase* | **GridStat**: *GridStatToolUseCase* | **GridDiag**: *GridDiagToolUseCase* + | **IODA2NC**: *IODA2NCToolUseCase* | **MODE**: *MODEToolUseCase* | **MTD**: *MTDToolUseCase* | **PB2NC**: *PB2NCToolUseCase* diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 18fc97d57e..40df66983e 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -3266,6 +3266,275 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`GRID_STAT_DISTANCE_MAP_BETA_VALUE_N` - distance_map.beta_value(n) +.. _ioda2nc_wrapper: + +IODA2NC +======== + +Description +----------- + +Used to configure the MET tool ioda2nc + +METplus Configuration +--------------------- + +| :term:`IODA2NC_INPUT_DIR` +| :term:`IODA2NC_INPUT_TEMPLATE` +| :term:`IODA2NC_OUTPUT_DIR` +| :term:`IODA2NC_OUTPUT_TEMPLATE` +| :term:`LOG_IODA2NC_VERBOSITY` +| :term:`IODA2NC_SKIP_IF_OUTPUT_EXISTS` +| :term:`IODA2NC_CONFIG_FILE` +| :term:`IODA2NC_FILE_WINDOW_BEG` +| :term:`IODA2NC_FILE_WINDOW_END` +| :term:`IODA2NC_VALID_BEG` +| :term:`IODA2NC_VALID_END` +| :term:`IODA2NC_NMSG` +| :term:`IODA2NC_MESSAGE_TYPE` +| :term:`IODA2NC_MESSAGE_TYPE_MAP` +| :term:`IODA2NC_MESSAGE_TYPE_GROUP_MAP` +| :term:`IODA2NC_STATION_ID` +| :term:`IODA2NC_OBS_WINDOW_BEG` +| :term:`IODA2NC_OBS_WINDOW_END` +| :term:`IODA2NC_MASK_GRID` +| :term:`IODA2NC_MASK_POLY` +| :term:`IODA2NC_ELEVATION_RANGE_BEG` +| :term:`IODA2NC_ELEVATION_RANGE_END` +| :term:`IODA2NC_LEVEL_RANGE_BEG` +| :term:`IODA2NC_LEVEL_RANGE_END` +| :term:`IODA2NC_OBS_VAR` +| :term:`IODA2NC_OBS_NAME_MAP` +| :term:`IODA2NC_METADATA_MAP` +| :term:`IODA2NC_MISSING_THRESH` +| :term:`IODA2NC_QUALITY_MARK_THRESH` +| :term:`IODA2NC_TIME_SUMMARY_FLAG` +| :term:`IODA2NC_TIME_SUMMARY_RAW_DATA` +| :term:`IODA2NC_TIME_SUMMARY_BEG` +| :term:`IODA2NC_TIME_SUMMARY_END` +| :term:`IODA2NC_TIME_SUMMARY_STEP` +| :term:`IODA2NC_TIME_SUMMARY_WIDTH` +| :term:`IODA2NC_TIME_SUMMARY_GRIB_CODE` +| :term:`IODA2NC_TIME_SUMMARY_OBS_VAR` +| :term:`IODA2NC_TIME_SUMMARY_TYPE` +| :term:`IODA2NC_TIME_SUMMARY_VLD_FREQ` +| :term:`IODA2NC_TIME_SUMMARY_VLD_THRESH` +| :term:`IODA2NC_CUSTOM_LOOP_LIST` +| :term:`IODA2NC_MET_CONFIG_OVERRIDES` + +.. _ioda2nc-met-conf: + +MET Configuration +----------------- + +Below is the wrapped MET configuration file used for this wrapper. +Environment variables are used to control entries in this configuration file. +The default value for each environment variable is obtained from +(except where noted below): + +`MET_INSTALL_DIR/share/met/config/IODA2NCConfig_default `_ + +Below the file contents are descriptions of each environment variable +referenced in this file and the corresponding METplus configuration item used +to set the value of the environment variable. For detailed examples showing +how METplus sets the values of these environment variables, +see :ref:`How METplus controls MET config file settings`. + +.. literalinclude:: ../../parm/met_config/IODA2NCConfig_wrapped + +**${METPLUS_MESSAGE_TYPE}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_MESSAGE_TYPE` + - message_type + +**${METPLUS_MESSAGE_TYPE_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_MESSAGE_TYPE_MAP` + - message_type_map + +**${METPLUS_MESSAGE_TYPE_GROUP_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_MESSAGE_TYPE_GROUP_MAP` + - message_type_group_map + +**${METPLUS_STATION_ID}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_STATION_ID` + - station_id + +**${METPLUS_OBS_WINDOW_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_OBS_WINDOW_BEG` + - obs_window.beg + * - :term:`IODA2NC_OBS_WINDOW_END` + - obs_window.end + +**${METPLUS_MASK_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_MASK_GRID` + - mask.grid + * - :term:`IODA2NC_MASK_POLY` + - mask.poly + +**${METPLUS_ELEVATION_RANGE_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_ELEVATION_RANGE_BEG` + - elevation_range.beg + * - :term:`IODA2NC_ELEVATION_RANGE_END` + - elevation_range.end + +**${METPLUS_LEVEL_RANGE_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_LEVEL_RANGE_BEG` + - level_range.beg + * - :term:`IODA2NC_LEVEL_RANGE_END` + - level_range.end + +**${METPLUS_OBS_VAR}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_OBS_VAR` + - obs_var + +**${METPLUS_OBS_NAME_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_OBS_NAME_MAP` + - obs_name_map + +**${METPLUS_METADATA_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_METADATA_MAP` + - metadata_map + +**${METPLUS_MISSING_THRESH}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_MISSING_THRESH` + - missing_thresh + +**${METPLUS_QUALITY_MARK_THRESH}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_QUALITY_MARK_THRESH` + - quality_mark_thresh + +**${METPLUS_TIME_SUMMARY_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_TIME_SUMMARY_FLAG` + - time_summary.flag + * - :term:`IODA2NC_TIME_SUMMARY_RAW_DATA` + - time_summary.raw_data + * - :term:`IODA2NC_TIME_SUMMARY_BEG` + - time_summary.beg + * - :term:`IODA2NC_TIME_SUMMARY_END` + - time_summary.end + * - :term:`IODA2NC_TIME_SUMMARY_STEP` + - time_summary.step + * - :term:`IODA2NC_TIME_SUMMARY_WIDTH` + - time_summary.width + * - :term:`IODA2NC_TIME_SUMMARY_GRIB_CODE` + - time_summary.grib_code + * - :term:`IODA2NC_TIME_SUMMARY_OBS_VAR` + - time_summary.obs_var + * - :term:`IODA2NC_TIME_SUMMARY_TYPE` + - time_summary.type + * - :term:`IODA2NC_TIME_SUMMARY_VLD_FREQ` + - time_summary.vld_freq + * - :term:`IODA2NC_TIME_SUMMARY_VLD_THRESH` + - time_summary.vld_thresh + +**${METPLUS_MET_CONFIG_OVERRIDES}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`IODA2NC_MET_CONFIG_OVERRIDES` + - n/a + .. _make_plots_wrapper: MakePlots diff --git a/docs/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.py b/docs/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.py new file mode 100644 index 0000000000..6e997293f7 --- /dev/null +++ b/docs/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.py @@ -0,0 +1,110 @@ +""" +IODA2NC: Basic Use Case +======================= + +met_tool_wrapper/IODA2NC/IODA2NC.conf + +""" +############################################################################## +# Scientific Objective +# -------------------- +# +# Convert IODA NetCDF files to MET NetCDF format. + +############################################################################## +# Datasets +# -------- +# +# **Input:** IODA NetCDF observation +# +# **Location:** All of the input data required for this use case can be found +# in the met_test sample data tarball. Click here to the METplus releases +# page and download sample data for the appropriate release: +# https://github.com/dtcenter/METplus/releases +# This tarball should be unpacked into the directory that you will set the +# value of INPUT_BASE. See the `Running METplus`_ section for more information. +# + +############################################################################## +# METplus Components +# ------------------ +# +# This use case utilizes the METplus IODA2NC wrapper to generate a command +# to run the MET tool ioda2nc if all required files are found. + +############################################################################## +# METplus Workflow +# ---------------- +# +# IODA2NC is the only tool called in this example. +# It processes the following run time(s): +# +# | **Valid:** 2020-03-10 12Z +# + +############################################################################## +# METplus Configuration +# --------------------- +# +# parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf + +############################################################################## +# MET Configuration +# --------------------- +# +# .. note:: +# See the :ref:`IODA2NC MET Configuration` +# section of the User's Guide for more information on the environment +# variables used in the file below. +# +# parm/met_config/IODA2NCConfig_wrapped +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/met_config/IODA2NCConfig_wrapped + +############################################################################## +# Running METplus +# --------------- +# +# Provide the use case .conf configuration file to the run_metplus.py script. +# +# /path/to/METplus/parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf +# +# See the :ref:`running-metplus` section of the System Configuration chapter +# for more details. +# + +############################################################################## +# Expected Output +# --------------- +# +# A successful run will output the following to the screen and the logfile:: +# +# INFO: METplus has successfully finished running. +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data +# was generated. Output for this use case will be found in +# met_tool_wrapper/ioda2nc +# (relative to **OUTPUT_BASE**) +# and will contain the following file(s): +# +# * ioda.NC001007.2020031012.summary.nc +# + +############################################################################## +# Keywords +# -------- +# +# .. note:: +# +# * IODA2NCToolUseCase +# +# Navigate to :ref:`quick-search` to discover other similar use cases. +# +# +# +# sphinx_gallery_thumbnail_path = '_static/met_tool_wrapper-IODA2NC.png' +# diff --git a/docs/use_cases/met_tool_wrapper/IODA2NC/README.rst b/docs/use_cases/met_tool_wrapper/IODA2NC/README.rst new file mode 100644 index 0000000000..84d389d33c --- /dev/null +++ b/docs/use_cases/met_tool_wrapper/IODA2NC/README.rst @@ -0,0 +1,2 @@ +IODA2NC +------- diff --git a/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py b/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py index 48da2ba78d..ed27e0784f 100644 --- a/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py +++ b/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py @@ -51,60 +51,6 @@ def set_minimum_config_settings(config): config.set('config', 'ENS_VAR1_NAME', ens_name) config.set('config', 'ENS_VAR1_LEVELS', ens_level) -@pytest.mark.parametrize( - 'config_overrides, env_var_values', [ - # 0 no climo settings - ({}, {}), - # 1 mean template only - ({'GEN_ENS_PROD_CLIMO_MEAN_INPUT_TEMPLATE': 'gs_mean_{init?fmt=%Y%m%d%H}.tmpl'}, - {'CLIMO_MEAN_FILE': '"gs_mean_YMDH.tmpl"', - 'CLIMO_STDEV_FILE': '', }), - # 2 mean template and dir - ({'GEN_ENS_PROD_CLIMO_MEAN_INPUT_TEMPLATE': 'gs_mean_{init?fmt=%Y%m%d%H}.tmpl', - 'GEN_ENS_PROD_CLIMO_MEAN_INPUT_DIR': '/climo/mean/dir'}, - {'CLIMO_MEAN_FILE': '"/climo/mean/dir/gs_mean_YMDH.tmpl"', - 'CLIMO_STDEV_FILE': '', }), - # 3 stdev template only - ({'GEN_ENS_PROD_CLIMO_STDEV_INPUT_TEMPLATE': 'gs_stdev_{init?fmt=%Y%m%d%H}.tmpl'}, - {'CLIMO_STDEV_FILE': '"gs_stdev_YMDH.tmpl"', }), - # 4 stdev template and dir - ({'GEN_ENS_PROD_CLIMO_STDEV_INPUT_TEMPLATE': 'gs_stdev_{init?fmt=%Y%m%d%H}.tmpl', - 'GEN_ENS_PROD_CLIMO_STDEV_INPUT_DIR': '/climo/stdev/dir'}, - {'CLIMO_STDEV_FILE': '"/climo/stdev/dir/gs_stdev_YMDH.tmpl"', }), - ] -) -def test_handle_climo_file_variables(metplus_config, config_overrides, - env_var_values): - """! Ensure that old and new variables for setting climo_mean and - climo_stdev are set to the correct values - """ - old_env_vars = ['CLIMO_MEAN_FILE', - 'CLIMO_STDEV_FILE'] - config = metplus_config() - - set_minimum_config_settings(config) - - # set config variable overrides - for key, value in config_overrides.items(): - config.set('config', key, value) - - wrapper = GenEnsProdWrapper(config) - assert wrapper.isOK - - all_cmds = wrapper.run_all_times() - for (_, actual_env_vars), run_time in zip(all_cmds, run_times): - run_dt = datetime.strptime(run_time, time_fmt) - ymdh = run_dt.strftime('%Y%m%d%H') - print(f"ACTUAL ENV VARS: {actual_env_vars}") - for old_env in old_env_vars: - match = next((item for item in actual_env_vars if - item.startswith(old_env)), None) - assert(match is not None) - actual_value = match.split('=', 1)[1] - expected_value = env_var_values.get(old_env, '') - expected_value = expected_value.replace('YMDH', ymdh) - assert(expected_value == actual_value) - @pytest.mark.parametrize( 'config_overrides, env_var_values', [ ({'MODEL': 'my_model'}, diff --git a/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py b/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py new file mode 100644 index 0000000000..47030515d3 --- /dev/null +++ b/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py @@ -0,0 +1,246 @@ +import os + +import pytest + +from metplus.wrappers.ioda2nc_wrapper import IODA2NCWrapper + + +time_fmt = '%Y%m%d%H' +run_times = ['2020031012', '2020031100'] + +def set_minimum_config_settings(config): + config.set('config', 'DO_NOT_RUN_EXE', True) + config.set('config', 'INPUT_MUST_EXIST', False) + + # set process and time config variables + config.set('config', 'PROCESS_LIST', 'IODA2NC') + config.set('config', 'LOOP_BY', 'VALID') + config.set('config', 'VALID_TIME_FMT', time_fmt) + config.set('config', 'VALID_BEG', run_times[0]) + config.set('config', 'VALID_END', run_times[-1]) + config.set('config', 'VALID_INCREMENT', '12H') + config.set('config', 'LOOP_ORDER', 'times') + config.set('config', 'IODA2NC_INPUT_DIR', + '{INPUT_BASE}/met_test/new/ioda') + config.set('config', 'IODA2NC_INPUT_TEMPLATE', + 'ioda.NC001007.{valid?fmt=%Y%m%d%H}.nc') + config.set('config', 'IODA2NC_OUTPUT_DIR', + '{OUTPUT_BASE}/ioda2nc') + config.set('config', 'IODA2NC_OUTPUT_TEMPLATE', + 'ioda.NC001007.{valid?fmt=%Y%m%d%H}.summary.nc') + +@pytest.mark.parametrize( + 'config_overrides, env_var_values, extra_args', [ + # 0 + ({'IODA2NC_MESSAGE_TYPE': 'ADPUPA, ADPSFC', }, + {'METPLUS_MESSAGE_TYPE': 'message_type = ["ADPUPA", "ADPSFC"];'}, ''), + # 1 + ({'IODA2NC_MESSAGE_TYPE_MAP': '{ key = “AIRCAR”; val = “AIRCAR_PROFILES”; }', }, + {'METPLUS_MESSAGE_TYPE_MAP': 'message_type_map = [{ key = “AIRCAR”; val = “AIRCAR_PROFILES”; }];'}, ''), + # 2 + ({'IODA2NC_MESSAGE_TYPE_GROUP_MAP': '{ key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET";},{ key = "ANYAIR"; val = "AIRCAR,AIRCFT";}', }, + {'METPLUS_MESSAGE_TYPE_GROUP_MAP': 'message_type_group_map = [{ key = "SURFACE"; val = "ADPSFC, SFCSHP, MSONET";}, { key = "ANYAIR"; val = "AIRCAR, AIRCFT";}];'}, ''), + # 3 + ({'IODA2NC_STATION_ID': 'value1, value2', }, + {'METPLUS_STATION_ID': 'station_id = ["value1", "value2"];'}, ''), + # 4 + ({'IODA2NC_OBS_WINDOW_BEG': '-5400', }, + {'METPLUS_OBS_WINDOW_DICT': 'obs_window = {beg = -5400;}'}, ''), + # 5 + ({'IODA2NC_OBS_WINDOW_END': '5400', }, + {'METPLUS_OBS_WINDOW_DICT': 'obs_window = {end = 5400;}'}, ''), + # 6 + ({ + 'IODA2NC_OBS_WINDOW_BEG': '-5400', + 'IODA2NC_OBS_WINDOW_END': '5400', + }, + {'METPLUS_OBS_WINDOW_DICT': 'obs_window = {beg = -5400;end = 5400;}'} + , ''), + # 7 + ({'IODA2NC_MASK_GRID': 'FULL', }, + {'METPLUS_MASK_DICT': 'mask = {grid = "FULL";}'}, ''), + # 8 + ({'IODA2NC_MASK_POLY': '/some/polyfile.nc', }, + {'METPLUS_MASK_DICT': 'mask = {poly = "/some/polyfile.nc";}'}, ''), + # 9 + ({ + 'IODA2NC_MASK_GRID': 'FULL', + 'IODA2NC_MASK_POLY': '/some/polyfile.nc', + }, + {'METPLUS_MASK_DICT': 'mask = {grid = "FULL";poly = "/some/polyfile.nc";}'}, ''), + # 10 + ({'IODA2NC_ELEVATION_RANGE_BEG': '-1000', }, + {'METPLUS_ELEVATION_RANGE_DICT': 'elevation_range = {beg = -1000;}'}, ''), + # 11 + ({'IODA2NC_ELEVATION_RANGE_END': '100000', }, + {'METPLUS_ELEVATION_RANGE_DICT': 'elevation_range = {end = 100000;}'}, ''), + # 12 + ({ + 'IODA2NC_ELEVATION_RANGE_BEG': '-1000', + 'IODA2NC_ELEVATION_RANGE_END': '100000', + }, + {'METPLUS_ELEVATION_RANGE_DICT': 'elevation_range = {beg = -1000;end = 100000;}'}, ''), + # 13 + ({'IODA2NC_LEVEL_RANGE_BEG': '1', }, + {'METPLUS_LEVEL_RANGE_DICT': 'level_range = {beg = 1;}'}, ''), + # 14 + ({'IODA2NC_LEVEL_RANGE_END': '255', }, + {'METPLUS_LEVEL_RANGE_DICT': 'level_range = {end = 255;}'}, ''), + # 15 + ({ + 'IODA2NC_LEVEL_RANGE_BEG': '1', + 'IODA2NC_LEVEL_RANGE_END': '255', + }, + {'METPLUS_LEVEL_RANGE_DICT': 'level_range = {beg = 1;end = 255;}'}, ''), + # 16 + ({'IODA2NC_OBS_VAR': 'TMP,WDIR,RH', }, + {'METPLUS_OBS_VAR': 'obs_var = ["TMP", "WDIR", "RH"];'}, ''), + # 17 + ({'IODA2NC_OBS_NAME_MAP': '{ key = "message_type"; val = "msg_type"; },{ key = "station_id"; val = "report_identifier"; }', }, + {'METPLUS_OBS_NAME_MAP': 'obs_name_map = [{ key = "message_type"; val = "msg_type"; }, { key = "station_id"; val = "report_identifier"; }];'}, ''), + # 18 + ({'IODA2NC_METADATA_MAP': '{ key = "message_type"; val = "msg_type"; },{ key = "station_id"; val = "report_identifier"; }', }, + {'METPLUS_METADATA_MAP': 'metadata_map = [{ key = "message_type"; val = "msg_type"; }, { key = "station_id"; val = "report_identifier"; }];'}, ''), + # 19 + ({'IODA2NC_MISSING_THRESH': '<=-1e9, >=1e9, ==-9999', }, + {'METPLUS_MISSING_THRESH': 'missing_thresh = [<=-1e9, >=1e9, ==-9999];'}, ''), + # 20 + ({'IODA2NC_QUALITY_MARK_THRESH': '2', }, + {'METPLUS_QUALITY_MARK_THRESH': 'quality_mark_thresh = 2;'}, ''), + # 21 + ({}, + {'METPLUS_TIME_SUMMARY_DICT': ''}, ''), + # 22 + ({'IODA2NC_TIME_SUMMARY_FLAG': 'True'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {flag = TRUE;}'}, ''), + # 23 + ({'IODA2NC_TIME_SUMMARY_RAW_DATA': 'true'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {raw_data = TRUE;}'}, ''), + # 24 + ({'IODA2NC_TIME_SUMMARY_BEG': '123456'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {beg = "123456";}'}, ''), + # 25 + ({'IODA2NC_TIME_SUMMARY_END': '123456'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {end = "123456";}'}, ''), + # 26 + ({'IODA2NC_TIME_SUMMARY_STEP': '500'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {step = 500;}'}, ''), + # 27 + ({'IODA2NC_TIME_SUMMARY_WIDTH': '900'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {width = 900;}'}, ''), + # 28 width as dictionary + ({'IODA2NC_TIME_SUMMARY_WIDTH': '{ beg = -21600; end = 0; }'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {width = { beg = -21600; end = 0; };}'}, ''), + # 29 + ({'IODA2NC_TIME_SUMMARY_GRIB_CODE': '12, 203, 212'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {grib_code = [12, 203, 212];}'}, ''), + # 30 + ({'IODA2NC_TIME_SUMMARY_OBS_VAR': 'TMP, HGT, PRES'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {obs_var = ["TMP", "HGT", "PRES"];}'}, ''), + # 31 + ({'IODA2NC_TIME_SUMMARY_TYPE': 'min, range, max'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {type = ["min", "range", "max"];}'}, ''), + # 32 + ({'IODA2NC_TIME_SUMMARY_VALID_FREQ': '2'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {vld_freq = 2;}'}, ''), + # 33 + ({'IODA2NC_TIME_SUMMARY_VALID_THRESH': '0.5'}, + {'METPLUS_TIME_SUMMARY_DICT': + 'time_summary = {vld_thresh = 0.5;}'}, ''), + # 34 additional input file with full path + ({'IODA2NC_INPUT_TEMPLATE': 'ioda.NC001007.{valid?fmt=%Y%m%d%H}.nc, /other/file.nc'}, + {}, ' -iodafile /other/file.nc'), + # 35 additional input file with relative path + ({'IODA2NC_INPUT_TEMPLATE': 'ioda.NC001007.{valid?fmt=%Y%m%d%H}.nc, other/file.nc'}, + {}, ' -iodafile *INPUT_DIR*/other/file.nc'), + # 36 + ({'IODA2NC_VALID_BEG': '20200309_12'}, + {}, ' -valid_beg 20200309_12'), + # 37 + ({'IODA2NC_VALID_END': '20200310_12'}, + {}, ' -valid_end 20200310_12'), + # 38 + ({'IODA2NC_NMSG': '10'}, + {}, ' -nmsg 10'), + # 39 all optional command line args + ({'IODA2NC_INPUT_TEMPLATE': 'ioda.NC001007.{valid?fmt=%Y%m%d%H}.nc, other/file.nc', + 'IODA2NC_VALID_BEG': '20200309_12', + 'IODA2NC_VALID_END': '20200310_12', + 'IODA2NC_NMSG': '10', + }, + {}, ' -iodafile *INPUT_DIR*/other/file.nc -valid_beg 20200309_12 -valid_end 20200310_12 -nmsg 10'), + ] +) +def test_ioda2nc_wrapper(metplus_config, config_overrides, + env_var_values, extra_args): + config = metplus_config() + + set_minimum_config_settings(config) + + # set config variable overrides + for key, value in config_overrides.items(): + config.set('config', key, value) + + wrapper = IODA2NCWrapper(config) + assert wrapper.isOK + + input_dir = wrapper.c_dict.get('OBS_INPUT_DIR') + output_dir = wrapper.c_dict.get('OUTPUT_DIR') + + app_path = os.path.join(config.getdir('MET_BIN_DIR'), wrapper.app_name) + verbosity = f"-v {wrapper.c_dict['VERBOSITY']}" + config_file = wrapper.c_dict.get('CONFIG_FILE') + + extra_args = extra_args.replace('*INPUT_DIR*', input_dir) + expected_cmds = [ + (f"{app_path} {verbosity} {input_dir}/ioda.NC001007.2020031012.nc" + f" {output_dir}/ioda.NC001007.2020031012.summary.nc" + f" -config {config_file}{extra_args}"), + (f"{app_path} {verbosity} {input_dir}/ioda.NC001007.2020031100.nc" + f" {output_dir}/ioda.NC001007.2020031100.summary.nc" + f" -config {config_file}{extra_args}"), + ] + + all_cmds = wrapper.run_all_times() + print(f"ALL COMMANDS: {all_cmds}") + assert len(all_cmds) == len(expected_cmds) + + for (cmd, env_vars), expected_cmd in zip(all_cmds, expected_cmds): + # ensure commands are generated as expected + assert cmd == expected_cmd + + # check that environment variables were set properly + for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + match = next((item for item in env_vars if + item.startswith(env_var_key)), None) + assert match is not None + actual_value = match.split('=', 1)[1] + assert(env_var_values.get(env_var_key, '') == actual_value) + +def test_get_config_file(metplus_config): + fake_config_name = '/my/config/file' + config = metplus_config() + config.set('config', 'INPUT_MUST_EXIST', False) + + wrapper = IODA2NCWrapper(config) + + default_config_file = os.path.join(config.getdir('PARM_BASE'), + 'met_config', + 'IODA2NCConfig_wrapped') + + assert wrapper.c_dict['CONFIG_FILE'] == default_config_file + + config.set('config', 'IODA2NC_CONFIG_FILE', fake_config_name) + wrapper = IODA2NCWrapper(config) + assert wrapper.c_dict['CONFIG_FILE'] == fake_config_name diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index a9ea0ea510..2d196b9b67 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -58,6 +58,7 @@ Category: met_tool_wrapper 56::GFDLTracker_ETC::met_tool_wrapper/GFDLTracker/GFDLTracker_ETC.conf::gfdl-tracker_env 57::GFDLTracker_Genesis::met_tool_wrapper/GFDLTracker/GFDLTracker_Genesis.conf::gfdl-tracker_env 58::GenEnsProd::met_tool_wrapper/GenEnsProd/GenEnsProd.conf +59::IODA2NC::met_tool_wrapper/IODA2NC/IODA2NC.conf Category: air_quality_and_comp 0::EnsembleStat_fcstICAP_obsMODIS_aod::model_applications/air_quality_and_comp/EnsembleStat_fcstICAP_obsMODIS_aod.conf diff --git a/metplus/util/doc_util.py b/metplus/util/doc_util.py index e72337ad6d..5bf834d86f 100755 --- a/metplus/util/doc_util.py +++ b/metplus/util/doc_util.py @@ -17,6 +17,7 @@ 'gfdltracker': 'GFDLTracker', 'griddiag': 'GridDiag', 'gridstat': 'GridStat', + 'ioda2nc': 'IODA2NC', 'makeplots': 'MakePlots', 'metdbload': 'METDbLoad', 'mode': 'MODE', diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index 4e5118b6a3..414b9795a4 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -1694,7 +1694,7 @@ def getlist(list_str, expand_begin_end_incr=True): return [] # FIRST remove surrounding comma, and spaces, form the string. - list_str = list_str.strip().strip(',').strip() + list_str = list_str.strip(';[] ').strip().strip(',').strip() # remove space around commas list_str = re.sub(r'\s*,\s*', ',', list_str) diff --git a/metplus/wrappers/ascii2nc_wrapper.py b/metplus/wrappers/ascii2nc_wrapper.py index 23d7c53e93..01e675833f 100755 --- a/metplus/wrappers/ascii2nc_wrapper.py +++ b/metplus/wrappers/ascii2nc_wrapper.py @@ -76,11 +76,11 @@ def create_c_dict(self): ) # MET config variables - self.handle_time_summary_dict(c_dict, - ['TIME_SUMMARY_GRIB_CODES', - 'TIME_SUMMARY_VAR_NAMES', - 'TIME_SUMMARY_TYPES'] - ) + self.handle_time_summary_legacy(c_dict, + ['TIME_SUMMARY_GRIB_CODES', + 'TIME_SUMMARY_VAR_NAMES', + 'TIME_SUMMARY_TYPES'] + ) # handle file window variables for edge in ['BEGIN', 'END']: diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 9e22aa6642..4384f45307 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -1984,7 +1984,43 @@ def get_env_var_value(self, env_var_name, read_dict=None, item_type=None): return mask_value.split('=', 1)[1].rstrip(';').strip() - def handle_time_summary_dict(self, c_dict, remove_bracket_list=None): + def handle_time_summary_dict(self): + """! Read METplusConfig variables for the MET config time_summary + dictionary and format values into an environment variable + METPLUS_TIME_SUMMARY_DICT that is referenced in the wrapped MET + config files. + """ + self.handle_met_config_dict('time_summary', { + 'flag': 'bool', + 'raw_data': 'bool', + 'beg': 'string', + 'end': 'string', + 'step': 'int', + 'width': ('string', 'remove_quotes'), + 'grib_code': ('list', 'remove_quotes,allow_empty', None, + ['TIME_SUMMARY_GRIB_CODES']), + 'obs_var': ('list', 'allow_empty', None, + ['TIME_SUMMARY_VAR_NAMES']), + 'type': ('list', 'allow_empty', None, ['TIME_SUMMARY_TYPES']), + 'vld_freq': ('int', None, None, ['TIME_SUMMARY_VALID_FREQ']), + 'vld_thresh': ('float', None, None, ['TIME_SUMMARY_VALID_THRESH']), + }) + + def handle_time_summary_legacy(self, c_dict, remove_bracket_list=None): + """! Read METplusConfig variables for the MET config time_summary + dictionary and format values into environment variable + METPLUS_TIME_SUMMARY_DICT as well as other environment variables + that contain individuals items of the time_summary dictionary + that were referenced in wrapped MET config files prior to METplus 4.0. + Developer note: If we discontinue support for legacy wrapped MET + config files + + @param c_dict dictionary to store time_summary item values + @param remove_bracket_list (optional) list of items that need the + square brackets around the value removed because the legacy (pre 4.0) + wrapped MET config includes square braces around the environment + variable. + """ tmp_dict = {} app = self.app_name.upper() self.set_met_config_bool(tmp_dict, @@ -2054,7 +2090,7 @@ def handle_time_summary_dict(self, c_dict, remove_bracket_list=None): time_summary = self.format_met_config_dict(tmp_dict, 'time_summary', - keys=None) + keys=None) self.env_var_dict['METPLUS_TIME_SUMMARY_DICT'] = time_summary # set c_dict values to support old method of setting env vars @@ -2307,6 +2343,11 @@ def add_met_config(self, **kwargs): in order of precedence (first variable is used if it is set, otherwise 2nd variable is used if set, etc.) """ + # if metplus_configs is not provided, use _ + if not kwargs.get('metplus_configs'): + kwargs['metplus_configs'] = [ + f"{self.app_name}_{kwargs.get('name')}".upper() + ] item = met_config(**kwargs) output_dict = kwargs.get('output_dict') self.handle_met_config_item(item, output_dict) diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index 3c2a4367de..00c7c58e73 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -59,10 +59,6 @@ def __init__(self, config, instance=None, config_overrides=None): def create_c_dict(self): c_dict = super().create_c_dict() - c_dict['VERBOSITY'] = self.config.getstr('config', - 'LOG_GEN_ENS_PROD_VERBOSITY', - c_dict['VERBOSITY']) - # get the MET config file path or use default c_dict['CONFIG_FILE'] = self.get_config_file( 'GenEnsProdConfig_wrapped' diff --git a/metplus/wrappers/ioda2nc_wrapper.py b/metplus/wrappers/ioda2nc_wrapper.py new file mode 100755 index 0000000000..3fecb4a4b0 --- /dev/null +++ b/metplus/wrappers/ioda2nc_wrapper.py @@ -0,0 +1,174 @@ +""" +Program Name: ioda2nc_wrapper.py +Contact(s): George McCabe +Abstract: Builds commands to run ioda2nc +""" + +import os + +from ..util import do_string_sub +from . import LoopTimesWrapper + +'''!@namespace IODA2NCWrapper +@brief Wraps the IODA2NC tool to reformat IODA NetCDF data to MET NetCDF +@endcode +''' + + +class IODA2NCWrapper(LoopTimesWrapper): + + WRAPPER_ENV_VAR_KEYS = [ + 'METPLUS_MESSAGE_TYPE', + 'METPLUS_MESSAGE_TYPE_GROUP_MAP', + 'METPLUS_MESSAGE_TYPE_MAP', + 'METPLUS_STATION_ID', + 'METPLUS_OBS_WINDOW_DICT', + 'METPLUS_MASK_DICT', + 'METPLUS_ELEVATION_RANGE_DICT', + 'METPLUS_LEVEL_RANGE_DICT', + 'METPLUS_OBS_VAR', + 'METPLUS_OBS_NAME_MAP', + 'METPLUS_METADATA_MAP', + 'METPLUS_MISSING_THRESH', + 'METPLUS_QUALITY_MARK_THRESH', + 'METPLUS_TIME_SUMMARY_DICT', + ] + + def __init__(self, config, instance=None, config_overrides=None): + self.app_name = "ioda2nc" + self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), + self.app_name) + super().__init__(config, + instance=instance, + config_overrides=config_overrides) + + def create_c_dict(self): + """! Read METplusConfig object and sets values in dictionary to be + used by the wrapper to generate commands. Gets information regarding + input/output files, optional command line arguments, and values to + set in the wrapped MET config file. Calls self.log_error if any + required METplusConfig variables were not set properly which logs the + error and sets self.isOK to False which causes wrapper initialization + to fail. + + @returns dictionary containing configurations for this wrapper + """ + c_dict = super().create_c_dict() + + # file I/O + c_dict['ALLOW_MULTIPLE_FILES'] = True + c_dict['OBS_INPUT_DIR'] = self.config.getdir('IODA2NC_INPUT_DIR', '') + c_dict['OBS_INPUT_TEMPLATE'] = ( + self.config.getraw('config', 'IODA2NC_INPUT_TEMPLATE') + ) + if not c_dict['OBS_INPUT_TEMPLATE']: + self.log_error("IODA2NC_INPUT_TEMPLATE required to run") + + # handle input file window variables + self.handle_file_window_variables(c_dict, dtypes=['OBS']) + + c_dict['OUTPUT_DIR'] = self.config.getdir('IODA2NC_OUTPUT_DIR', '') + c_dict['OUTPUT_TEMPLATE'] = ( + self.config.getraw('config', 'IODA2NC_OUTPUT_TEMPLATE') + ) + + # optional command line arguments + c_dict['VALID_BEG'] = self.config.getraw('config', 'IODA2NC_VALID_BEG') + c_dict['VALID_END'] = self.config.getraw('config', 'IODA2NC_VALID_END') + c_dict['NMSG'] = self.config.getint('config', 'IODA2NC_NMSG', 0) + + # MET config variables + c_dict['CONFIG_FILE'] = self.get_config_file('IODA2NCConfig_wrapped') + + self.add_met_config(name='message_type', data_type='list') + self.add_met_config(name='message_type_map', data_type='list', + extra_args={'remove_quotes': True}) + self.add_met_config(name='message_type_group_map', data_type='list', + extra_args={'remove_quotes': True}) + self.add_met_config(name='station_id', data_type='list') + self.handle_met_config_window('obs_window') + self.handle_mask(single_value=True) + self.handle_met_config_window('elevation_range') + self.handle_met_config_window('level_range') + self.add_met_config(name='obs_var', data_type='list') + self.add_met_config(name='obs_name_map', data_type='list', + extra_args={'remove_quotes': True}) + self.add_met_config(name='metadata_map', data_type='list', + extra_args={'remove_quotes': True}) + self.add_met_config(name='missing_thresh', data_type='list', + extra_args={'remove_quotes': True}) + self.add_met_config(name='quality_mark_thresh', data_type='int') + self.handle_time_summary_dict() + + return c_dict + + def get_command(self): + """! Build the command to call ioda2nc + + @returns string containing command to run + """ + return (f"{self.app_path} -v {self.c_dict['VERBOSITY']}" + f" {self.infiles[0]} {self.get_output_path()}" + f" {' '.join(self.args)}") + + def run_at_time_once(self, time_info): + """! Process runtime and try to build command to run ioda2nc + + @param time_info dictionary containing timing information + @returns True if command was built/run successfully or + False if something went wrong + """ + # get input files + if not self.find_input_files(time_info): + return False + + # get output path + if not self.find_and_check_output_file(time_info): + return False + + # get other configurations for command + self.set_command_line_arguments(time_info) + + # set environment variables if using config file + self.set_environment_variables(time_info) + + # build command and run + return self.build() + + def find_input_files(self, time_info): + """! Get all input files for ioda2nc. Sets self.infiles list. + + @param time_info dictionary containing timing information + @returns List of files that were found or None if no files were found + """ + # get list of files even if only one is found (return_list=True) + obs_path = self.find_obs(time_info, var_info=None, return_list=True) + if obs_path is None: + return None + + self.infiles.extend(obs_path) + return self.infiles + + def set_command_line_arguments(self, time_info): + """! Set all arguments for ioda2nc command. + Note: -obs_var will be set in wrapped MET config file, not command line + + @param time_info dictionary containing timing information + """ + config_file = do_string_sub(self.c_dict['CONFIG_FILE'], **time_info) + self.args.append(f"-config {config_file}") + + # if more than 1 input file was found, add them with -iodafile + for infile in self.infiles[1:]: + self.args.append(f"-iodafile {infile}") + + if self.c_dict['VALID_BEG']: + valid_beg = do_string_sub(self.c_dict['VALID_BEG'], **time_info) + self.args.append(f"-valid_beg {valid_beg}") + + if self.c_dict['VALID_END']: + valid_end = do_string_sub(self.c_dict['VALID_END'], **time_info) + self.args.append(f"-valid_end {valid_end}") + + if self.c_dict['NMSG']: + self.args.append(f"-nmsg {self.c_dict['NMSG']}") diff --git a/metplus/wrappers/pb2nc_wrapper.py b/metplus/wrappers/pb2nc_wrapper.py index 2e8241c11f..891fc367bc 100755 --- a/metplus/wrappers/pb2nc_wrapper.py +++ b/metplus/wrappers/pb2nc_wrapper.py @@ -111,7 +111,7 @@ def create_c_dict(self): 'METPLUS_OBS_BUFR_VAR', allow_empty=True) - self.handle_time_summary_dict(c_dict) + self.handle_time_summary_legacy(c_dict) self.handle_file_window_variables(c_dict, dtypes=['OBS']) diff --git a/metplus/wrappers/runtime_freq_wrapper.py b/metplus/wrappers/runtime_freq_wrapper.py index 8e02885295..eccc0604a5 100755 --- a/metplus/wrappers/runtime_freq_wrapper.py +++ b/metplus/wrappers/runtime_freq_wrapper.py @@ -46,7 +46,7 @@ def create_c_dict(self): app_name_upper = self.app_name.upper() c_dict['VERBOSITY'] = ( - self.config.getstr('config', + self.config.getint('config', f'LOG_{app_name_upper}_VERBOSITY', c_dict['VERBOSITY']) ) diff --git a/parm/met_config/Ascii2NcConfig_wrapped b/parm/met_config/Ascii2NcConfig_wrapped index 6efa3e9675..4233450615 100644 --- a/parm/met_config/Ascii2NcConfig_wrapped +++ b/parm/met_config/Ascii2NcConfig_wrapped @@ -36,4 +36,6 @@ message_type_map = [ // //version = "V10.0"; +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/EnsembleStatConfig_wrapped b/parm/met_config/EnsembleStatConfig_wrapped index e9ef2d5667..6374340917 100644 --- a/parm/met_config/EnsembleStatConfig_wrapped +++ b/parm/met_config/EnsembleStatConfig_wrapped @@ -223,4 +223,6 @@ ${METPLUS_OUTPUT_PREFIX} //////////////////////////////////////////////////////////////////////////////// +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/GenEnsProdConfig_wrapped b/parm/met_config/GenEnsProdConfig_wrapped index df51805eba..59c794310a 100644 --- a/parm/met_config/GenEnsProdConfig_wrapped +++ b/parm/met_config/GenEnsProdConfig_wrapped @@ -103,4 +103,6 @@ ${METPLUS_ENSEMBLE_FLAG_DICT} //////////////////////////////////////////////////////////////////////////////// +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/GridDiagConfig_wrapped b/parm/met_config/GridDiagConfig_wrapped index 41592f2448..06b95662bf 100644 --- a/parm/met_config/GridDiagConfig_wrapped +++ b/parm/met_config/GridDiagConfig_wrapped @@ -33,4 +33,6 @@ ${METPLUS_DATA_DICT} ${METPLUS_MASK_DICT} +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/GridStatConfig_wrapped b/parm/met_config/GridStatConfig_wrapped index 9152297a18..fdaf8cf209 100644 --- a/parm/met_config/GridStatConfig_wrapped +++ b/parm/met_config/GridStatConfig_wrapped @@ -178,7 +178,9 @@ ${METPLUS_NC_PAIRS_FLAG_DICT} //grid_weight_flag = ${METPLUS_GRID_WEIGHT_FLAG} -tmp_dir = "/tmp"; + +tmp_dir = "${MET_TMP_DIR}"; + // output_prefix = ${METPLUS_OUTPUT_PREFIX} diff --git a/parm/met_config/IODA2NCConfig_wrapped b/parm/met_config/IODA2NCConfig_wrapped new file mode 100644 index 0000000000..ba1faa1695 --- /dev/null +++ b/parm/met_config/IODA2NCConfig_wrapped @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// IODA2NC configuration file. +// +// For additional information, please see the MET Users Guide. +// +//////////////////////////////////////////////////////////////////////////////// + +// +// IODA message type +// +// message_type = [ +${METPLUS_MESSAGE_TYPE} + +// +// Mapping of message type group name to comma-separated list of values +// Derive PRMSL only for SURFACE message types +// +// message_type_group_map = [ +${METPLUS_MESSAGE_TYPE_GROUP_MAP} + +// +// Mapping of input IODA message types to output message types +// +// message_type_map = [ +${METPLUS_MESSAGE_TYPE_MAP} + +// +// IODA station ID +// +// station_id = [ +${METPLUS_STATION_ID} + +//////////////////////////////////////////////////////////////////////////////// + +// +// Observation time window +// +// obs_window = { +${METPLUS_OBS_WINDOW_DICT} + +//////////////////////////////////////////////////////////////////////////////// + +// +// Observation retention regions +// +// mask = { +${METPLUS_MASK_DICT} + +//////////////////////////////////////////////////////////////////////////////// + +// +// Observing location elevation +// +// elevation_range = { +${METPLUS_ELEVATION_RANGE_DICT} + +//////////////////////////////////////////////////////////////////////////////// + +// +// Vertical levels to retain +// +// level_range = { +${METPLUS_LEVEL_RANGE_DICT} + +/////////////////////////////////////////////////////////////////////////////// + +// +// IODA variable names to retain or derive. +// Use obs_bufr_map to rename variables in the output. +// If empty or 'all', process all available variables. +// +// obs_var = [ +${METPLUS_OBS_VAR} + +//////////////////////////////////////////////////////////////////////////////// + +// +// Mapping of input IODA variable names to output variables names. +// The default IODA map, obs_var_map, is appended to this map. +// +// obs_name_map = [ +${METPLUS_OBS_NAME_MAP} + +// +// Default mapping for Metadata. +// +// metadata_map = [ +${METPLUS_METADATA_MAP} + +// missing_thresh = [ +${METPLUS_MISSING_THRESH} + +//////////////////////////////////////////////////////////////////////////////// + +// quality_mark_thresh = +${METPLUS_QUALITY_MARK_THRESH} + +//////////////////////////////////////////////////////////////////////////////// + +// +// Time periods for the summarization +// obs_var (string array) is added and works like grib_code (int array) +// when use_var_id is enabled and variable names are saved. +// +// time_summary = { +${METPLUS_TIME_SUMMARY_DICT} + +//////////////////////////////////////////////////////////////////////////////// + +tmp_dir = "${MET_TMP_DIR}"; + +//version = "V10.0"; + +//////////////////////////////////////////////////////////////////////////////// + +${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/MODEConfig_wrapped b/parm/met_config/MODEConfig_wrapped index a08a76164c..5f0442addc 100644 --- a/parm/met_config/MODEConfig_wrapped +++ b/parm/met_config/MODEConfig_wrapped @@ -226,6 +226,8 @@ shift_right = 0; // grid squares ${METPLUS_OUTPUT_PREFIX} //version = "V10.0"; +tmp_dir = "${MET_TMP_DIR}"; + //////////////////////////////////////////////////////////////////////////////// ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/MTDConfig_wrapped b/parm/met_config/MTDConfig_wrapped index f027e44369..f8310334a2 100644 --- a/parm/met_config/MTDConfig_wrapped +++ b/parm/met_config/MTDConfig_wrapped @@ -239,6 +239,8 @@ txt_output = { ${METPLUS_OUTPUT_PREFIX} //version = "V9.0"; +tmp_dir = "${MET_TMP_DIR}"; + //////////////////////////////////////////////////////////////////////////////// ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/PB2NCConfig_wrapped b/parm/met_config/PB2NCConfig_wrapped index 29d981e4a6..25cab0375b 100644 --- a/parm/met_config/PB2NCConfig_wrapped +++ b/parm/met_config/PB2NCConfig_wrapped @@ -131,7 +131,8 @@ ${METPLUS_TIME_SUMMARY_DICT} //////////////////////////////////////////////////////////////////////////////// -tmp_dir = "/tmp"; +tmp_dir = "${MET_TMP_DIR}"; + //version = "V9.0"; //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/PointStatConfig_wrapped b/parm/met_config/PointStatConfig_wrapped index 2a654d6d23..824aed7145 100644 --- a/parm/met_config/PointStatConfig_wrapped +++ b/parm/met_config/PointStatConfig_wrapped @@ -170,7 +170,8 @@ ${METPLUS_OUTPUT_FLAG_DICT} //////////////////////////////////////////////////////////////////////////////// -tmp_dir = "/tmp"; +tmp_dir = "${MET_TMP_DIR}"; + // output_prefix = ${METPLUS_OUTPUT_PREFIX} //version = "V10.0.0"; diff --git a/parm/met_config/STATAnalysisConfig_wrapped b/parm/met_config/STATAnalysisConfig_wrapped index 2fac673f6c..f317e2aa99 100644 --- a/parm/met_config/STATAnalysisConfig_wrapped +++ b/parm/met_config/STATAnalysisConfig_wrapped @@ -101,7 +101,9 @@ wmo_fisher_stats = [ "CNT:PR_CORR", "CNT:SP_CORR", ${METPLUS_HSS_EC_VALUE} rank_corr_flag = FALSE; vif_flag = FALSE; -tmp_dir = "/tmp"; + +tmp_dir = "${MET_TMP_DIR}"; + //version = "V10.0"; ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/SeriesAnalysisConfig_wrapped b/parm/met_config/SeriesAnalysisConfig_wrapped index dfa6f5a4c1..94fd1b2629 100644 --- a/parm/met_config/SeriesAnalysisConfig_wrapped +++ b/parm/met_config/SeriesAnalysisConfig_wrapped @@ -122,7 +122,9 @@ output_stats = { //hss_ec_value = ${METPLUS_HSS_EC_VALUE} rank_corr_flag = FALSE; -tmp_dir = "/tmp"; + +tmp_dir = "${MET_TMP_DIR}"; + //version = "V10.0"; //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/TCGenConfig_wrapped b/parm/met_config/TCGenConfig_wrapped index 65655c4699..c18f895f8b 100644 --- a/parm/met_config/TCGenConfig_wrapped +++ b/parm/met_config/TCGenConfig_wrapped @@ -292,4 +292,6 @@ ${METPLUS_NC_PAIRS_GRID} // //version = "V10.0.0"; +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/TCPairsConfig_wrapped b/parm/met_config/TCPairsConfig_wrapped index c780a3d486..ce13c1db82 100644 --- a/parm/met_config/TCPairsConfig_wrapped +++ b/parm/met_config/TCPairsConfig_wrapped @@ -145,4 +145,6 @@ watch_warn = { // //version = "V9.0"; +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/TCRMWConfig_wrapped b/parm/met_config/TCRMWConfig_wrapped index 3b5cb2d7dd..66dbc2cdf8 100644 --- a/parm/met_config/TCRMWConfig_wrapped +++ b/parm/met_config/TCRMWConfig_wrapped @@ -68,4 +68,6 @@ ${METPLUS_RMW_SCALE} //////////////////////////////////////////////////////////////////////////////// +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/met_config/TCStatConfig_wrapped b/parm/met_config/TCStatConfig_wrapped index 03911f0516..cef47ecc53 100644 --- a/parm/met_config/TCStatConfig_wrapped +++ b/parm/met_config/TCStatConfig_wrapped @@ -161,4 +161,6 @@ ${METPLUS_MATCH_POINTS} // ${METPLUS_JOBS} +tmp_dir = "${MET_TMP_DIR}"; + ${METPLUS_MET_CONFIG_OVERRIDES} diff --git a/parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf b/parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf new file mode 100644 index 0000000000..2d184ac189 --- /dev/null +++ b/parm/use_cases/met_tool_wrapper/IODA2NC/IODA2NC.conf @@ -0,0 +1,83 @@ +[config] + +PROCESS_LIST = IODA2NC + +### +# Time Info +### + +LOOP_BY = VALID +VALID_TIME_FMT = %Y%m%d%H +VALID_BEG = 2020031012 +VALID_END = 2020031012 +VALID_INCREMENT = 6H + +### +# File I/O Info +### + +IODA2NC_INPUT_DIR = {INPUT_BASE}/met_test/new/ioda +IODA2NC_INPUT_TEMPLATE = ioda.NC001007.{valid?fmt=%Y%m%d%H}.nc + +IODA2NC_OUTPUT_DIR = {OUTPUT_BASE}/ioda2nc +IODA2NC_OUTPUT_TEMPLATE = ioda.NC001007.{valid?fmt=%Y%m%d%H}.summary.nc + + +# OPTIONAL CONFIGURATIONS + +### +# ioda2nc command line arguments +### + +#IODA2NC_VALID_BEG = {valid?fmt=%Y%m%d_%H?shift=-24H} +#IODA2NC_VALID_END = {valid?fmt=%Y%m%d_%H} +#IODA2NC_NMSG = 10 + + +### +# ioda2nc configuration variables +### + +#IODA2NC_MESSAGE_TYPE = + +#IODA2NC_MESSAGE_TYPE_MAP = + +#IODA2NC_MESSAGE_TYPE_GROUP_MAP = + +#IODA2NC_STATION_ID = + +IODA2NC_OBS_WINDOW_BEG = -5400 +IODA2NC_OBS_WINDOW_END = 5400 + +#IODA2NC_MASK_GRID = +#IODA2NC_MASK_POLY = + +IODA2NC_ELEVATION_RANGE_BEG = -1000 +IODA2NC_ELEVATION_RANGE_END = 100000 + +#IODA2NC_LEVEL_RANGE_BEG = 1 +#IODA2NC_LEVEL_RANGE_END = 255 + +#IODA2NC_OBS_VAR = + +IODA2NC_OBS_NAME_MAP = + { key = "wind_direction"; val = "WDIR"; }, + { key = "wind_speed"; val = "WIND"; } + +#IODA2NC_METADATA_MAP = + +#IODA2NC_MISSING_THRESH = <=-1e9, >=1e9, ==-9999 + +IODA2NC_QUALITY_MARK_THRESH = 0 + +IODA2NC_TIME_SUMMARY_FLAG = True +IODA2NC_TIME_SUMMARY_RAW_DATA = True +IODA2NC_TIME_SUMMARY_BEG = 000000 +IODA2NC_TIME_SUMMARY_END = 235959 +IODA2NC_TIME_SUMMARY_STEP = 300 +IODA2NC_TIME_SUMMARY_WIDTH = 600 +IODA2NC_TIME_SUMMARY_GRIB_CODE = +IODA2NC_TIME_SUMMARY_OBS_VAR = "WIND" +IODA2NC_TIME_SUMMARY_TYPE = "min", "max", "range", "mean", "stdev", "median", "p80" +IODA2NC_TIME_SUMMARY_VLD_FREQ = 0 +IODA2NC_TIME_SUMMARY_VLD_THRESH = 0.0 From 1f79119cc3e2c0048fd7ed7158d5aa5e144b9a19 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 14:43:49 -0700 Subject: [PATCH 197/770] Update statistics_list.rst A little clean up and the rest of Fs --- docs/Users_Guide/statistics_list.rst | 70 ++++++++++++++-------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index fe329abe2b..6123dd5542 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -294,14 +294,14 @@ METplus Database of Statistics - Diagnostic Attr - MODE - MODE obj - * - The Continuous Ranked :raw-html:`
` + * - Continuous Ranked :raw-html:`
` Probability Score :raw-html:`
` (normal dist.) - CRPS - Ensemble - Ensemble-Stat - ECNT - * - The Continuous Ranked :raw-html:`
` + * - Continuous Ranked :raw-html:`
` Probability Score :raw-html:`
` (empirical dist.) - CRPS_EMP @@ -322,14 +322,14 @@ METplus Database of Statistics - Ensemble - Ensemble-Stat - ECNT - * - The Continuous Ranked :raw-html:`
` + * - Continuous Ranked :raw-html:`
` Probability Skill Score :raw-html:`
` (normal dist.) - CRPSS - Ensemble - Ensemble-Stat - ECNT - * - The Continuous Ranked :raw-html:`
` + * - Continuous Ranked :raw-html:`
` Probability Skill Score :raw-html:`
` (empirical dist.) - CRPSS_EMP @@ -502,8 +502,8 @@ METplus Database of Statistics - FAR - Categorical - Point-Stat :raw-html:`
` - MODE :raw-html:`
` - Grid-Stat + Grid-Stat :raw-html:`
` + MODE - CTS :raw-html:`
` MODE :raw-html:`
` NBRCTCS @@ -761,26 +761,26 @@ METplus Database of Statistics * - Mean of absolute value :raw-html:`
` of forecast gradients - FGBAR - - + - Continuous - Grid-Stat - GRAD * - Ratio of forecast and :raw-html:`
` observed gradients - FGOG_RATIO - - + - Continuous - Grid-Stat - GRAD * - Count of events in :raw-html:`
` forecast category i and :raw-html:`
` observation category j - Fi_Oj - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - MCTC * - Forecast mean - FMEAN - - + - Continuous - MODE :raw-html:`
` Grid-Stat :raw-html:`
` Point-Stat @@ -790,7 +790,7 @@ METplus Database of Statistics * - Number of forecast no :raw-html:`
` and observation no - FN_ON - - + - Categorical - MODE :raw-html:`
` Grid-Stat :raw-html:`
` Point-Stat @@ -800,7 +800,7 @@ METplus Database of Statistics * - Number of forecast no :raw-html:`
` and observation yes - FN_OY - - + - Categorical - MODE :raw-html:`
` Grid-Stat :raw-html:`
` Point-Stat @@ -809,14 +809,17 @@ METplus Database of Statistics CTC * - Attributes for pairs of :raw-html:`
` simple forecast and :raw-html:`
` - observation objects + observation objects - FNNN_ONNN - - + - Diagnostic Attr - MODE - MODE ascii object - * - Mean((f-c)*(o-c)) + * - Average product of :raw-html:`
` + forecast-climo and :raw-html:`
` + observation-climo :raw-html:`
` + / Mean(f-c)*(o-c) - FOABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - SAL1L2 @@ -824,7 +827,7 @@ METplus Database of Statistics forecast and observation :raw-html:`
` / Mean(f*o) - FOBAR - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat @@ -834,60 +837,57 @@ METplus Database of Statistics from observation to :raw-html:`
` forecast - FOM_FO - - + - Diagnostic Attr - Grid-Stat - DMAP * - Maximum of FOM_FO :raw-html:`
` and FOM_OF - FOM_MAX - - + - Diagnostic Attr - Grid-Stat - DMAP - * - Mean of FOM_FO and FOM_OF + * - Mean of FOM_FO :raw-html:`
` + and FOM_OF :raw-html:`
` - FOM_MEAN - - + - Diagnostic Attr - Grid-Stat - DMAP * - Minimum of FOM_FO and FOM_OF - FOM_MIN - - + - Diagnostic Attr - Grid-Stat - DMAP * - Pratt’s Figure of Merit :raw-html:`
` from forecast to :raw-html:`
` observation - FOM_OF - - + - Diagnostic Attr - Grid-Stat - DMAP * - Number of tied forecast :raw-html:`
` ranks used in computing :raw-html:`
` Kendall’s tau statistic - FRANK_TIES - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Root mean square forecast :raw-html:`
` wind speed - FS_RMS - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT * - Fractions Skill Score :raw-html:`
` - including bootstrap upper :raw-html:`
` - and lower confidence limits - FSS - - + - Neighborhood - Grid-Stat - NBRCNT * - Standard deviation of the :raw-html:`
` - error including normal :raw-html:`
` - upper and lower :raw-html:`
` - confidence limits + error - FSTDEV - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat @@ -896,13 +896,13 @@ METplus Database of Statistics VCNT * - Number of forecast events - FY - - + - Categorical - Grid-Stat - DMAP * - Number of forecast yes :raw-html:`
` and observation no - FY_ON - - + - Categorical - MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat @@ -912,7 +912,7 @@ METplus Database of Statistics * - Number of forecast yes :raw-html:`
` and observation yes - FY_OY - - + - Categorical - MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat From 0db466f111b551b625825f89f57cbcc4161339e6 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 15:32:07 -0700 Subject: [PATCH 198/770] Update statistics_list.rst G, H, I, Ks --- docs/Users_Guide/statistics_list.rst | 176 ++++++++------------------- 1 file changed, 49 insertions(+), 127 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6123dd5542..9b3d441f10 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -923,26 +923,26 @@ METplus Database of Statistics forecast and Best track :raw-html:`
` genesis events (km) - GEN_DIST - - + - Diagnostic Attr - TC-Gen - GENMPR * - Forecast minus Best track :raw-html:`
` genesis time in HHMMSS :raw-html:`
` format - GEN_TDIFF - - + - Diagnostic Attr - TC-Gen - GENMPR * - Gerrity Score and :raw-html:`
` bootstrap confidence limits - GER - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - MCTS * - Gilbert Skill Score - GSS - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` MODE @@ -951,191 +951,119 @@ METplus Database of Statistics MODE * - Hit rate - H_RATE - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - FHO * - Hausdorff Distance - HAUSDORFF - - + - Diagnostic Attr - Grid-Stat - DMAP * - Hanssen and Kuipers :raw-html:`
` Discriminant - HK - - + - Categorical - MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - - MODE :raw-html:`
` + - MODE cts :raw-html:`
` MCTS :raw-html:`
` CTS :raw-html:`
` NBRCTS * - Heidke Skill Score - HSS - - + - Categorical - MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - - MODE :raw-html:`
` + - MODE cts:raw-html:`
` MCTS :raw-html:`
` CTS :raw-html:`
` NBRCTS - * - Heidke Skill Score with :raw-html:`
` + * - Heidke Skill Score :raw-html:`
` user-specific expected :raw-html:`
` - correct and bootstrap :raw-html:`
` - confidence limits + correct - HSS_EC - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - MCTS - * - The Ignorance Score + * - Ignorance Score - IGN - - + - Ensemble - Ensemble-Stat - ECNT - * - Line number in ORANK file :raw-html:`
` - Index for the current :raw-html:`
` - matched pair - - INDEX - - - - Ensemble-Stat :raw-html:`
` - TC-Gen :raw-html:`
` - TC-Pairs :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - GENMPR :raw-html:`
` - TCMPR :raw-html:`
` - MPR * - Best track genesis minus :raw-html:`
` forecast initialization :raw-html:`
` time in HHMMSS format - INIT_TDIFF - - + - Diagnostic Attr - TC-Gen - GENMPR - * - Forecaster initials - - INITIALS - - - - TC-Pairs - - PROBRIRW :raw-html:`
` - TCMPR - * - User-specified percentile :raw-html:`
` - intensity in time slice :raw-html:`
` - / inside object - - INTENSITY_* - - - - MTD - - MTD 2D & 3D attribute output - * - 10th percentile intensity :raw-html:`
` - in time slice / intensity :raw-html:`
` - inside object - - INTENSITY_10 - - - - MTD - - MTD 2D & 3D attribute output * - 10th, 25th, 50th, 75th, :raw-html:`
` - and 90th percentiles :raw-html:`
` - of intensity of the raw :raw-html:`
` - field within the object + 90th, and user-specified :raw-html:`
` + percentiles of :raw-html:`
` + intensity of the raw :raw-html:`
` + field within the :raw-html:`
` + object or time slice - INTENSITY :raw-html:`
` _10, _25, :raw-html:`
` _50, _75, :raw-html:`
` - _90 - - - - MODE - - MODE ascii object - * - 25th percentile intensity :raw-html:`
` - in time slice / :raw-html:`
` - inside object - - INTENSITY_25 - - - - MTD - - MTD 2D & 3D attribute output - * - 60th percentile intensity :raw-html:`
` - in time slice / :raw-html:`
` - inside object - - INTENSITY_50 - - - - MTD - - MTD 2D & 3D attribute output - * - 75th percentile intensity :raw-html:`
` - in time slice / :raw-html:`
` - inside object - - INTENSITY_75 - - - - MTD - - MTD 2D & 3D attribute output - * - 90th percentile intensity :raw-html:`
` - in time slice / :raw-html:`
` - inside object - - INTENSITY_90 - - - - MTD - - MTD 2D & 3D attribute output - * - The percentile of :raw-html:`
` - intensity chosen for use :raw-html:`
` - in the PERCENTILE :raw-html:`
` - _INTENSITY_RATIO column - - INTENSITY - _NN - - + _90, _NN + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Sum of the intensities of :raw-html:`
` the raw field within the :raw-html:`
` object (variable units) - INTENSITY :raw-html:`
` _SUM - - + - Diagnostics Attr - MODE - MODE ascii object * - Total interest for this :raw-html:`
` object pair - INTEREST - - + - Diagnostic Attr - MTD :raw-html:`
` MODE - - MTD 3D pair attribute output :raw-html:`
` - MODE ascii object + - MTD 3D obj :raw-html:`
` + MODE obj * - Intersection area of two :raw-html:`
` objects (in grid squares) - - INTERSEC :raw-html:`
` - TION_AREA - - + - INTERSECT :raw-html:`
` + ION_AREA + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - Ratio of intersection area :raw-html:`
` to the lesser of the :raw-html:`
` forecast and observation :raw-html:`
` object areas (unitless) - - INTERSEC :raw-html:`
` - TION_OVER :raw-html:`
` + - INTERSECT :raw-html:`
` + ION_OVER :raw-html:`
` _AREA - - + - Diagnostic Attr - MODE - - MODE ascii object + - MODE obj * - “Volume” of object :raw-html:`
` intersection - - INTERSEC :raw-html:`
` - TION_VOLUME - - + - INTERSECT:raw-html:`
` + ION_VOLUME + - Diagnostic Attr - MTD - - MTD 3D pair attribute output - * - The Interquartile Range :raw-html:`
` - including bootstrap upper :raw-html:`
` - and lower confidence limits + - MTD 3D obj + * - Interquartile Range :raw-html:`
` - IQR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - The intensity scale :raw-html:`
` skill score - ISC - - + - - Wavelet-Stat - ISC * - The scale at which all :raw-html:`
` @@ -1147,27 +1075,21 @@ METplus Database of Statistics - ISC * - Kendall’s tau statistic - KT_CORR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Dimension of the latitude - - lat - - + - LAT + - Diagnostic Attr - MODE - - MODE netCDF dimensions & variables + - MODE obj * - Length of the :raw-html:`
` enclosing rectangle - LENGTH - - + - Diagnostic Attr - MODE - - MODE ascii object - * - Level of storm :raw-html:`
` - classification - - LEVEL - - - - TC-Pairs - - TCMPR + - MODE obj * - Likelihood when forecast :raw-html:`
` is between the ith and :raw-html:`
` i+1th probability :raw-html:`
` From 787f3ae3510573f01a6c993d994daf40bb903b33 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 16:00:13 -0700 Subject: [PATCH 199/770] Update statistics_list.rst L, M, Ns --- docs/Users_Guide/statistics_list.rst | 115 +++++++-------------------- 1 file changed, 28 insertions(+), 87 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 9b3d441f10..6ddf7d2911 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1096,32 +1096,32 @@ METplus Database of Statistics thresholds repeated - LIKELIHOOD :raw-html:`
` _i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PJC * - Logarithm of the Odds Ratio - LODDS - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` NBRCTS * - Dimension of the longitude - - lon + - LON - - MODE - - MODE netCDF dimensions & variables + - MODE netCDF * - The Median Absolute :raw-html:`
` Deviation - MAD - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Mean absolute error - MAE - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT :raw-html:`
` @@ -1130,7 +1130,7 @@ METplus Database of Statistics * - Magnitude & :raw-html:`
` Multiplicative bias - MBIAS - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat @@ -1138,56 +1138,54 @@ METplus Database of Statistics CNT * - The Mean Error - ME - - + - Continuous - Ensemble-Stat :raw-html:`
` - . :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - ECNT :raw-html:`
` SSVAR :raw-html:`
` - . :raw-html:`
` CNT * - The Mean Error of the :raw-html:`
` PERTURBED ensemble mean - ME_OERR - - + - Continuous - Ensemble-Stat - ECNT * - The square of the :raw-html:`
` mean error (bias) - ME2 - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Mean-error Distance from :raw-html:`
` observation to forecast - MED_FO - - + - Distance - Grid-Stat - DMAP * - Maximum of MED_FO :raw-html:`
` and MED_OF - MED_MAX - - + - Distance - Grid-Stat - DMAP * - Mean of MED_FO :raw-html:`
` and MED_OF - MED_MEAN - - + - Distance - Grid-Stat - DMAP * - Minimum of MED_FO :raw-html:`
` and MED_OF - MED_MIN - - + - Distance - Grid-Stat - DMAP * - Mean-error Distance from :raw-html:`
` forecast to observation - MED_OF - - + - Distance - Grid-Stat - DMAP * - Mean of maximum of :raw-html:`
` @@ -1200,7 +1198,7 @@ METplus Database of Statistics - GRAD * - Mean squared error - MSE - - + - Continuous - Ensemble-Stat :raw-html:`
` Wavelet-Stat :raw-html:`
` Point-Stat :raw-html:`
` @@ -1208,11 +1206,10 @@ METplus Database of Statistics - SSVAR :raw-html:`
` ISC :raw-html:`
` CNT :raw-html:`
` - . * - The mean squared error :raw-html:`
` skill - MSESS - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT @@ -1221,95 +1218,39 @@ METplus Database of Statistics between the forecast :raw-html:`
` and observed winds - MSVE - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Total number of :raw-html:`
` - probability intervals :raw-html:`
` - and current forecast run - - N_BIN - - - - Ensemble-Stat - - PHIST :raw-html:`
` - SSVAR * - Dimension of the :raw-html:`
` contingency table & the :raw-html:`
` total number of :raw-html:`
` categories in each :raw-html:`
` dimension - N_CAT - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - MCTC :raw-html:`
` MCTS * - Number of cluster objects - - n_clus - - + - N_CLUS + - Diagnostic Attr - MODE - - MODE netCDF variables - * - Number of ensemble :raw-html:`
` - values / members - - N_ENS - - - - Ensemble-Stat - - ECNT :raw-html:`
` - ORANK :raw-html:`
` - RELP - * - Number of valid :raw-html:`
` - ensemble values - - N_ENS_VLD - - - - Ensemble-Stat - - ORANK + - MODE obj * - Number of simple :raw-html:`
` forecast objects - - n_fcst_simp - - + - N_FCST_SIMP + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Number of simple :raw-html:`
` observation objects - - n_obs_simp - - + - N_OBS_SIMP + - Diagnostic Attr - MODE - MODE netCDF variables - * - Number of Cost/Loss :raw-html:`
` - ratios - - N_PNT - - - - Point-Stat :raw-html:`
` - Grid-Stat - - ECLV - * - Number of possible ranks :raw-html:`
` - for observation - - N_RANK - - - - Ensemble-Stat - - RHIST - * - Number of probability :raw-html:`
` - thresholds - - N_THRESH - - - - TC-Pairs :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat :raw-html:`
` - . :raw-html:`
` - . - - PROBRIRW :raw-html:`
` - PJC :raw-html:`
` - PRC :raw-html:`
` - PSTD output format :raw-html:`
` - PTC - * - Total number of scales :raw-html:`
` - used in decomposition - - NSCALE - - - - Wavelet-Stat - - ISC - * - NBRCNT output format :raw-html:`
` - & observation rate + * - Observation rate - O_RATE - - Point-Stat :raw-html:`
` From 4cd0a2cfac952ab100407f7f03a461e85ca1db89 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 16:34:42 -0700 Subject: [PATCH 200/770] Update statistics_list.rst A little clean-up and Os --- docs/Users_Guide/statistics_list.rst | 309 +++++---------------------- 1 file changed, 57 insertions(+), 252 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 6ddf7d2911..468f4a4c5d 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1049,7 +1049,7 @@ METplus Database of Statistics - MODE obj * - “Volume” of object :raw-html:`
` intersection - - INTERSECT:raw-html:`
` + - INTERSECT :raw-html:`
` ION_VOLUME - Diagnostic Attr - MTD @@ -1109,7 +1109,7 @@ METplus Database of Statistics NBRCTS * - Dimension of the longitude - LON - - + - Diagnostic Attr - MODE - MODE netCDF * - The Median Absolute :raw-html:`
` @@ -1252,29 +1252,26 @@ METplus Database of Statistics - MODE netCDF variables * - Observation rate - O_RATE - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - NBRCNT :raw-html:`
` FHO * - Mean observed wind speed - O_SPEED_BAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 - * - Mean(o-c) + * - Mean Observation Anomaly - OABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - SAL1L2 * - Average observed value :raw-html:`
` - observation mean :raw-html:`
` - Mean (o) :raw-html:`
` - & mean value - OBAR - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` . @@ -1282,50 +1279,18 @@ METplus Database of Statistics CNT :raw-html:`
` SL1L2 :raw-html:`
` VCNT - * - Mean observation normal :raw-html:`
` - upper and lower :raw-html:`
` - confidence limits - - OBAR_NCL - - - - Ensemble-Stat - - SSVAR * - Length (speed) of the :raw-html:`
` average observed wind :raw-html:`
` vector - OBAR_SPEED - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Object category - - OBJECT_CAT - - - - MODE :raw-html:`
` - MTD - - MODE ascii object :raw-html:`
` - MTD 2D & 3D attribute output :raw-html:`
` - MTD 3D pair attribute output - * - Object number - - OBJECT_ID - - - - MODE :raw-html:`
` - MTD - - MODE ascii object :raw-html:`
` - MTD 2D & 3D attribute output :raw-html:`
` - MTD 3D pair attribute output - * - Observation value - - OBS - - - - Ensemble-Stat :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - MPR :raw-html:`
` - . * - Number of observed :raw-html:`
` clusters - obs_clus - - + - Diagnostic Attr - MODE - MODE netCDF dimensions * - Number of points used to :raw-html:`
` @@ -1334,260 +1299,105 @@ METplus Database of Statistics objects - obs_clus :raw-html:`
` _hull - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Latitude - obs_clus :raw-html:`
` _hull_lat - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Longitude - obs_clus :raw-html:`
` _hull_lon - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Number of Observation :raw-html:`
` Cluster Convex Hull Points - obs_clus :raw-html:`
` _hull_npts - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Starting Index - obs_clus :raw-html:`
` _hull_start - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point X-Coordinate - obs_clus :raw-html:`
` _hull_x - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Y-Coordinate - obs_clus :raw-html:`
` _hull_y - - - - MODE - - MODE netCDF variables - * - Cluster observation object :raw-html:`
` - id number for each :raw-html:`
` - grid point - - obs_clus_id - - - - MODE - - MODE netCDF variables - * - Observation convolution :raw-html:`
` - threshold - - obs_conv :raw-html:`
` - _threshold - - - - MODE - - MODE netCDF variables - * - Observation convolution :raw-html:`
` - radius - - obs_conv :raw-html:`
` - _radius - - - - MODE - - MODE netCDF variables - * - Elevation of the :raw-html:`
` - observation - - OBS_ELV - - - - Ensemble-Stat :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - MPR :raw-html:`
` - . - * - Latitude of the :raw-html:`
` - observation - - OBS_LAT - - - - Ensemble-Stat :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - MPR :raw-html:`
` - . - * - Longitude of the :raw-html:`
` - observation - - OBS_LON - - - - Ensemble-Stat :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - MPR :raw-html:`
` . - * - Level of the observation - - OBS_LVL - - - - Ensemble-Stat :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - MPR :raw-html:`
` - . - * - Simple observation object :raw-html:`
` - id number for each :raw-html:`
` - grid point - - obs_obj_id - - - - MODE - - MODE netCDF variables - * - Observation Object Raw :raw-html:`
` - Values - - obs_obj_raw - - - - MODE - - MODE netCDF variables - * - Quality control flag for :raw-html:`
` - observation - - OBS_QC - - - - Point-Stat :raw-html:`
` - Grid-Stat - - MPR - * - Observation Raw Values - - obs_raw - - + - Diagnostic Attr - MODE - - MODE netCDF variables - * - Station Identifier - - OBS_SID - - - - Ensemble-Stat :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat - - ORANK :raw-html:`
` - MPR :raw-html:`
` . + - MODE obj * - Number of simple :raw-html:`
` observation objects - obs_simp - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE obj * - Number of points used :raw-html:`
` to define the boundaries :raw-html:`
` of the simple observation :raw-html:`
` objects - obs_simp :raw-html:`
` _bdy - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE obj * - Observation Simple :raw-html:`
` Boundary Point Latitude - obs_simp :raw-html:`
` _bdy_lat - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Observation Simple :raw-html:`
` Boundary Point Longitude - obs_simp :raw-html:`
` _bdy_lon - - - - MODE - - MODE netCDF variables - * - Observation Simple :raw-html:`
` - Boundary Starting Index - - obs_simp :raw-html:`
` - _bdy_start - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Number of Observation :raw-html:`
` Simple Boundary Points - obs_simp :raw-html:`
` _bdy_npts - - - - MODE - - MODE netCDF variables - * - Observation Simple Boundary :raw-html:`
` - Point X-Coordinate - - obs_simp :raw-html:`
` - _bdy_x - - - - MODE - - MODE netCDF variables - * - Observation Simple Boundary :raw-html:`
` - Point Y-Coordinate - - obs_simp :raw-html:`
` - _bdy_y - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Number of points used to :raw-html:`
` define the hull of the :raw-html:`
` simple observation objects - obs_simp :raw-html:`
` _hull - - - - MODE - - MODE netCDF dimensions - * - Observation Simple Convex :raw-html:`
` - Hull Point Latitude - - obs_simp :raw-html:`
` - _hull_lat - - - - MODE - - MODE netCDF variables - * - Observation Simple Convex :raw-html:`
` - Hull Point Longitude - - obs_simp :raw-html:`
` - _hull_lon - - + - Diagnostic Attr - MODE - - MODE netCDF variables + - MODE obj * - Number of Observation :raw-html:`
` Simple Convex Hull Points - obs_simp :raw-html:`
` _hull_npts - - - - MODE - - MODE netCDF variables - * - Observation Simple Convex :raw-html:`
` - Hull Starting Index - - obs_simp :raw-html:`
` - _hull_start - - - - MODE - - MODE netCDF variables - * - Observation Simple Convex :raw-html:`
` - Hull Point X-Coordinate - - obs_simp :raw-html:`
` - _hull_x - - - - MODE - - MODE netCDF variables - * - Observation Simple Convex :raw-html:`
` - Hull Point Y-Coordinate - - obs_simp :raw-html:`
` - _hull_y - - - - MODE - - MODE netCDF variables - * - Number of thresholds :raw-html:`
` - applied to the observations - - obs_thresh :raw-html:`
` - _length - - + - Diagnostic Attr - MODE - - MODE netCDF dimensions + - MODE obj * - Odds Ratio - ODDS - - + - Categorical - MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat @@ -1597,7 +1407,7 @@ METplus Database of Statistics * - Direction of the average :raw-html:`
` observed wind vector - ODIR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT @@ -1613,57 +1423,51 @@ METplus Database of Statistics - - Grid-Stat - GRAD - * - Number of observation no :raw-html:`
` + * - Number of observation :raw-html:`
` when forecast is between :raw-html:`
` the ith and i+1th :raw-html:`
` probability thresholds - ON_i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PTC - * - Number of observation no :raw-html:`
` + * - Number of observation :raw-html:`
` when forecast is between :raw-html:`
` the ith and i+1th :raw-html:`
` probability thresholds - ON_TP_i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PJC - * - Mean((o-c)²) + * - Mean Squared :raw-html:`
` + Observation Anomaly - OOABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - SAL1L2 * - Average of observation :raw-html:`
` - squared & Mean(o²) + squared - OOBAR - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - SSVAR :raw-html:`
` - SL1L2 :raw-html:`
` . - * - Operational methodology :raw-html:`
` - category (FYOY, FYON, :raw-html:`
` - FNOY, or DISCARD) - - OPS_CAT - - - - TC-Gen - - GENMPR + SL1L2 :raw-html:`
` * - Number of tied observation :raw-html:`
` ranks used in computing :raw-html:`
` Kendall’s tau statistic - ORANK_TIES - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Odds Ratio Skill Score - ORSS - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` @@ -1671,13 +1475,14 @@ METplus Database of Statistics * - Root mean square observed :raw-html:`
` wind speed - OS_RMS - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Standard deviation + * - Standard deviation :raw-html:`
` + of observations - OSTDEV - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat @@ -1687,7 +1492,7 @@ METplus Database of Statistics * - Number of observation :raw-html:`
` events - OY - - + - Categorical - Grid-Stat - DMAP * - Number of observation yes :raw-html:`
` @@ -1706,7 +1511,7 @@ METplus Database of Statistics as a proportion of the :raw-html:`
` total OY (repeated) - OY_TP_i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PJC From 2d879494645ca59fd15573ec5f437d895337726a Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 16:58:29 -0700 Subject: [PATCH 201/770] Update statistics_list.rst A little clean-up and Os --- docs/Users_Guide/statistics_list.rst | 46 +++++++++++----------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 468f4a4c5d..51384a8dc3 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -740,7 +740,7 @@ METplus Database of Statistics * - Forecast energy squared :raw-html:`
` for this scale - FENERGY - - Diagnostic Attr + - - Wavelet-Stat - ISC * - Mean Forecast Anomaly Squared @@ -761,13 +761,13 @@ METplus Database of Statistics * - Mean of absolute value :raw-html:`
` of forecast gradients - FGBAR - - Continuous + - - Grid-Stat - GRAD * - Ratio of forecast and :raw-html:`
` observed gradients - FGOG_RATIO - - Continuous + - - Grid-Stat - GRAD * - Count of events in :raw-html:`
` @@ -852,7 +852,8 @@ METplus Database of Statistics - Diagnostic Attr - Grid-Stat - DMAP - * - Minimum of FOM_FO and FOM_OF + * - Minimum of FOM_FO :raw-html:`
` + and FOM_OF - FOM_MIN - Diagnostic Attr - Grid-Stat @@ -1500,7 +1501,7 @@ METplus Database of Statistics the ith and i+1th :raw-html:`
` probability thresholds - OY_i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PTC @@ -1518,33 +1519,29 @@ METplus Database of Statistics * - Ratio of the nth percentile :raw-html:`
` (INTENSITY_NN column) of :raw-html:`
` intensity of the two :raw-html:`
` - objects defined as the :raw-html:`
` - lesser of the forecast :raw-html:`
` - intensity divided by the :raw-html:`
` - observation intensity or :raw-html:`
` - its reciprocal (unitless) + objects - PERCENTILE :raw-html:`
` _INTENSITY :raw-html:`
` _RATIO - - + - Diagnostic Attr - MODE - MODE ascii object * - Probability Integral :raw-html:`
` Transform - PIT - - + - Ensemble - Ensemble-Stat - ORANK * - Probability of false :raw-html:`
` detection - PODF - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS * - Probability of detecting no - PODN - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` MODE @@ -1554,7 +1551,7 @@ METplus Database of Statistics * - Probability of detecting :raw-html:`
` yes - PODY - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` MODE @@ -1566,14 +1563,14 @@ METplus Database of Statistics greater than the ith :raw-html:`
` probability thresholds - PODY_i - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - PRC * - Probability of false :raw-html:`
` detection - POFD - - + - Categorical - MODE :raw-html:`
` Grid-Stat - MODE :raw-html:`
` @@ -1583,35 +1580,28 @@ METplus Database of Statistics greater than the ith :raw-html:`
` probability thresholds - POFD_i - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - PRC * - Pearson correlation :raw-html:`
` coefficient - PR_CORR - - + - Continuous - Ensemble-Stat :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - SSVAR :raw-html:`
` CNT :raw-html:`
` - . - * - The ith probability :raw-html:`
` - value (repeated) - - PROB_i - - - - TC-Pairs - - PROBRIRW * - Rank of the observation - RANK - - + - Ensemble - Ensemble-Stat - ORANK * - Count of observations :raw-html:`
` with the i-th rank - RANK_i - - + - Ensemble - Ensemble-Stat - RHIST * - Number of ranks used in :raw-html:`
` From 5aa62359d0baaafc010817a06fea12fda12ae146 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 17:17:38 -0700 Subject: [PATCH 202/770] Update statistics_list.rst Rs --- docs/Users_Guide/statistics_list.rst | 75 ++++++++++------------------ 1 file changed, 26 insertions(+), 49 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 51384a8dc3..68f95a6f95 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1608,7 +1608,7 @@ METplus Database of Statistics computing Kendall’s tau :raw-html:`
` statistic - RANKS - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT @@ -1618,13 +1618,13 @@ METplus Database of Statistics thresholds (repeated) - REFINEMENT :raw-html:`
` _i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PJC * - Reliability - RELIABILITY - - + - Probaility - Point-Stat :raw-html:`
` Grid-Stat - PSTD output format @@ -1636,71 +1636,48 @@ METplus Database of Statistics 1/n is assigned to each :raw-html:`
` member. - RELP_i - - + - Ensemble - Ensemble-Stat - RELP * - Resolution - RESOLUTION - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PSTD output format - * - Start of RI time window :raw-html:`
` - in HH format - - RI_BEG - - - - TC-Pairs - - PROBRIRW - * - End of RI time window :raw-html:`
` - in HH format - - RI_END - - - - TC-Pairs - - PROBRIRW - * - Width of RI time window :raw-html:`
` - in HH format - - RI_WINDOW - - - - TC-Pairs - - PROBRIRW * - Root mean squared error - RMSE - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Ensemble-Stat :raw-html:`
` - . - CNT :raw-html:`
` - . :raw-html:`
` ECNT :raw-html:`
` SSVAR - * - The Root Mean Square Error :raw-html:`
` - of the PERTURBED ensemble :raw-html:`
` - mean (e.g. with :raw-html:`
` - Observation Error) + * - Root Mean Square Error :raw-html:`
` + of the PERTURBED :raw-html:`
` + ensemble mean - RMSE_OERR - - + - Continuous - Ensemble-Stat - ECNT * - Root mean squared forecast :raw-html:`
` - anomaly (f-c) + anomaly - RMSFA - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Root mean squared :raw-html:`
` - observation anomaly (o-c) :raw-html:`
` - including bootstrap upper :raw-html:`
` - & lower confidence limits + observation anomaly - RMSOA - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Square root of MSVE - RMSVE - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT @@ -1708,46 +1685,46 @@ METplus Database of Statistics operating characteristic :raw-html:`
` curve - ROC_AUC - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - - PSTD outpu format + - PSTD * - Mean of the Brier Scores :raw-html:`
` for each RPS threshold - RPS - - + - Ensemble - Ensemble-Stat - RPS * - Mean of the reliabilities :raw-html:`
` for each RPS threshold - RPS_REL - - + - Ensemble - Ensemble-Stat - - RPS Reliability + - RPS * - Mean of the resolutions :raw-html:`
` for each RPS threshold - RPS_RES - - + - Ensemble - Ensemble-Stat - - RPS Resolution + - RPS * - Mean of the uncertainties :raw-html:`
` for each RPS threshold - RPS_UNC - - + - Ensemble - Ensemble-Stat - - RPS Uncertainty + - RPS * - Ranked Probability Skill :raw-html:`
` Score relative to external :raw-html:`
` climatology - RPSS - - + - Ensemble - Ensemble-Stat - RPS * - Ranked Probability Skill :raw-html:`
` Score relative to sample :raw-html:`
` climatology - RPSS_SMPL - - + - Ensemble - Ensemble-Stat - RPS * - S1 score From 68683ff5ebe482a16f70ed4cc4e0878e9cbc6363 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 17:41:36 -0700 Subject: [PATCH 203/770] Update statistics_list.rst S and Ts --- docs/Users_Guide/statistics_list.rst | 118 +++++++-------------------- 1 file changed, 30 insertions(+), 88 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 68f95a6f95..e00bbb7ff1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1729,19 +1729,19 @@ METplus Database of Statistics - RPS * - S1 score - S1 - - + - Continuous - Grid-Stat - GRAD * - S1 score with respect to :raw-html:`
` observed gradient - S1_OG - - + - Continuous - Grid-Stat - GRAD * - Symmetric Extremal :raw-html:`
` Dependency Index - SEDI - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` @@ -1749,21 +1749,21 @@ METplus Database of Statistics * - Symmetric Extreme :raw-html:`
` Dependency Score - SEDS - - + - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` NBRCTS * - Scatter Index - SI - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT * - Spearman’s rank :raw-html:`
` correlation coefficient - SP_CORR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - CNT @@ -1773,148 +1773,90 @@ METplus Database of Statistics - SPACE :raw-html:`
` _CENTROID :raw-html:`
` _DIST - - + - Diagnostics Attr - MTD - - MTD 3D pair attribute output + - MTD 3D obs * - Absolute value of SPEED_ERR - SPEED :raw-html:`
` _ABSERR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT * - Difference in object speeds - SPEED_DELTA - - + - Diagnostics Attr - MTD - - MTD 3D pair attribute output + - MTD 3D obs * - Difference between the :raw-html:`
` length of the average :raw-html:`
` forecast wind vector and :raw-html:`
` the average observed wind :raw-html:`
` vector (in the sense F - O) - SPEED_ERR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - The square root or the :raw-html:`
` - spread (standard deviation) :raw-html:`
` - of the mean of the variance :raw-html:`
` - of the unperturbed ensemble :raw-html:`
` - member values at each :raw-html:`
` - observation location + * - Standard deviation :raw-html:`
` + of the mean of the :raw-html:`
` + UNPERTURBED ensemble - SPREAD - - + - Ensemble - Ensemble-Stat - ECNT :raw-html:`
` ORANK - * - The square root or the :raw-html:`
` - spread (standard deviation) :raw-html:`
` - of the mean of the variance :raw-html:`
` - of the PERTURBED ensemble :raw-html:`
` - member values at each :raw-html:`
` - observation location + * - Standard deviation :raw-html:`
` + of the mean of the :raw-html:`
` + PERTURBED ensemble - SPREAD_OERR - - + - Ensemble - Ensemble-Stat - ECNT :raw-html:`
` ORANK - * - The square root of the sum :raw-html:`
` + * - Standard Deviation :raw-html:`
` of unperturbed ensemble :raw-html:`
` variance and the :raw-html:`
` observation error variance - SPREAD_PLUS :raw-html:`
` _OERR - - + - Ensemble - Ensemble-Stat - ECNT :raw-html:`
` ORANK - * - Object start time - - START_TIME - - - - MTD - - MTD 3D attribute output * - Difference in object :raw-html:`
` starting time steps - START_TIME :raw-html:`
` _DELTA - - + - Diagnostic Attr - MTD - - MTD 3D pair attribute output + - MTD 3D obj * - Symmetric difference of :raw-html:`
` two objects :raw-html:`
` (in grid squares) - SYMMETRIC :raw-html:`
` _DIFF - - + - Diagnostics Attr - MODE - - MODE ascii object - * - The ith probability :raw-html:`
` - threshold value (repeated) - - THRESH_i - - - - TC-Pairs :raw-html:`
` - Point-Stat :raw-html:`
` - Grid-Stat :raw-html:`
` - . :raw-html:`
` - . - - PROBRIRW :raw-html:`
` - PJC :raw-html:`
` - PRC :raw-html:`
` - PSTD output format :raw-html:`
` - PTC - * - Last probability :raw-html:`
` - threshold value - - THRESH_n - - - - Point-Stat :raw-html:`
` - Grid-Stat - - PJC :raw-html:`
` - PRC :raw-html:`
` - PTC - * - The dimensions of the tile - - TILE_DIM - - - - Wavelet-Stat - - ISC - * - Horizontal coordinate of :raw-html:`
` - the lower left corner of :raw-html:`
` - the tile - - TILE_XLL - - - - Wavelet-Stat - - ISC - * - Vertical coordinate of :raw-html:`
` - the lower left corner :raw-html:`
` - of the tile - - TILE_YLL - - - - Wavelet-Stat - - ISC + - MODE obj * - Difference in t index of :raw-html:`
` object spacetime centroid - TIME :raw-html:`
` _CENTROID :raw-html:`
` _DELTA - - + - Diagnostic Attr - MTD - - MTD 3D pair attribute output - * - Time index of slice - - TIME_INDEX - - - - MTD - - MTD 2D attribute output + - MTD 3D obj * - Track error of adeck :raw-html:`
` relative to bdeck (nm) - TK_ERR - - + - Continuous - TC-Pairs - PROBRIRW * - Track error of adeck :raw-html:`
` relative to bdeck (nm) - TK_ERR - - + - Continuous - TC-Pairs - TCMPR * - Mean(uf-uc) From 858a137cb551871a3a097ae52283b44f80da437e Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 18:13:05 -0700 Subject: [PATCH 204/770] Update statistics_list.rst The rest of the list --- docs/Users_Guide/statistics_list.rst | 130 ++++++++++++++------------- 1 file changed, 68 insertions(+), 62 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index e00bbb7ff1..4340dbc269 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1859,114 +1859,126 @@ METplus Database of Statistics - Continuous - TC-Pairs - TCMPR - * - Mean(uf-uc) + * - Mean U-component :raw-html:`
` + Forecast Anomaly - UFABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 - * - Mean(uf) + * - Mean U-component - UFBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 * - Uniform Fractions Skill :raw-html:`
` - Score including bootstrap :raw-html:`
` - upper and lower :raw-html:`
` - confidence limits + Score - UFSS - - + - Neighborhood - Grid-Stat - NBRCNT - * - Uncertainty + * - Variability of :raw-html:`
` + Observations - UNCERTAINTY - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - - PSTD outpu format - * - Union area of two objects :raw-html:`
` + - PSTD + * - Union area of :raw-html:`
` + two objects :raw-html:`
` (in grid squares) - UNION_AREA - - + - Diagnostic Attr - MODE - - MODE ascii object - * - Mean(uo-uc) + - MODE obj + * - Mean U-component :raw-html:`
` + Observation Anomaly - UOABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 - * - Mean(uo) + * - Mean U-component :raw-html:`
` + Observation - UOBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 - * - Mean((uf-uc)²+(vf-vc)²) + * - Mean U-component :raw-html:`
` + Squared :raw-html:`
` + Forecast Anomaly :raw-html:`
` + plus Squared :raw-html:`
` + Observation :raw-html:`
` + Anomaly - UVFFABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 - * - Mean(uf²+vf²) + * - Mean U-component :raw-html:`
` + Squared :raw-html:`
` + Forecast :raw-html:`
` + plus Squared :raw-html:`
` + Observation - UVFFBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 * - Mean((uf-uc)*(uo-uc)+ :raw-html:`
` (vf-vc)*(vo-vc)) - UVFOABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 * - Mean(uf*uo+vf*vo) - UVFOBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 * - Mean((uo-uc)²+(vo-vc)²) - UVOOABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 * - Mean(uo²+vo²) - UVOOBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 * - Economic value of the :raw-html:`
` base rate - VALUE_BASER - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - ECLV * - Relative value for the :raw-html:`
` ith Cost/Loss ratio - VALUE_i - - + - Probability - Point-Stat :raw-html:`
` Grid-Stat - ECLV * - Maximum variance - VAR_MAX - - + - Ensemble - Ensemble-Stat - SSVAR * - Average variance - VAR_MEAN - - + - Ensemble - Ensemble-Stat - SSVAR * - Minimum variance - VAR_MIN - - + - Ensemble - Ensemble-Stat - SSVAR * - Direction of the vector :raw-html:`
` @@ -1974,7 +1986,7 @@ METplus Database of Statistics average forecast and :raw-html:`
` average wind vectors - VDIFF_DIR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT @@ -1984,31 +1996,31 @@ METplus Database of Statistics average observed wind :raw-html:`
` vectors - VDIFF_SPEED - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VCNT * - Mean(vf-vc) - VFABAR - - + - Continous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 * - Mean(vf) - VFBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 * - Mean(vo-vc) - VOABAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 * - Mean(vo) - VOBAR - - + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 @@ -2016,87 +2028,81 @@ METplus Database of Statistics number of 3D “cells” :raw-html:`
` in an object - VOLUME - - + - Diagnostic Attr - MTD - - MTD 3D attribute output + - MTD 3D obj * - Forecast object volume :raw-html:`
` divided by observation :raw-html:`
` object volume - VOLUME :raw-html:`
` _RATIO - - + - Diagnostic Attr - MTD - - MTD 3D pair attribute output - * - HU or TS watch or :raw-html:`
` - warning in effect - - WATCH_WARN - - - - TC-Pairs - - TCMPR + - MTD 3D obj * - Width of the enclosing :raw-html:`
` rectangle (in grid units) - WIDTH - - + - Diagnostic Attr - MODE - - MODE ascii object - * - x component of :raw-html:`
` + - MODE obj + * - X component of :raw-html:`
` object velocity - X_DOT - - + - Diagnostic Attr - MTD - - MTD 3D attribute output + - MTD 3D obj * - X component position :raw-html:`
` error (nm) - X_ERR - - + - Diagnostic - TC-Pairs - PROBRIRW * - X component position :raw-html:`
` error (nm) - X_ERR - - + - Diagnostic - TC-Pairs - TCMPR * - y component of :raw-html:`
` object velocity - Y_DOT - - + - Diagnostic - MTD - - MTD 3D attribute output + - MTD 3D obj * - Y component position :raw-html:`
` error (nm) - Y_ERR - - + - Diagnostic - TC-Pairs - PROBRIRW :raw-html:`
` TCMPR * - Zhu’s Measure from :raw-html:`
` observation to forecast - ZHU_FO - - + - Diagnostic - Grid-Stat - DMAP * - Maximum of ZHU_FO :raw-html:`
` and ZHU_OF - ZHU_MAX - - + - Diagnostic - Grid-Stat - DMAP * - Mean of ZHU_FO :raw-html:`
` and ZHU_OF - ZHU_MEAN - - + - Diagnostic - Grid-Stat - DMAP * - Minimum of ZHU_FO :raw-html:`
` and ZHU_OF - ZHU_MIN - - + - Diagnostic - Grid-Stat - DMAP * - Zhu’s Measure from :raw-html:`
` forecast to observation - ZHU_OF - - + - Diagnostic - Grid-Stat - DMAP From d74e9a30a48a017b357bd53956487ab52d9a6ba5 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 18:45:30 -0700 Subject: [PATCH 205/770] Update statistics_list.rst Removed Attr from Stat Type thru E --- docs/Users_Guide/statistics_list.rst | 60 +++++++++++++--------------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 4340dbc269..2c42fb00ca 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -49,7 +49,7 @@ METplus Database of Statistics * - Difference between the axis :raw-html:`
` angles of two objects (in degrees) - ANGLE_DIFF - - Diagnostic Attr + - Diagnostic - MODE - MODE * - Anomaly Correlation :raw-html:`
` @@ -73,7 +73,7 @@ METplus Database of Statistics - CNT * - Object area (in grid squares) - AREA - - Diagnostic Attr + - Diagnostic - MODE :raw-html:`
` MTD - MODE obj @@ -81,7 +81,7 @@ METplus Database of Statistics divided by the observation :raw-html:`
` object area (unitless) - AREA_RATIO - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Area of the object :raw-html:`
` @@ -89,7 +89,7 @@ METplus Database of Statistics definition threshold :raw-html:`
` criteria (in grid squares) - AREA_THRESH - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Absolute value of :raw-html:`
` @@ -98,20 +98,20 @@ METplus Database of Statistics ratios of two objects :raw-html:`
` (unitless) - ASPECT_DIFF - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Object axis angle :raw-html:`
` (in degrees) - AXIS_ANG - - Diagnostic Attr + - Diagnostic - MODE :raw-html:`
` MTD - MTD obj * - Difference in spatial :raw-html:`
` axis plane angles - AXIS_DIFF - - Diagnostic Attr + - Diagnostic - MTD - MTD obj * - Baddeley’s Delta Metric @@ -198,7 +198,7 @@ METplus Database of Statistics of the 3D object - CDIST :raw-html:`
` _TRAVELLED - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Distance between two :raw-html:`
` @@ -206,14 +206,14 @@ METplus Database of Statistics (in grid units) - CENTROID :raw-html:`
` _DIST - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Latitude of centroid :raw-html:`
` Location of the centroid - CENTROID :raw-html:`
` _LAT - - Diagnostic Attr + - Diagnostic - MTD :raw-html:`
` MODE - MTD 2D & 3D obj :raw-html:`
` @@ -222,20 +222,20 @@ METplus Database of Statistics Location of the centroid - CENTROID :raw-html:`
` _LON - - Diagnostic Attr + - Diagnostic - MTD :raw-html:`
` MODE - MTD 2D & 3D obj :raw-html:`
` MODE obj * - Time coordinate of centroid - CENTROID_T - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - X coordinate of centroid :raw-html:`
` Location of the centroid - CENTROID_X - - Diagnostic Attr + - Diagnostic - MTD :raw-html:`
` MODE - MTD 2D & 3D obj :raw-html:`
` @@ -243,7 +243,7 @@ METplus Database of Statistics * - Y coordinate of centroid :raw-html:`
` Location of the centroid - CENTROID_Y - - Diagnostic Attr + - Diagnostic - MTD :raw-html:`
` MODE - MTD 2D & 3D obj :raw-html:`
` @@ -272,7 +272,7 @@ METplus Database of Statistics by the area of the :raw-html:`
` complex hull (unitless) - COMPLEXITY - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Ratio of complexities of :raw-html:`
` @@ -283,7 +283,7 @@ METplus Database of Statistics its reciprocal (unitless) - COMPLEXITY :raw-html:`
` _RATIO - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Minimum distance between :raw-html:`
` @@ -291,7 +291,7 @@ METplus Database of Statistics objects (in grid units) - CONVEX_HULL :raw-html:`
` _DIST - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Continuous Ranked :raw-html:`
` @@ -354,27 +354,27 @@ METplus Database of Statistics MBRCTCS * - Radius of curvature - CURVATURE - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Ratio of the curvature - CURVATURE :raw-html:`
` _RATIO - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Center of curvature :raw-html:`
` (in grid coordinates) - CURVATURE :raw-html:`
` _X - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Center of curvature :raw-html:`
` (in grid coordinates) - CURVATURE :raw-html:`
` _Y - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Absolute value of :raw-html:`
` @@ -397,7 +397,7 @@ METplus Database of Statistics direction of movement - DIRECTION :raw-html:`
` _DIFF - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Difference in the :raw-html:`
` @@ -405,7 +405,7 @@ METplus Database of Statistics two objects - DURATION :raw-html:`
` _DIFF - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Expected correct rate :raw-html:`
` @@ -415,20 +415,14 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - MCTC - * - Extreme Dependency Index :raw-html:`
` - including normal and :raw-html:`
` - bootstrap upper and :raw-html:`
` - lower confidence limits + * - Extreme Dependency Index - EDI - Categorical - Point-Stat :raw-html:`
` Grid-Stat - CTS :raw-html:`
` NBRCTS - * - Extreme Dependency Score :raw-html:`
` - including normal and :raw-html:`
` - bootstrap upper and :raw-html:`
` - lower confidence limits + * - Extreme Dependency Score - EDS - Categorical - Point-Stat :raw-html:`
` @@ -444,14 +438,14 @@ METplus Database of Statistics - GRAD * - Object end time - END_TIME - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Difference in object :raw-html:`
` ending time steps - END_TIME :raw-html:`
` _DELTA - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D pair * - The unperturbed :raw-html:`
` From 18622ec3aff1db4c1c303a61e305e50b26e30ecf Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 19:08:01 -0700 Subject: [PATCH 206/770] Update statistics_list.rst Remove Attr from Statistics Type through Gs --- docs/Users_Guide/statistics_list.rst | 126 +++++++++++++-------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 2c42fb00ca..043c3b77a0 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -153,7 +153,7 @@ METplus Database of Statistics the boundaries of two objects - BOUNDARY :raw-html:`
` _DIST - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Brier Score @@ -451,7 +451,7 @@ METplus Database of Statistics * - The unperturbed :raw-html:`
` ensemble mean value - ENS_MEAN - - Diagnostic Fld + - Ensemble - Ensemble-Stat - ORANK * - The PERTURBED ensemble :raw-html:`
` @@ -459,7 +459,7 @@ METplus Database of Statistics Observation Error). - ENS_MEAN :raw-html:`
` _OERR - - Diagnostic Fld + - Ensemble - Ensemble-Stat - ORANK * - Standard deviation of :raw-html:`
` @@ -540,190 +540,190 @@ METplus Database of Statistics * - Number of forecast :raw-html:`
` clusters - fcst_clus - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of points used to :raw-html:`
` define the hull of all :raw-html:`
` of the cluster forecast :raw-html:`
` objects - fcst_clus :raw-html:`
` _hull - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Latitude - fcst_clus :raw-html:`
` _hull_lat - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Longitude - fcst_clus :raw-html:`
` _hull _lon - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of Forecast :raw-html:`
` Cluster Convex Hull Points - fcst_clus :raw-html:`
` _hull_npts - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Starting Index - fcst_clus :raw-html:`
` _hull_start - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point X-Coordinate - fcst_clus :raw-html:`
` _hull_x - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Y-Coordinate - fcst_clus :raw-html:`
` _hull_y - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Object Raw :raw-html:`
` Values - fcst_obj :raw-html:`
` _raw - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of simple :raw-html:`
` forecast objects - fcst_simp - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of points used :raw-html:`
` to define the boundaries :raw-html:`
` of all of the simple :raw-html:`
` forecast objects - fcst_simp :raw-html:`
` _bdy - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple :raw-html:`
` Boundary Latitude - fcst_simp :raw-html:`
` _bdy_lat - - Diagnostic Attr + - Diagnostic - MODE - MODE netCDF * - Forecast Simple :raw-html:`
` Boundary Longitude - fcst_simp :raw-html:`
` _bdy_lon - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of Forecast :raw-html:`
` Simple Boundary Points - fcst_simp :raw-html:`
` _bdy_npts - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple :raw-html:`
` Boundary Starting Index - fcst_simp :raw-html:`
` _bdy_start - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple :raw-html:`
` Boundary X-Coordinate - fcst_simp :raw-html:`
` _bdy_x - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple :raw-html:`
` Boundary Y-Coordinate - fcst_simp :raw-html:`
` _bdy_y - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of points used to :raw-html:`
` define the hull of all :raw-html:`
` of the simple forecast :raw-html:`
` objects - fcst_simp :raw-html:`
` _hull - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point Latitude - fcst_simp :raw-html:`
` _hull_lat - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point Longitude - fcst_simp :raw-html:`
` _hull_lon - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF variables + - MODE obj * - Number of Forecast :raw-html:`
` Simple Convex Hull Points - fcst_simp :raw-html:`
` _hull_npts - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Starting Index - fcst_simp :raw-html:`
` _hull_start - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point X-Coordinate - fcst_simp :raw-html:`
` _hull_x - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point Y-Coordinate - fcst_simp :raw-html:`
` _hull_y - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of thresholds :raw-html:`
` applied to the forecast - fcst :raw-html:`
` _thresh :raw-html:`
` _length - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Number of thresholds :raw-html:`
` applied to the forecast - fcst_thresh :raw-html:`
` _length - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Direction of the average :raw-html:`
` forecast wind vector - FDIR @@ -805,9 +805,9 @@ METplus Database of Statistics simple forecast and :raw-html:`
` observation objects - FNNN_ONNN - - Diagnostic Attr + - Categorical - MODE - - MODE ascii object + - MODE obj * - Average product of :raw-html:`
` forecast-climo and :raw-html:`
` observation-climo :raw-html:`
` @@ -831,32 +831,32 @@ METplus Database of Statistics from observation to :raw-html:`
` forecast - FOM_FO - - Diagnostic Attr + - Diagnostic - Grid-Stat - DMAP * - Maximum of FOM_FO :raw-html:`
` and FOM_OF - FOM_MAX - - Diagnostic Attr + - Diagnostic - Grid-Stat - DMAP * - Mean of FOM_FO :raw-html:`
` and FOM_OF :raw-html:`
` - FOM_MEAN - - Diagnostic Attr + - Diagnostic - Grid-Stat - DMAP * - Minimum of FOM_FO :raw-html:`
` and FOM_OF - FOM_MIN - - Diagnostic Attr + - Diagnostic - Grid-Stat - DMAP * - Pratt’s Figure of Merit :raw-html:`
` from forecast to :raw-html:`
` observation - FOM_OF - - Diagnostic Attr + - Diagnostic - Grid-Stat - DMAP * - Number of tied forecast :raw-html:`
` @@ -918,14 +918,14 @@ METplus Database of Statistics forecast and Best track :raw-html:`
` genesis events (km) - GEN_DIST - - Diagnostic Attr + - Diagnostic - TC-Gen - GENMPR * - Forecast minus Best track :raw-html:`
` genesis time in HHMMSS :raw-html:`
` format - GEN_TDIFF - - Diagnostic Attr + - Diagnostic - TC-Gen - GENMPR * - Gerrity Score and :raw-html:`
` From b3c42fdc1f2dc304fa6cceba71d308c7d718ecaa Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 19:27:17 -0700 Subject: [PATCH 207/770] Update statistics_list.rst Remove Attr from Statistic Type through Rs --- docs/Users_Guide/statistics_list.rst | 66 ++++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 043c3b77a0..8453bee82a 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -447,7 +447,7 @@ METplus Database of Statistics _DELTA - Diagnostic - MTD - - MTD 3D pair + - MTD 3D obj * - The unperturbed :raw-html:`
` ensemble mean value - ENS_MEAN @@ -993,7 +993,7 @@ METplus Database of Statistics forecast initialization :raw-html:`
` time in HHMMSS format - INIT_TDIFF - - Diagnostic Attr + - Diagnostic - TC-Gen - GENMPR * - 10th, 25th, 50th, 75th, :raw-html:`
` @@ -1014,13 +1014,13 @@ METplus Database of Statistics object (variable units) - INTENSITY :raw-html:`
` _SUM - - Diagnostics Attr + - Diagnostics - MODE - - MODE ascii object + - MODE obj * - Total interest for this :raw-html:`
` object pair - INTEREST - - Diagnostic Attr + - Diagnostic - MTD :raw-html:`
` MODE - MTD 3D obj :raw-html:`
` @@ -1029,7 +1029,7 @@ METplus Database of Statistics objects (in grid squares) - INTERSECT :raw-html:`
` ION_AREA - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Ratio of intersection area :raw-html:`
` @@ -1039,14 +1039,14 @@ METplus Database of Statistics - INTERSECT :raw-html:`
` ION_OVER :raw-html:`
` _AREA - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - “Volume” of object :raw-html:`
` intersection - INTERSECT :raw-html:`
` ION_VOLUME - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Interquartile Range :raw-html:`
` @@ -1076,13 +1076,13 @@ METplus Database of Statistics - CNT * - Dimension of the latitude - LAT - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Length of the :raw-html:`
` enclosing rectangle - LENGTH - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Likelihood when forecast :raw-html:`
` @@ -1104,9 +1104,9 @@ METplus Database of Statistics NBRCTS * - Dimension of the longitude - LON - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF + - MODE obj * - The Median Absolute :raw-html:`
` Deviation - MAD @@ -1230,19 +1230,19 @@ METplus Database of Statistics MCTS * - Number of cluster objects - N_CLUS - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of simple :raw-html:`
` forecast objects - N_FCST_SIMP - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of simple :raw-html:`
` observation objects - N_OBS_SIMP - - Diagnostic Attr + - Diagnostic - MODE - MODE netCDF variables * - Observation rate @@ -1285,64 +1285,64 @@ METplus Database of Statistics * - Number of observed :raw-html:`
` clusters - obs_clus - - Diagnostic Attr + - Diagnostic - MODE - - MODE netCDF dimensions + - MODE obj * - Number of points used to :raw-html:`
` define the hull of all of :raw-html:`
` the cluster observation :raw-html:`
` objects - obs_clus :raw-html:`
` _hull - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Latitude - obs_clus :raw-html:`
` _hull_lat - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Longitude - obs_clus :raw-html:`
` _hull_lon - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of Observation :raw-html:`
` Cluster Convex Hull Points - obs_clus :raw-html:`
` _hull_npts - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Starting Index - obs_clus :raw-html:`
` _hull_start - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point X-Coordinate - obs_clus :raw-html:`
` _hull_x - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Y-Coordinate - obs_clus :raw-html:`
` _hull_y - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of simple :raw-html:`
` observation objects - obs_simp - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of points used :raw-html:`
` @@ -1351,28 +1351,28 @@ METplus Database of Statistics objects - obs_simp :raw-html:`
` _bdy - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Simple :raw-html:`
` Boundary Point Latitude - obs_simp :raw-html:`
` _bdy_lat - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Observation Simple :raw-html:`
` Boundary Point Longitude - obs_simp :raw-html:`
` _bdy_lon - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of Observation :raw-html:`
` Simple Boundary Points - obs_simp :raw-html:`
` _bdy_npts - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of points used to :raw-html:`
` @@ -1380,14 +1380,14 @@ METplus Database of Statistics simple observation objects - obs_simp :raw-html:`
` _hull - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Number of Observation :raw-html:`
` Simple Convex Hull Points - obs_simp :raw-html:`
` _hull_npts - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Odds Ratio @@ -1517,9 +1517,9 @@ METplus Database of Statistics - PERCENTILE :raw-html:`
` _INTENSITY :raw-html:`
` _RATIO - - Diagnostic Attr + - Diagnostic - MODE - - MODE ascii object + - MODE obj * - Probability Integral :raw-html:`
` Transform - PIT From e0556f914cc61b5a26ce84ce219846966d0ddd30 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 19:39:44 -0700 Subject: [PATCH 208/770] Update statistics_list.rst Remove Attr from Stat Type to the end --- docs/Users_Guide/statistics_list.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8453bee82a..81fa1773de 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -952,7 +952,7 @@ METplus Database of Statistics - FHO * - Hausdorff Distance - HAUSDORFF - - Diagnostic Attr + - Diagnostic - Grid-Stat - DMAP * - Hanssen and Kuipers :raw-html:`
` @@ -1006,7 +1006,7 @@ METplus Database of Statistics _10, _25, :raw-html:`
` _50, _75, :raw-html:`
` _90, _NN - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Sum of the intensities of :raw-html:`
` @@ -1618,7 +1618,7 @@ METplus Database of Statistics - PJC * - Reliability - RELIABILITY - - Probaility + - Probability - Point-Stat :raw-html:`
` Grid-Stat - PSTD output format @@ -1767,7 +1767,7 @@ METplus Database of Statistics - SPACE :raw-html:`
` _CENTROID :raw-html:`
` _DIST - - Diagnostics Attr + - Diagnostics - MTD - MTD 3D obs * - Absolute value of SPEED_ERR @@ -1779,7 +1779,7 @@ METplus Database of Statistics - VCNT * - Difference in object speeds - SPEED_DELTA - - Diagnostics Attr + - Diagnostics - MTD - MTD 3D obs * - Difference between the :raw-html:`
` @@ -1822,7 +1822,7 @@ METplus Database of Statistics starting time steps - START_TIME :raw-html:`
` _DELTA - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Symmetric difference of :raw-html:`
` @@ -1830,7 +1830,7 @@ METplus Database of Statistics (in grid squares) - SYMMETRIC :raw-html:`
` _DIFF - - Diagnostics Attr + - Diagnostics - MODE - MODE obj * - Difference in t index of :raw-html:`
` @@ -1838,7 +1838,7 @@ METplus Database of Statistics - TIME :raw-html:`
` _CENTROID :raw-html:`
` _DELTA - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Track error of adeck :raw-html:`
` @@ -1883,7 +1883,7 @@ METplus Database of Statistics two objects :raw-html:`
` (in grid squares) - UNION_AREA - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - Mean U-component :raw-html:`
` From 71f70f8b0683877056150a508b70b62de8ac2777 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 19:52:07 -0700 Subject: [PATCH 209/770] Update statistics_list.rst Cleaned up some Line Type typos --- docs/Users_Guide/statistics_list.rst | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 81fa1773de..12926bdb1c 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -210,7 +210,6 @@ METplus Database of Statistics - MODE - MODE obj * - Latitude of centroid :raw-html:`
` - Location of the centroid - CENTROID :raw-html:`
` _LAT - Diagnostic @@ -219,7 +218,6 @@ METplus Database of Statistics - MTD 2D & 3D obj :raw-html:`
` MODE obj * - Longitude of centroid :raw-html:`
` - Location of the centroid - CENTROID :raw-html:`
` _LON - Diagnostic @@ -233,21 +231,19 @@ METplus Database of Statistics - MTD - MTD 3D obj * - X coordinate of centroid :raw-html:`
` - Location of the centroid - CENTROID_X - Diagnostic - MTD :raw-html:`
` MODE - MTD 2D & 3D obj :raw-html:`
` - MODE ascii object + MODE obj * - Y coordinate of centroid :raw-html:`
` - Location of the centroid - CENTROID_Y - Diagnostic - MTD :raw-html:`
` MODE - MTD 2D & 3D obj :raw-html:`
` - MODE ascii object + MODE obj * - Climatological mean value - CLIMO_MEAN - Continuous @@ -622,7 +618,7 @@ METplus Database of Statistics _bdy_lat - Diagnostic - MODE - - MODE netCDF + - MODE obj * - Forecast Simple :raw-html:`
` Boundary Longitude - fcst_simp :raw-html:`
` @@ -972,7 +968,7 @@ METplus Database of Statistics - MODE :raw-html:`
` Point-Stat :raw-html:`
` Grid-Stat - - MODE cts:raw-html:`
` + - MODE cts :raw-html:`
` MCTS :raw-html:`
` CTS :raw-html:`
` NBRCTS @@ -1244,7 +1240,7 @@ METplus Database of Statistics - N_OBS_SIMP - Diagnostic - MODE - - MODE netCDF variables + - MODE obj * - Observation rate - O_RATE - Categorical @@ -1621,7 +1617,7 @@ METplus Database of Statistics - Probability - Point-Stat :raw-html:`
` Grid-Stat - - PSTD output format + - PSTD * - Number of times the i-th :raw-html:`
` ensemble member’s value :raw-html:`
` was closest to the :raw-html:`
` @@ -1638,7 +1634,7 @@ METplus Database of Statistics - Probability - Point-Stat :raw-html:`
` Grid-Stat - - PSTD output format + - PSTD * - Root mean squared error - RMSE - Continuous From 6ff05223391e91f9a5eec78e7230fb8661281270 Mon Sep 17 00:00:00 2001 From: TaraJensen Date: Mon, 15 Nov 2021 19:55:48 -0700 Subject: [PATCH 210/770] Update statistics_list.rst Still more Attr cleanup --- docs/Users_Guide/statistics_list.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 12926bdb1c..856f073ee1 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -2018,7 +2018,7 @@ METplus Database of Statistics number of 3D “cells” :raw-html:`
` in an object - VOLUME - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Forecast object volume :raw-html:`
` @@ -2026,19 +2026,19 @@ METplus Database of Statistics object volume - VOLUME :raw-html:`
` _RATIO - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - Width of the enclosing :raw-html:`
` rectangle (in grid units) - WIDTH - - Diagnostic Attr + - Diagnostic - MODE - MODE obj * - X component of :raw-html:`
` object velocity - X_DOT - - Diagnostic Attr + - Diagnostic - MTD - MTD 3D obj * - X component position :raw-html:`
` From a68c61cda9c290ff8101ccd88da9187fd9cad1cd Mon Sep 17 00:00:00 2001 From: johnhg Date: Tue, 16 Nov 2021 11:24:47 -0700 Subject: [PATCH 211/770] Add default title for the new use case issue template. --- .github/ISSUE_TEMPLATE/new_use_case.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/new_use_case.md b/.github/ISSUE_TEMPLATE/new_use_case.md index ad54e6baf7..d16e19960f 100644 --- a/.github/ISSUE_TEMPLATE/new_use_case.md +++ b/.github/ISSUE_TEMPLATE/new_use_case.md @@ -1,7 +1,7 @@ --- name: New use case about: Add a new use case -title: '' +title: 'New Use Case:' labels: 'alert: NEED ACCOUNT KEY, alert: NEED MORE DEFINITION, alert: NEED PROJECT ASSIGNMENT, type: new use case' assignees: '' From 083b80dbe342b206d3b75b403e162eb68d68b8c5 Mon Sep 17 00:00:00 2001 From: Christina Kalb Date: Tue, 16 Nov 2021 12:16:50 -0700 Subject: [PATCH 212/770] Feature 1019 harmonic preprocessing (#1272) Co-authored-by: George McCabe <23407799+georgemccabe@users.noreply.github.com> --- .github/parm/use_case_groups.json | 7 +- docs/_static/s2s-OMI_GFS_phase_diagram.png | Bin 0 -> 55990 bytes .../s2s/UserScript_fcstGFS_obsERA_OMI.py | 20 +- .../s2s/UserScript_fcstGFS_obsERA_RMM.py | 263 ----------- .../s2s/UserScript_obsERA_obsOnly_OMI.py | 141 ++++++ ...UserScript_obsERA_obsOnly_PhaseDiagram.py} | 36 +- .../s2s/UserScript_obsERA_obsOnly_RMM.py | 147 ++++++ internal_tests/use_cases/all_use_cases.txt | 7 +- .../s2s/UserScript_fcstGFS_obsERA_OMI.conf | 78 +++- .../OMI_driver.py | 3 +- .../s2s/UserScript_fcstGFS_obsERA_RMM.conf | 214 --------- .../s2s/UserScript_obsERA_obsOnly_OMI.conf | 157 +++++++ .../OMI_driver.py | 1 + ...erScript_obsERA_obsOnly_PhaseDiagram.conf} | 8 +- .../PhaseDiagram_driver.py | 0 .../save_input_files_txt.py | 0 .../s2s/UserScript_obsERA_obsOnly_RMM.conf | 436 ++++++++++++++++++ .../RMM_driver.py | 28 +- .../compute_harmonic_anomalies.py | 110 +++++ 19 files changed, 1129 insertions(+), 527 deletions(-) create mode 100644 docs/_static/s2s-OMI_GFS_phase_diagram.png delete mode 100644 docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.py create mode 100644 docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.py rename docs/use_cases/model_applications/s2s/{UserScript_fcstGFS_obsERA_PhaseDiagram.py => UserScript_obsERA_obsOnly_PhaseDiagram.py} (82%) create mode 100644 docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.py delete mode 100644 parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf create mode 100644 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf create mode 120000 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py rename parm/use_cases/model_applications/s2s/{UserScript_fcstGFS_obsERA_PhaseDiagram.conf => UserScript_obsERA_obsOnly_PhaseDiagram.conf} (92%) rename parm/use_cases/model_applications/s2s/{UserScript_fcstGFS_obsERA_PhaseDiagram => UserScript_obsERA_obsOnly_PhaseDiagram}/PhaseDiagram_driver.py (100%) rename parm/use_cases/model_applications/s2s/{UserScript_fcstGFS_obsERA_PhaseDiagram => UserScript_obsERA_obsOnly_PhaseDiagram}/save_input_files_txt.py (100%) create mode 100644 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf rename parm/use_cases/model_applications/s2s/{UserScript_fcstGFS_obsERA_RMM => UserScript_obsERA_obsOnly_RMM}/RMM_driver.py (88%) create mode 100755 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 4cb2de818b..6c5f13f407 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -136,7 +136,7 @@ }, { "category": "s2s", - "index_list": "8-9", + "index_list": "7-9", "run": false }, { @@ -144,6 +144,11 @@ "index_list": "10", "run": false }, + { + "category": "s2s", + "index_list": "11", + "run": false + }, { "category": "space_weather", "index_list": "0-1", diff --git a/docs/_static/s2s-OMI_GFS_phase_diagram.png b/docs/_static/s2s-OMI_GFS_phase_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec1b3adb66614f7a9bb525374cacbf2c257c98c GIT binary patch literal 55990 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%0D1YG=!A1Si{2FwTbo$=dxB}l;;Ct~~ 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, -# `RegridDataPlaneUseCase `_, -# `PCPCombineUseCase `_ - diff --git a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.py b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.py deleted file mode 100644 index b6b618c66b..0000000000 --- a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.py +++ /dev/null @@ -1,263 +0,0 @@ -""" -UserScript: Make RMM plots from calculated MJO indices -=========================================================================== - -model_applications/ -s2s/ -UserScript_fcstGFS_obsERA_RMM.py - -""" - -############################################################################## -# Scientific Objective -# -------------------- -# -# To compute the Real-time Multivariate MJO Index (RMM) using Outgoing Longwave Radiation (OLR), 850 hPa wind (U850), and 200 hPa wind (U200). Specifically, RMM is computed using OLR, U850, andU200 data between 15N and 15S. Anomalies of OLR, U850, and U200 are then created, 120 day day mean removed, and the data are normalized by normalization factors (generally the square root of the average variance) The anomalies are projected onto Empirical Orthogonal Function (EOF) data. The OLR is then filtered for 20 - 96 days, and regressed onto the daily EOFs. Finally, it's normalized and these normalized components are plotted on a phase diagram and timeseries plot. -# - -############################################################################## -# Datasets -# -------- -# -# * Forecast dataset: GFS Model Outgoing Longwave Radiation -# * Observation dataset: ERA Reanlaysis Outgoing Longwave Radiation. - -############################################################################## -# External Dependencies -# --------------------- -# -# You will need to use a version of Python 3.6+ that has the following packages installed:: -# -# * numpy -# * netCDF4 -# * datetime -# * xarray -# * matplotlib -# * scipy -# * pandas -# -# If the version of Python used to compile MET did not have these libraries at the time of compilation, you will need to add these packages or create a new Python environment with these packages. -# -# If this is the case, you will need to set the MET_PYTHON_EXE environment variable to the path of the version of Python you want to use. If you want this version of Python to only apply to this use case, set it in the [user_env_vars] section of a METplus configuration file.: -# -# [user_env_vars] -# MET_PYTHON_EXE = /path/to/python/with/required/packages/bin/python -# - -############################################################################## -# METplus Components -# ------------------ -# -# This use case runs the OMI driver which computes OMI and creates a phase diagram. Inputs to the OMI driver include netCDF files that are in MET's netCDF version. In addition, a txt file containing the listing of these input netCDF files is required, as well as text file listings of the EOF1 and EOF2 files. Some optional pre-processing steps include using regrid_data_plane to either regrid your data or cut the domain t0 20N - 20S. -# - -############################################################################## -# METplus Workflow -# ---------------- -# -# The OMI driver script python code is run for each lead time on the forecast and observations data. This example loops by valid time for the model pre-processing, and valid time for the other steps. This version is set to only process the OMI calculation and creating a text file listing of the EOF files, omitting the regridding, and anomaly caluclation pre-processing steps. However, the configurations for pre-processing are available for user reference. - -############################################################################## -# METplus Configuration -# --------------------- -# -# METplus first loads all of the configuration files found in parm/metplus_config, -# then it loads any configuration files passed to METplus via the command line -# i.e. parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf. -# The file OMI_driver.py runs the python program and -# UserScript_fcstGFS_obsERA_OMI/UserScript_fcstGFS_obsERA_OMI.conf sets the -# variables for all steps of the OMI use case. -# -# .. highlight:: bash -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf - -############################################################################## -# MET Configuration -# --------------------- -# -# METplus sets environment variables based on the values in the METplus configuration file. -# These variables are referenced in the MET configuration file. **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** If there is a setting in the MET configuration file that is not controlled by an environment variable, you can add additional environment variables to be set only within the METplus environment using the [user_env_vars] section of the METplus configuration files. See the 'User Defined Config' section on the 'System Configuration' page of the METplus User's Guide for more information. -# -# - -############################################################################## -# Python Scripts -# ---------------- -# -# The OMI driver script orchestrates the calculation of the MJO indices and -# the generation of a phase diagram OMI plot: -# parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py: -# -# .. highlight:: python -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py -# - -############################################################################## -# Running METplus -# --------------- -# -# This use case is run in the following ways: -# -# 1) Passing in UserScript_fcstGFS_obsERA_OMI.conf then a user-specific system configuration file:: -# -# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf -c /path/to/user_system.conf -# -# 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_fcstGFS_obsERA_OMI.py:: -# -# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf -# -# The following variables must be set correctly: -# -# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases -# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions -# * **MET_INSTALL_DIR** - Path to location where MET is installed locally -# -# Example User Configuration File:: -# -# [dir] -# INPUT_BASE = /path/to/sample/input/data -# OUTPUT_BASE = /path/to/output/dir -# MET_INSTALL_DIR = /path/to/met-X.Y -# - -############################################################################## -# Expected Output -# --------------- -# -# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. Output for this use case will be found in model_applications/s2s/UserScript_fcstGFS_obsERA_OMI. This may include the regridded data and daily averaged files. In addition, the phase diagram plots will be generated and the output location can be specified as OMI_PLOT_OUTPUT_DIR. If it is not specified, plots will be sent to model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/plots (relative to **OUTPUT_BASE**). - -############################################################################## -# Keywords -# -------- -# -# sphinx_gallery_thumbnail_path = '_static/s2s-OMI_phase_diagram.png' -# -# .. note:: `XXXX`, `S2SAppUseCase `_ -# `RegridDataPlaneUseCase `_, -# `PCPCombineUseCase `_ - - -# -# - -############################################################################## -# Datasets -# -------- -# -# * Forecast dataset: GFS Model Outgoing Longwave Radiation, 850 hPa wind and 200 hPa wind -# * Observation dataset: ERA Reanlaysis Outgoing Longwave Radiation, 850 hPa wind and 200 hPa wind - -############################################################################## -# External Dependencies -# --------------------- -# -# You will need to use a version of Python 3.6+ that has the following packages installed:: -# -# * numpy -# * netCDF4 -# * datetime -# * xarray -# * matplotlib -# * scipy -# * pandas -# -# If the version of Python used to compile MET did not have these libraries at the time of compilation, you will need to add these packages or create a new Python environment with these packages. -# -# If this is the case, you will need to set the MET_PYTHON_EXE environment variable to the path of the version of Python you want to use. If you want this version of Python to only apply to this use case, set it in the [user_env_vars] section of a METplus configuration file.: -# -# [user_env_vars] -# MET_PYTHON_EXE = /path/to/python/with/required/packages/bin/python -# - -############################################################################## -# METplus Components -# ------------------ -# -# This use case runs the RMM driver which computes RMM and creates a phase diagram, time series, and EOF plot. Inputs to the RMM driver include netCDF files that are in MET's netCDF version. In addition, a text file containing the listing of these input netCDF files for OLR, u850 and u200 is required. Some optional pre-processing steps include using regrid_data_plane to either regrid your data or cut the domain t0 20N - 20S. -# - -############################################################################## -# METplus Workflow -# ---------------- -# The RMM driver script python code is run for each lead time on the forecast and observations data. This example loops by valid time for the model pre-processing, and valid time for the other steps. This version is set to only process the RMM calculation, omitting the regridding, and anomaly caluclation, and creation of the text file listing for OLR, u850, and u200 pre-processing steps. However, the configurations for pre-processing are available for user reference. -# - -############################################################################## -# METplus Configuration -# --------------------- -# -# METplus first loads all of the configuration files found in parm/metplus_config, -# then it loads any configuration files passed to METplus via the command line -# i.e. parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf. -# The file UserScript_fcstGFS_obsERA_RMM/RMM_driver.py runs the python program and -# UserScript_fcstGFS_obsERA_RMM.conf sets the variables for all steps of the RMM use case. -# -# .. highlight:: bash -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf - -############################################################################## -# MET Configuration -# --------------------- -# -# METplus sets environment variables based on the values in the METplus configuration file. -# These variables are referenced in the MET configuration file. **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** If there is a setting in the MET configuration file that is not controlled by an environment variable, you can add additional environment variables to be set only within the METplus environment using the [user_env_vars] section of the METplus configuration files. See the 'User Defined Config' section on the 'System Configuration' page of the METplus User's Guide for more information. -# -# - -############################################################################## -# Python Scripts -# ---------------- -# -# The RMM driver script orchestrates the calculation of the MJO indices and -# the generation of three RMM plots: -# parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/RMM_driver.py: -# -# .. highlight:: python -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/RMM_driver.py -# - -############################################################################## -# Running METplus -# --------------- -# -# This use case is run in the following ways: -# -# 1) Passing in UserScript_fcstGFS_obsERA_RMM.conf then a user-specific system configuration file:: -# -# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf -c /path/to/user_system.conf -# -# 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_fcstGFS_obsERA_RMM.py:: -# -# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf -# -# The following variables must be set correctly: -# -# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases -# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions -# * **MET_INSTALL_DIR** - Path to location where MET is installed locally -# -# Example User Configuration File:: -# -# [dir] -# INPUT_BASE = /path/to/sample/input/data -# OUTPUT_BASE = /path/to/output/dir -# MET_INSTALL_DIR = /path/to/met-X.Y -# - -############################################################################## -# Expected Output -# --------------- -# -# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. Output for this use case will be found in model_applications/s2s/UserScript_fcstGFS_obsERA_RMM. This may include the regridded data and daily averaged files. In addition, three plots will be generated, a phase diagram, time series, and EOF plot, and the output location can be specified as RMM_PLOT_OUTPUT_DIR. If it is not specified, plots will be sent to model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/plots (relative to **OUTPUT_BASE**). -# - -############################################################################## -# Keywords -# -------- -# -# sphinx_gallery_thumbnail_path = '_static/s2s-RMM_time_series.png' -# -# .. note:: `XXXX`, `S2SAppUseCase `_, -# `NetCDFFileUseCase ` -# `RegridDataPlaneUseCase `_, -# `PCPCombineUseCase `__ diff --git a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.py b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.py new file mode 100644 index 0000000000..90450b8134 --- /dev/null +++ b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.py @@ -0,0 +1,141 @@ +""" +UserScript: Make OMI plot from calculated MJO indices +=========================================================================== + +model_applications/ +s2s/ +UserScript_obsERA_obsOnly_OMI.py + +""" + +############################################################################## +# Scientific Objective +# -------------------- +# +# To use Outgoing Longwave Radiation (OLR) to compute the OLR based MJO Index (OMI). Specifically, OMI is computed using OLR data between 20N and 20S. The OLR data are then projected onto Empirical Orthogonal Function (EOF) data that is computed for each day of the year, latitude, and longitude. The OLR is then filtered for 20 - 96 days, and regressed onto the daily EOFs. Finally, it's normalized and these normalized components are plotted on a phase diagram. +# + +############################################################################## +# Datasets +# -------- +# +# * Forecast dataset: None +# * Observation dataset: ERA Reanlaysis Outgoing Longwave Radiation. + +############################################################################## +# External Dependencies +# --------------------- +# +# You will need to use a version of Python 3.6+ that has the following packages installed:: +# +# * numpy +# * netCDF4 +# * datetime +# * xarray +# * matplotlib +# * scipy +# * pandas +# +# If the version of Python used to compile MET did not have these libraries at the time of compilation, you will need to add these packages or create a new Python environment with these packages. +# +# If this is the case, you will need to set the MET_PYTHON_EXE environment variable to the path of the version of Python you want to use. If you want this version of Python to only apply to this use case, set it in the [user_env_vars] section of a METplus configuration file.: +# +# [user_env_vars] +# MET_PYTHON_EXE = /path/to/python/with/required/packages/bin/python +# + +############################################################################## +# METplus Components +# ------------------ +# +# This use case runs the OMI driver which computes OMI and creates a phase diagram. Inputs to the OMI driver include netCDF files that are in MET's netCDF version. In addition, a txt file containing the listing of these input netCDF files is required, as well as text file listings of the EOF1 and EOF2 files. These text files can be generated using the USER_SCRIPT_INPUT_TEMPLATES in the [create_eof_filelist] and [script_omi] sections. Some optional pre-processing steps include using regrid_data_plane to either regrid your data or cut the domain to 20N - 20S. +# + +############################################################################## +# METplus Workflow +# ---------------- +# +# The OMI driver script python code is run for each lead time on the forecast and observations data. This example loops by valid time for the model pre-processing, and valid time for the other steps. This version is set to only process the OMI calculation and creating a text file listing of the EOF files, omitting the creation of daily means for the model and the regridding pre-processing steps. However, the configurations for pre-processing are available for user reference. + +############################################################################## +# METplus Configuration +# --------------------- +# +# METplus first loads all of the configuration files found in parm/metplus_config, +# then it loads any configuration files passed to METplus via the command line +# i.e. parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf. +# The file UserScript_obsERA_obsOnly_OMI/OMI_driver.py runs the python program and +# UserScript_fcstGFS_obsERA_OMI.conf sets the variables for all steps of the OMI use case. +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf + +############################################################################## +# MET Configuration +# --------------------- +# +# METplus sets environment variables based on the values in the METplus configuration file. +# These variables are referenced in the MET configuration file. **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** If there is a setting in the MET configuration file that is not controlled by an environment variable, you can add additional environment variables to be set only within the METplus environment using the [user_env_vars] section of the METplus configuration files. See the 'User Defined Config' section on the 'System Configuration' page of the METplus User's Guide for more information. +# +# + +############################################################################## +# Python Scripts +# ---------------- +# +# The OMI driver script orchestrates the calculation of the MJO indices and +# the generation of a phase diagram OMI plot: +# parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py: +# +# .. highlight:: python +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py +# + +############################################################################## +# Running METplus +# --------------- +# +# This use case is run in the following ways: +# +# 1) Passing in UserScript_obsERA_obsOnly_OMI.conf then a user-specific system configuration file:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf -c /path/to/user_system.conf +# +# 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_obsERA_obsOnly_OMI.py:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf +# +# The following variables must be set correctly: +# +# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases +# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions +# * **MET_INSTALL_DIR** - Path to location where MET is installed locally +# +# Example User Configuration File:: +# +# [dir] +# INPUT_BASE = /path/to/sample/input/data +# OUTPUT_BASE = /path/to/output/dir +# MET_INSTALL_DIR = /path/to/met-X.Y +# + +############################################################################## +# Expected Output +# --------------- +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. Output for this use case will be found in model_applications/s2s/UserScript_obsERA_obsOnly_OMI. This may include the regridded data and daily averaged files. In addition, the phase diagram plots will be generated and the output location can be specified as OMI_PLOT_OUTPUT_DIR. If it is not specified, plots will be sent to model_applications/s2s/UserScript_obsERA_obsOnly_OMI/plots (relative to **OUTPUT_BASE**). + +############################################################################## +# Keywords +# -------- +# +# .. note:: +# +# * S2SAppUseCase +# * RegridDataPlaneUseCase +# * PCPCombineUseCase +# +# Navigate to :ref:`quick-search` to discover other similar use cases. +# +# sphinx_gallery_thumbnail_path = '_static/s2s-OMI_phase_diagram.png' +# \ No newline at end of file diff --git a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.py b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.py similarity index 82% rename from docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.py rename to docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.py index 9a9e2996e3..3266d628dd 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.py +++ b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.py @@ -4,7 +4,7 @@ model_applications/ s2s/ -UserScript_fcstGFS_obsERA_PhaseDiagram.py +UserScript_obsERA_obsOnly_PhaseDiagram.py """ @@ -63,13 +63,13 @@ # # METplus first loads all of the configuration files found in parm/metplus_config, # then it loads any configuration files passed to METplus via the command line -# i.e. parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf. -# The file UserScript_fcstGFS_obsERA_PhaseDiagram/PhaseDiagram_driver.py runs the python -# program and UserScript_fcstGFS_obsERA_PhaseDiagram.conf sets the variables for all steps +# i.e. parm/use_cases/model_applications/s2s/UserScript_obsERA_obsERA_OMI.conf. +# The file UserScript_obsERA_obsOnly_PhaseDiagram/PhaseDiagram_driver.py runs the python +# program and UserScript_obsERA_obsOnly_PhaseDiagram.conf sets the variables for all steps # of the use case. # # .. highlight:: bash -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf ############################################################################## # MET Configuration @@ -84,11 +84,11 @@ # ---------------- # # The phase diagram driver script orchestrates the generation of a phase diagram plot: -# parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/PhaseDiagram_driver.py: +# parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/PhaseDiagram_driver.py: # # .. highlight:: python -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/PhaseDiagram_driver.py -# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/save_input_files_txt.py +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/PhaseDiagram_driver.py +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/save_input_files_txt.py # ############################################################################## @@ -97,13 +97,13 @@ # # This use case is run in the following ways: # -# 1) Passing in UserScript_fcstGFS_obsERA_OMI.conf then a user-specific system configuration file:: +# 1) Passing in UserScript_obsERA_obsOnly_PhaseDiagram.conf then a user-specific system configuration file:: # -# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf -c /path/to/user_system.conf # -# 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_fcstGFS_obsERA_PhaseDiagram.py:: +# 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_obsERA_obsOnly_PhaseDiagram.py:: # -# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf # # The following variables must be set correctly: # @@ -123,12 +123,18 @@ # Expected Output # --------------- # -# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. Output for this use case will be found in model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram. This may include the regridded data and daily averaged files. In addition, the phase diagram plots will be generated and the output location can be specified as OMI_PLOT_OUTPUT_DIR. If it is not specified, plots will be sent to model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/plots (relative to **OUTPUT_BASE**). +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. Output for this use case will be found in model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram. This may include the regridded data and daily averaged files. In addition, the phase diagram plots will be generated and the output location can be specified as PHASE_DIAGRAM_PLOT_OUTPUT_DIR. If it is not specified, plots will be sent to model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/plots (relative to **OUTPUT_BASE**). ############################################################################## # Keywords # -------- # -# sphinx_gallery_thumbnail_path = '_static/s2s-PhaseDiagram.png' # -# .. note:: `XXXX`, `S2SAppUseCase `_ +# .. note:: +# +# * S2SAppUseCase +# +# Navigate to :ref:`quick-search` to discover other similar use cases. +# +# sphinx_gallery_thumbnail_path = '_static/s2s-PhaseDiagram.png' +# \ No newline at end of file diff --git a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.py b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.py new file mode 100644 index 0000000000..6c4f3e5c6c --- /dev/null +++ b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.py @@ -0,0 +1,147 @@ +""" +UserScript: Make RMM plots from calculated MJO indices +=========================================================================== + +model_applications/ +s2s/ +UserScript_obsERA_obsOnly_RMM.py + +""" + +############################################################################## +# Scientific Objective +# -------------------- +# +# To compute the Real-time Multivariate MJO Index (RMM) using Outgoing Longwave Radiation (OLR), 850 hPa wind (U850), and 200 hPa wind (U200). Specifically, RMM is computed using OLR, U850, and U200 data between 15N and 15S. Anomalies of OLR, U850, and U200 are created using a harmonic analysis, 120 day day mean removed, and the data are normalized by normalization factors (generally the square root of the average variance) The anomalies are projected onto Empirical Orthogonal Function (EOF) data. The OLR is then filtered for 20 - 96 days, and regressed onto the daily EOFs. Finally, it's normalized and these normalized components are plotted on a phase diagram and timeseries plot. +# + +############################################################################## +# Datasets +# -------- +# +# * Forecast dataset: None +# * Observation dataset: ERA Reanlaysis Outgoing Longwave Radiation, 850 hPa wind and 200 hPa wind + +############################################################################## +# External Dependencies +# --------------------- +# +# You will need to use a version of Python 3.6+ that has the following packages installed:: +# +# * numpy +# * netCDF4 +# * datetime +# * xarray +# * matplotlib +# * scipy +# * pandas +# +# If the version of Python used to compile MET did not have these libraries at the time of compilation, you will need to add these packages or create a new Python environment with these packages. +# +# If this is the case, you will need to set the MET_PYTHON_EXE environment variable to the path of the version of Python you want to use. If you want this version of Python to only apply to this use case, set it in the [user_env_vars] section of a METplus configuration file.: +# +# [user_env_vars] +# MET_PYTHON_EXE = /path/to/python/with/required/packages/bin/python +# + +############################################################################## +# METplus Components +# ------------------ +# +# This use case runs the RMM driver which computes first computes anomalies of outgoing longwave raidation, 850 hPa wind and 200 hPa wind. Then, it regrids the data to 15S to 15N. Next, RMM is computed and a phase diagram, time series, and EOF plot are created. Inputs to the RMM driver include netCDF files that are in MET's netCDF version. In addition, a text file containing the listing of these input netCDF files for OLR, u850 and u200 is required. Some optional pre-processing steps include using pcp_combine to compute daily means and the mean daily annual cycle for the data. +# + +############################################################################## +# METplus Workflow +# ---------------- +# The RMM driver script python code is run for each lead time on the forecast and observations data. This example loops by valid time for the model pre-processing, and valid time for the other steps. This version is set to only process the creation of anomalies, regridding, and RMM calculation, omitting the caluclation of daily means and the mean daily annucal cycle pre-processing steps. However, the configurations for pre-processing are available for user reference. +# + +############################################################################## +# METplus Configuration +# --------------------- +# +# METplus first loads all of the configuration files found in parm/metplus_config, +# then it loads any configuration files passed to METplus via the command line +# i.e. parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf. +# The file UserScript_obsERA_obsOnly_RMM/RMM_driver.py runs the python program and +# UserScript_obsERA_obsOnly_RMM.conf sets the variables for all steps of the RMM use case. +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf + +############################################################################## +# MET Configuration +# --------------------- +# +# METplus sets environment variables based on the values in the METplus configuration file. +# These variables are referenced in the MET configuration file. **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** If there is a setting in the MET configuration file that is not controlled by an environment variable, you can add additional environment variables to be set only within the METplus environment using the [user_env_vars] section of the METplus configuration files. See the 'User Defined Config' section on the 'System Configuration' page of the METplus User's Guide for more information. +# +# + +############################################################################## +# Python Scripts +# ---------------- +# +# The RMM driver script orchestrates the calculation of the MJO indices and +# the generation of three RMM plots: +# parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/RMM_driver.py: +# The harmonic anomalies script creates anomalies of input data using a harmonic analysis: +# parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py +# +# .. highlight:: python +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/RMM_driver.py +# .. literalinclude:: ../../../../parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py +# + +############################################################################## +# Running METplus +# --------------- +# +# This use case is run in the following ways: +# +# 1) Passing in UserScript_obsERA_obsOnly_RMM.conf then a user-specific system configuration file:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf -c /path/to/user_system.conf +# +# 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_obsERA_obsOnly_RMM.py:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf +# +# The following variables must be set correctly: +# +# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases +# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions +# * **MET_INSTALL_DIR** - Path to location where MET is installed locally +# +# Example User Configuration File:: +# +# [dir] +# INPUT_BASE = /path/to/sample/input/data +# OUTPUT_BASE = /path/to/output/dir +# MET_INSTALL_DIR = /path/to/met-X.Y +# + +############################################################################## +# Expected Output +# --------------- +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. Output for this use case will be found in model_applications/s2s/UserScript_obsERA_obsOnly_RMM. This may include the regridded data and daily averaged files. In addition, three plots will be generated, a phase diagram, time series, and EOF plot, and the output location can be specified as RMM_PLOT_OUTPUT_DIR. If it is not specified, plots will be sent to model_applications/s2s/UserScript_obsERA_obsOnly_RMM/plots (relative to **OUTPUT_BASE**). +# + +############################################################################## +# Keywords +# -------- +# +# +# .. note:: +# +# * S2SAppUseCase +# * NetCDFFileUseCase +# * RegridDataPlaneUseCase +# * PCPCombineUseCase +# +# Navigate to :ref:`quick-search` to discover other similar use cases. +# +# sphinx_gallery_thumbnail_path = '_static/s2s-RMM_time_series.png' +# \ No newline at end of file diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index 2d196b9b67..b6cd0a3af5 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -123,10 +123,11 @@ Category: s2s 4::TCGen_fcstGFSO_obsBDECKS_GDF_TDF:: model_applications/s2s/TCGen_fcstGFSO_obsBDECKS_GDF_TDF.conf:: metplotpy_env,cartopy,metplus 5::UserScript_obsPrecip_obsOnly_Hovmoeller:: model_applications/s2s/UserScript_obsPrecip_obsOnly_Hovmoeller.conf:: metplotpy_env,cartopy 6:: UserScript_obsPrecip_obsOnly_CrossSpectraPlot:: model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf:: spacetime_env -7:: UserScript_fcstGFS_obsERA_PhaseDiagram:: model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf:: spacetime_env +7:: UserScript_obsERA_obsOnly_PhaseDiagram:: model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf:: spacetime_env 8:: UserScript_fcstGFS_obsERA_OMI:: model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf:: spacetime_env, metdatadb -9:: UserScript_fcstGFS_obsERA_RMM:: model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf:: spacetime_env, metdatadb -10::UserScript_fcstGFS_obsERA_WeatherRegime:: model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.conf:: weatherregime_env,cartopy,metplus +9:: UserScript_obsERA_obsOnly_OMI:: model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf:: spacetime_env, metdatadb +10:: UserScript_obsERA_obsOnly_RMM:: model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf:: spacetime_env, metdatadb +11:: UserScript_fcstGFS_obsERA_WeatherRegime:: model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.conf:: weatherregime_env,cartopy,metplus Category: space_weather 0::GridStat_fcstGloTEC_obsGloTEC_vx7:: model_applications/space_weather/GridStat_fcstGloTEC_obsGloTEC_vx7.conf diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf b/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf index 628ff33271..8ca46eba79 100644 --- a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf +++ b/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI.conf @@ -1,7 +1,7 @@ # OMI UserScript wrapper [config] # All steps, including pre-processing: -#PROCESS_LIST = RegridDataPlane(regrid_obs_olr), UserScript(create_eof_filelist), UserScript(script_omi) +#PROCESS_LIST = PcpCombine(daily_mean_fcst), RegridDataPlane(regrid_obs_olr), RegridDataPlane(regrid_fcst_olr), UserScript(create_eof_filelist), UserScript(script_omi) # Finding EOF files and OMI Analysis script for the observations PROCESS_LIST = UserScript(create_eof_filelist), UserScript(script_omi) @@ -19,10 +19,10 @@ LOOP_BY = VALID VALID_TIME_FMT = %Y%m%d%H # Start time for METplus run -VALID_BEG = 1979010100 +VALID_BEG = 2017010100 # End time for METplus run -VALID_END = 2012123000 +VALID_END = 2018123100 # Increment between METplus runs in seconds. Must be >= 60 VALID_INCREMENT = 86400 @@ -47,7 +47,7 @@ CONFIG_DIR={PARM_BASE}/use_cases/model_applications/s2s # Run the obs for these cases OBS_RUN = True -FCST_RUN = False +FCST_RUN = True # Mask to use for regridding REGRID_DATA_PLANE_VERIF_GRID = latlon 144 17 -20 0 2.5 2.5 @@ -59,12 +59,36 @@ REGRID_DATA_PLANE_METHOD = NEAREST REGRID_DATA_PLANE_WIDTH = 1 # Input and Output Directories for the OBS OLR Files and output text file containing the file list -OBS_OLR_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/ERA +OBS_OLR_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/ERA/Regrid OBS_OLR_INPUT_TEMPLATE = OLR_{valid?fmt=%Y%m%d}.nc +# Input and Output Directories for the OBS OLR Files and output text file containing the file list +FCST_OLR_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/GFS/Regrid +FCST_OLR_INPUT_TEMPLATE = OLR_{valid?fmt=%Y%m%d}.nc + + +# Configurations for pcp_combine: Create daily means for the GFS +[daily_mean_fcst] +# run pcp_combine on obs data +FCST_PCP_COMBINE_RUN = {FCST_RUN} + +# method to run pcp_combine on forecast data +# Options are ADD, SUM, SUBTRACT, DERIVE, and USER_DEFINED +FCST_PCP_COMBINE_METHOD = USER_DEFINED + +FCST_PCP_COMBINE_COMMAND = -derive mean {FCST_PCP_COMBINE_INPUT_DIR}/{valid?fmt=%Y}/{valid?fmt=%Y%m%d}/gfs.0p25.{valid?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=86400}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{valid?fmt=%Y}/{valid?fmt=%Y%m%d}/gfs.0p25.{valid?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=75600}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{valid?fmt=%Y}/{valid?fmt=%Y%m%d}/gfs.0p25.{valid?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=64800}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{init?fmt=%Y}/{init?fmt=%Y%m%d}/gfs.0p25.{init?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=54000}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{init?fmt=%Y}/{init?fmt=%Y%m%d}/gfs.0p25.{init?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=43200}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{init?fmt=%Y}/{init?fmt=%Y%m%d}/gfs.0p25.{init?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=32400}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{init?fmt=%Y}/{init?fmt=%Y%m%d}/gfs.0p25.{init?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=21600}.grib2 {FCST_PCP_COMBINE_INPUT_DIR}/{init?fmt=%Y}/{init?fmt=%Y%m%d}/gfs.0p25.{init?fmt=%Y%m%d%H}.f{lead?fmt=%HHH?shift=10800}.grib2 -field 'name="ULWRF"; level="L0"; set_attr_valid = "{valid?fmt=%Y%m%d_%H%M%S}"; GRIB2_ipdtmpl_index = 9; GRIB2_ipdtmpl_val = 8;' + +FCST_PCP_COMBINE_INPUT_DIR = /gpfs/fs1/collections/rda/data/ds084.1 +FCST_PCP_COMBINE_INPUT_TEMPLATE = {valid?fmt=%Y%m}/gfs.0p25.{init?fmt=%y%m%d%H}.f{lead?fmt=%HHH}.grib2 + +FCST_PCP_COMBINE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_OMI/GFS/daily_mean +FCST_PCP_COMBINE_OUTPUT_TEMPLATE = GFS_mean_{valid?fmt=%Y%m%d}.nc -# Configurations for regrid_data_plane: Regrid OLR to -20 to 20 latitude + +# Configurations for regrid_data_plane: Regrid ERA OLR to -20 to 20 latitude [regrid_obs_olr] +LEAD_SEQ = 0 + # Run regrid_data_plane on forecast data OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} @@ -84,7 +108,7 @@ OBS_REGRID_DATA_PLANE_VAR1_OPTIONS = file_type=NETCDF_NCCF; censor_thresh=eq-999 OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = olr # input and output data directories for each application in PROCESS_LIST -OBS_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI +OBS_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/ERA/daily_mean OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OBS_OLR_INPUT_DIR} # format of filenames @@ -93,6 +117,34 @@ OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = olr.1x.7920.nc OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = {OBS_OLR_INPUT_TEMPLATE} +# Configurations for regrid_data_plane: Regrid GFS OLR to -20 to 20 latitude +[regrid_fcst_olr] +# Run regrid_data_plane on forecast data +FCST_REGRID_DATA_PLANE_RUN = {FCST_RUN} + +# If true, process each field individually and write a file for each +# If false, run once per run time passing in all fields specified +REGRID_DATA_PLANE_ONCE_PER_FIELD = False + +# Name of input field to process +FCST_REGRID_DATA_PLANE_VAR1_NAME = ULWRF_L0_mean + +# Level of input field to process +FCST_REGRID_DATA_PLANE_VAR1_LEVELS = "(*,*)" + +# Name of output field to create +FCST_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = olr + +# input and output data directories for each application in PROCESS_LIST +FCST_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/GFS/daily_mean +FCST_REGRID_DATA_PLANE_OUTPUT_DIR = {FCST_OLR_INPUT_DIR} + +# format of filenames +# Input ERA Interim +FCST_REGRID_DATA_PLANE_INPUT_TEMPLATE = GFS_mean_{valid?fmt=%Y%m%d}.nc +FCST_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = {FCST_OLR_INPUT_TEMPLATE} + + # Create the EOF filelists [create_eof_filelist] # Find the files for each time to create the time list @@ -133,11 +185,13 @@ OBS_PER_DAY = 1 OMI_PLOT_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_OMI/plots # Phase Plot start date, end date, output name, and format -PHASE_PLOT_TIME_BEG = 2012010100 -PHASE_PLOT_TIME_END = 2012033000 +PHASE_PLOT_TIME_BEG = 2017010100 +PHASE_PLOT_TIME_END = 2017033100 PHASE_PLOT_TIME_FMT = {VALID_TIME_FMT} OBS_PHASE_PLOT_OUTPUT_NAME = obs_OMI_comp_phase -OBS_PHASE_PLOT_OUTPUT_FORMAT = png +OBS_PHASE_PLOT_OUTPUT_FORMAT = png +FCST_PHASE_PLOT_OUTPUT_NAME = fcst_OMI_comp_phase +FCST_PHASE_PLOT_OUTPUT_FORMAT = png # Configurations for UserScript: Run the RMM Analysis driver @@ -146,12 +200,12 @@ OBS_PHASE_PLOT_OUTPUT_FORMAT = png USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD ## Template of OLR filenames to input to the user-script -USER_SCRIPT_INPUT_TEMPLATE = {OBS_OLR_INPUT_DIR}/{OBS_OLR_INPUT_TEMPLATE} +USER_SCRIPT_INPUT_TEMPLATE = {OBS_OLR_INPUT_DIR}/{OBS_OLR_INPUT_TEMPLATE},{FCST_OLR_INPUT_DIR}/{FCST_OLR_INPUT_TEMPLATE} ## Name of the file containing the listing of OLR input files ## The options are OBS_OLR_INPUT and FCST_OLR_INPUT ## *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE -USER_SCRIPT_INPUT_TEMPLATE_LABELS = OBS_OLR_INPUT +USER_SCRIPT_INPUT_TEMPLATE_LABELS = OBS_OLR_INPUT,FCST_OLR_INPUT # Command to run the user script with input configuration file USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py b/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py index 1c63b88104..9e1ab61d05 100755 --- a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py +++ b/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_OMI/OMI_driver.py @@ -84,6 +84,7 @@ def run_omi_steps(inlabel, olr_filetxt, spd, EOF1, EOF2, oplot_dir): # Get the output name and format for the PC plase diagram phase_plot_name = os.path.join(oplot_dir,os.environ.get(inlabel+'_PHASE_PLOT_OUTPUT_NAME',inlabel+'_OMI_comp_phase')) + print(phase_plot_name) phase_plot_format = os.environ.get(inlabel+'_PHASE_PLOT_OUTPUT_FORMAT','png') # plot the PC phase diagram @@ -128,7 +129,7 @@ def main(): # Determine if doing forecast or obs run_obs_omi = os.environ.get('RUN_OBS','False').lower() - run_fcst_omi = os.environ.get('FCST_RUN_FCST', 'False').lower() + run_fcst_omi = os.environ.get('RUN_FCST', 'False').lower() # Run the steps to compute OMM # Observations diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf b/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf deleted file mode 100644 index 2eb51d1608..0000000000 --- a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM.conf +++ /dev/null @@ -1,214 +0,0 @@ -# RMM UserScript wrapper -[config] -# All steps, including pre-processing: -#PROCESS_LIST = RegridDataPlane(regrid_obs_olr), RegridDataPlane(regrid_obs_u850), RegridDataPlane(regrid_obs_u200), UserScript(script_rmm) -# Only RMM Analysis script for the observations -PROCESS_LIST = UserScript(script_rmm) - -# time looping - options are INIT, VALID, RETRO, and REALTIME -# If set to INIT or RETRO: -# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set -# If set to VALID or REALTIME: -# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set -LOOP_BY = VALID - -# Format of VALID_BEG and VALID_END using % items -# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. -# see www.strftime.org for more information -# %Y%m%d%H expands to YYYYMMDDHH -VALID_TIME_FMT = %Y%m%d%H - -# Start time for METplus run -VALID_BEG = 2000010100 - -# End time for METplus run -VALID_END = 2002123000 - -# Increment between METplus runs in seconds. Must be >= 60 -VALID_INCREMENT = 86400 - -# List of forecast leads to process for each run time (init or valid) -# In hours if units are not specified -# If unset, defaults to 0 (don't loop through forecast leads) -LEAD_SEQ = 0 - -# Order of loops to process data - Options are times, processes -# Not relevant if only one item is in the PROCESS_LIST -# times = run all wrappers in the PROCESS_LIST for a single run time, then -# increment the run time and run all wrappers again until all times have -# been evaluated. -# processes = run the first wrapper in the PROCESS_LIST for all times -# specified, then repeat for the next item in the PROCESS_LIST until all -# wrappers have been run -LOOP_ORDER = processes - -# location of configuration files used by MET applications -CONFIG_DIR={PARM_BASE}/use_cases/model_applications/s2s - -# Run the obs for these cases -OBS_RUN = True -FCST_RUN = False - -# Mask to use for regridding -REGRID_DATA_PLANE_VERIF_GRID = latlon 144 13 -15 0 2.5 2.5 - -# Method to run regrid_data_plane, not setting this will default to NEAREST -REGRID_DATA_PLANE_METHOD = NEAREST - -# Regridding width used in regrid_data_plane, not setting this will default to 1 -REGRID_DATA_PLANE_WIDTH = 1 - - -# Configurations for regrid_data_plane: Regrid OLR to -15 to 15 latitude -[regrid_obs_olr] -# Run regrid_data_plane on forecast data -OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} - -# If true, process each field individually and write a file for each -# If false, run once per run time passing in all fields specified -OBS_DATA_PLANE_ONCE_PER_FIELD = False - -# Name of input field to process -OBS_REGRID_DATA_PLANE_VAR1_NAME = olr - -# Level of input field to process -OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "({valid?fmt=%Y%m%d_%H%M%S},*,*)" - -OBS_REGRID_DATA_PLANE_VAR1_OPTIONS = file_type=NETCDF_NCCF; censor_thresh=eq-999.0; censor_val=-9999.0; - -# Name of output field to create -OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = olr - -# input and output data directories for each application in PROCESS_LIST -OBS_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM -OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_RMM/ERA - -# format of filenames -# Input ERA Interim -OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = olr.1x.7920.anom7901.nc -OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = OLR_{valid?fmt=%Y%m%d}.nc - - -# Configurations for regrid_data_plane: Regrid u850 to -15 to 15 latitude -[regrid_obs_u850] -# Run regrid_data_plane on forecast data -OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} - -# If true, process each field individually and write a file for each -# If false, run once per run time passing in all fields specified -OBS_DATA_PLANE_ONCE_PER_FIELD = False - -# Name of input field to process -OBS_REGRID_DATA_PLANE_VAR1_NAME = uwnd - -# Level of input field to process -OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "({valid?fmt=%Y%m%d_%H%M%S},*,*)" - -OBS_REGRID_DATA_PLANE_VAR1_OPTIONS = file_type=NETCDF_NCCF; censor_thresh=eq-999.0; censor_val=-9999.0; - -# Name of output field to create -OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = uwnd850 - -# input and output data directories for each application in PROCESS_LIST -OBS_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM -OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_RMM/ERA - -# format of filenames -# Input ERA Interim -OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = uwnd.erai.an.2p5.850.daily.anom7901.nc -OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = u850_{valid?fmt=%Y%m%d}.nc - - -# Configurations for regrid_data_plane: Regrid u200 to -15 to 15 latitude -[regrid_obs_u200] -# Run regrid_data_plane on forecast data -OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} - -# If true, process each field individually and write a file for each -# If false, run once per run time passing in all fields specified -OBS_DATA_PLANE_ONCE_PER_FIELD = False - -# Name of input field to process -OBS_REGRID_DATA_PLANE_VAR1_NAME = uwnd - -# Level of input field to process -OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "({valid?fmt=%Y%m%d_%H%M%S},*,*)" - -OBS_REGRID_DATA_PLANE_VAR1_OPTIONS = file_type=NETCDF_NCCF; censor_thresh=eq-999.0; censor_val=-9999.0; - -# Name of output field to create -OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = uwnd200 - -# input and output data directories for each application in PROCESS_LIST -OBS_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM -OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_RMM/ERA - -# format of filenames -# Input ERA Interim -OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = uwnd.erai.an.2p5.200.daily.anom7901.nc -OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = u200_{valid?fmt=%Y%m%d}.nc - - -# Configurations for the RMM analysis script -[user_env_vars] -# Whether to Run the model or obs -RUN_OBS = {OBS_RUN} -RUN_FCST = {FCST_RUN} - -# Make OUTPUT_BASE Available to the script -SCRIPT_OUTPUT_BASE = {OUTPUT_BASE} - -# Number of obs per day -OBS_PER_DAY = 1 - -# EOF Filename -OLR_EOF_INPUT_TEXTFILE = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/EOF/rmm_olr_eofs.txt -U850_EOF_INPUT_TEXTFILE = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/EOF/rmm_u850_eofs.txt -U200_EOF_INPUT_TEXTFILE = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/EOF/rmm_u200_eofs.txt - -# Normalization factors for RMM -RMM_OLR_NORM = 15.11623 -RMM_U850_NORM = 1.81355 -RMM_U200_NORM = 4.80978 -PC1_NORM = 8.618352504159244 -PC2_NORM = 8.40736449709697 - -# Output Directory for the plots -# If not set, it this will default to {OUTPUT_BASE}/plots -RMM_PLOT_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_RMM/plots - -# EOF plot information -EOF_PLOT_OUTPUT_NAME = RMM_EOFs -EOF_PLOT_OUTPUT_FORMAT = png - -# Phase Plot start date, end date, output name, and format -PHASE_PLOT_TIME_BEG = 2002010100 -PHASE_PLOT_TIME_END = 2002123000 -PHASE_PLOT_TIME_FMT = {VALID_TIME_FMT} -OBS_PHASE_PLOT_OUTPUT_NAME = obs_RMM_comp_phase -OBS_PHASE_PLOT_OUTPUT_FORMAT = png - -# Time Series Plot start date, end date, output name, and format -TIMESERIES_PLOT_TIME_BEG = 2002010100 -TIMESERIES_PLOT_TIME_END = 2002123000 -TIMESERIES_PLOT_TIME_FMT = {VALID_TIME_FMT} -OBS_TIMESERIES_PLOT_OUTPUT_NAME = obs_RMM_time_series -OBS_TIMESERIES_PLOT_OUTPUT_FORMAT = png - - -# Configurations for UserScript: Run the RMM Analysis driver -[script_rmm] -# list of strings to loop over for each run time. -# Run the user script once per lead -USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD - -# Template of filenames to input to the user-script -USER_SCRIPT_INPUT_TEMPLATE = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/ERA/OLR_{valid?fmt=%Y%m%d}.nc,{INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/ERA/u850_{valid?fmt=%Y%m%d}.nc,{INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/ERA/u200_{valid?fmt=%Y%m%d}.nc - -# Name of the file containing the listing of input files -# The options are OBS_OLR_INPUT, OBS_U850_INPUT, OBS_U200_INPUT, FCST_OLR_INPUT, FCST_U850_INPUT, and FCST_U200_INPUT -# *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE -USER_SCRIPT_INPUT_TEMPLATE_LABELS = OBS_OLR_INPUT,OBS_U850_INPUT, OBS_U200_INPUT - -# Command to run the user script with input configuration file -USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/RMM_driver.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf new file mode 100644 index 0000000000..fff56cde25 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf @@ -0,0 +1,157 @@ +# OMI UserScript wrapper +[config] +# All steps, including pre-processing: +#PROCESS_LIST = RegridDataPlane(regrid_obs_olr), UserScript(create_eof_filelist), UserScript(script_omi) +# Finding EOF files and OMI Analysis script for the observations +PROCESS_LIST = UserScript(create_eof_filelist), UserScript(script_omi) + +# time looping - options are INIT, VALID, RETRO, and REALTIME +# If set to INIT or RETRO: +# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set +# If set to VALID or REALTIME: +# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set +LOOP_BY = VALID + +# Format of VALID_BEG and VALID_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d%H + +# Start time for METplus run +VALID_BEG = 1979010100 + +# End time for METplus run +VALID_END = 2012123000 + +# Increment between METplus runs in seconds. Must be >= 60 +VALID_INCREMENT = 86400 + +# List of forecast leads to process for each run time (init or valid) +# In hours if units are not specified +# If unset, defaults to 0 (don't loop through forecast leads) +LEAD_SEQ = 0 + +# Order of loops to process data - Options are times, processes +# Not relevant if only one item is in the PROCESS_LIST +# times = run all wrappers in the PROCESS_LIST for a single run time, then +# increment the run time and run all wrappers again until all times have +# been evaluated. +# processes = run the first wrapper in the PROCESS_LIST for all times +# specified, then repeat for the next item in the PROCESS_LIST until all +# wrappers have been run +LOOP_ORDER = processes + +# location of configuration files used by MET applications +CONFIG_DIR={PARM_BASE}/use_cases/model_applications/s2s + +# Run the obs for these cases +OBS_RUN = True +FCST_RUN = False + +# Mask to use for regridding +REGRID_DATA_PLANE_VERIF_GRID = latlon 144 17 -20 0 2.5 2.5 + +# Method to run regrid_data_plane, not setting this will default to NEAREST +REGRID_DATA_PLANE_METHOD = NEAREST + +# Regridding width used in regrid_data_plane, not setting this will default to 1 +REGRID_DATA_PLANE_WIDTH = 1 + +# Input and Output Directories for the OBS OLR Files and output text file containing the file list +OBS_OLR_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/ERA +OBS_OLR_INPUT_TEMPLATE = OLR_{valid?fmt=%Y%m%d}.nc + + +# Configurations for regrid_data_plane: Regrid OLR to -20 to 20 latitude +[regrid_obs_olr] +# Run regrid_data_plane on forecast data +OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} + +# If true, process each field individually and write a file for each +# If false, run once per run time passing in all fields specified +OBS_DATA_PLANE_ONCE_PER_FIELD = False + +# Name of input field to process +OBS_REGRID_DATA_PLANE_VAR1_NAME = olr + +# Level of input field to process +OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "({valid?fmt=%Y%m%d_%H%M%S},*,*)" + +OBS_REGRID_DATA_PLANE_VAR1_OPTIONS = file_type=NETCDF_NCCF; censor_thresh=eq-999.0; censor_val=-9999.0; + +# Name of output field to create +OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = olr + +# input and output data directories for each application in PROCESS_LIST +OBS_REGRID_DATA_PLANE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_OMI +OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OBS_OLR_INPUT_DIR} + +# format of filenames +# Input ERA Interim +OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = olr.1x.7920.nc +OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = {OBS_OLR_INPUT_TEMPLATE} + + +# Create the EOF filelists +[create_eof_filelist] +# Find the files for each time to create the time list +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE + +# Valid Begin and End Times for the EOF files +VALID_BEG = 2012010100 +VALID_END = 2012123100 + +# Find the EOF files for each time +# Filename templates for EOF1 and EOF2 +USER_SCRIPT_INPUT_TEMPLATE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/EOF/eof1/eof{valid?fmt=%j}.txt,{INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/EOF/eof2/eof{valid?fmt=%j}.txt + +# Name of the file containing the listing of input files +# The options are EOF1_INPUT and EOF2_INPUT +# *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE +USER_SCRIPT_INPUT_TEMPLATE_LABELS = EOF1_INPUT, EOF2_INPUT + +# Placeholder command just to build the file list +# This just states that it's building the file list +USER_SCRIPT_COMMAND = echo Populated file list for EOF1 and EOF2 Input + + +# Configurations for the OMI analysis script +[user_env_vars] +# Whether to Run the model or obs +RUN_OBS = {OBS_RUN} +RUN_FCST = {FCST_RUN} + +# Make OUTPUT_BASE Available to the script +SCRIPT_OUTPUT_BASE = {OUTPUT_BASE} + +# Number of obs per day +OBS_PER_DAY = 1 + +# Output Directory for the plots +# If not set, it this will default to {OUTPUT_BASE}/plots +OMI_PLOT_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_OMI/plots + +# Phase Plot start date, end date, output name, and format +PHASE_PLOT_TIME_BEG = 2012010100 +PHASE_PLOT_TIME_END = 2012033000 +PHASE_PLOT_TIME_FMT = {VALID_TIME_FMT} +OBS_PHASE_PLOT_OUTPUT_NAME = obs_OMI_comp_phase +OBS_PHASE_PLOT_OUTPUT_FORMAT = png + + +# Configurations for UserScript: Run the RMM Analysis driver +[script_omi] +# Run the script once per lead time +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD + +## Template of OLR filenames to input to the user-script +USER_SCRIPT_INPUT_TEMPLATE = {OBS_OLR_INPUT_DIR}/{OBS_OLR_INPUT_TEMPLATE} + +## Name of the file containing the listing of OLR input files +## The options are OBS_OLR_INPUT and FCST_OLR_INPUT +## *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE +USER_SCRIPT_INPUT_TEMPLATE_LABELS = OBS_OLR_INPUT + +# Command to run the user script with input configuration file +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py new file mode 120000 index 0000000000..ff871c910e --- /dev/null +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_OMI/OMI_driver.py @@ -0,0 +1 @@ +../UserScript_fcstGFS_obsERA_OMI/OMI_driver.py \ No newline at end of file diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf similarity index 92% rename from parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf rename to parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf index 9326a96e2b..7f435f4034 100644 --- a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram.conf +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram.conf @@ -50,7 +50,7 @@ FCST_RUN = False # Input and Output Directories for the OBS OLR Files and output text file containing the file list OBS_PDTIME_FMT = %Y%m%d-%H%M%S OBS_PDTIME_INPUT_TEMPLATE = {valid?fmt=%Y%m%d-%H%M%S} -OBS_PDTIME_OUTPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/ +OBS_PDTIME_OUTPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/ OBS_PDTIME_OUTPUT_TEMPLATE = time_list_lead{lead?fmt=%HHH}.txt @@ -59,7 +59,7 @@ OBS_PDTIME_OUTPUT_TEMPLATE = time_list_lead{lead?fmt=%HHH}.txt # Find the files for each time USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_FOR_EACH -USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/save_input_files_txt.py {OBS_PDTIME_INPUT_TEMPLATE} {OBS_PDTIME_OUTPUT_DIR}/{OBS_PDTIME_OUTPUT_TEMPLATE} +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/save_input_files_txt.py {OBS_PDTIME_INPUT_TEMPLATE} {OBS_PDTIME_OUTPUT_DIR}/{OBS_PDTIME_OUTPUT_TEMPLATE} # Configurations for the Phase Diagram Plotting Script @@ -86,7 +86,7 @@ OBS_PHASE_DIAGRAM_INPUT_TIMELIST_TEXTFILE = {OBS_PDTIME_OUTPUT_DIR}/{OBS_PDTIME_ OBS_PHASE_DIAGRAM_INPUT_TIME_FMT = {OBS_PDTIME_FMT} # Plot Output Directory -PHASE_DIAGRAM_PLOT_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/plots +PHASE_DIAGRAM_PLOT_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/plots # Plot Ouptut Name OBS_PHASE_PLOT_OUTPUT_NAME = RMM_phase_diagram @@ -99,4 +99,4 @@ OBS_PHASE_PLOT_OUTPUT_NAME = RMM_phase_diagram USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD # Command to run the user script with input configuration file -USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/PhaseDiagram_driver.py +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/PhaseDiagram_driver.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/PhaseDiagram_driver.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/PhaseDiagram_driver.py similarity index 100% rename from parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/PhaseDiagram_driver.py rename to parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/PhaseDiagram_driver.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/save_input_files_txt.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/save_input_files_txt.py similarity index 100% rename from parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_PhaseDiagram/save_input_files_txt.py rename to parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_PhaseDiagram/save_input_files_txt.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf new file mode 100644 index 0000000000..495a124b2f --- /dev/null +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf @@ -0,0 +1,436 @@ +# RMM UserScript wrapper +[config] +# All steps, including creating daily means and mean daily annual cycle +#PROCESS_LIST = PcpCombine(mean_daily_annual_cycle_obs_wind), PcpCombine(mean_daily_annual_cycle_obs_olr), PcpCombine(daily_mean_obs_wind), PcpCombine(daily_mean_obs_olr), UserScript(create_mda_filelist), UserScript(harmonic_anomalies_olr), UserScript(harmonic_anomalies_u850), UserScript(harmonic_anomalies_u200), RegridDataPlane(regrid_obs_olr), RegridDataPlane(regrid_obs_u850), RegridDataPlane(regrid_obs_u200), UserScript(script_rmm) +# Computing anomalies, regridding, and RMM Analysis script +PROCESS_LIST = UserScript(create_mda_filelist), UserScript(harmonic_anomalies_olr), UserScript(harmonic_anomalies_u850), UserScript(harmonic_anomalies_u200), RegridDataPlane(regrid_obs_olr), RegridDataPlane(regrid_obs_u850), RegridDataPlane(regrid_obs_u200), UserScript(script_rmm) + +# time looping - options are INIT, VALID, RETRO, and REALTIME +# If set to INIT or RETRO: +# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set +# If set to VALID or REALTIME: +# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set +LOOP_BY = VALID + +# Format of VALID_BEG and VALID_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d%H + +# Start time for METplus run +VALID_BEG = 2000010100 + +# End time for METplus run +VALID_END = 2002123000 + +# Increment between METplus runs in seconds. Must be >= 60 +VALID_INCREMENT = 86400 + +# List of forecast leads to process for each run time (init or valid) +# In hours if units are not specified +# If unset, defaults to 0 (don't loop through forecast leads) +LEAD_SEQ = 0 + +# Order of loops to process data - Options are times, processes +# Not relevant if only one item is in the PROCESS_LIST +# times = run all wrappers in the PROCESS_LIST for a single run time, then +# increment the run time and run all wrappers again until all times have +# been evaluated. +# processes = run the first wrapper in the PROCESS_LIST for all times +# specified, then repeat for the next item in the PROCESS_LIST until all +# wrappers have been run +LOOP_ORDER = processes + +# location of configuration files used by MET applications +CONFIG_DIR={PARM_BASE}/use_cases/model_applications/s2s + +# Run the obs for these cases +OBS_RUN = True +FCST_RUN = False + +# Mask to use for regridding +REGRID_DATA_PLANE_VERIF_GRID = latlon 144 13 -15 0 2.5 2.5 + +# Method to run regrid_data_plane, not setting this will default to NEAREST +REGRID_DATA_PLANE_METHOD = NEAREST + +# Regridding width used in regrid_data_plane, not setting this will default to 1 +REGRID_DATA_PLANE_WIDTH = 1 + + +# Configurations for creating U200 and U850 mean daily annual cycle obs +# Mean daily annual cycle anomalies are computed for 1979 - 2001 +[mean_daily_annual_cycle_obs_wind] +LOOP_BY = VALID + +# Format of VALID_BEG and VALID_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d%H + +# Start time for METplus run +# Set to one year, since we want a mean daily across all years +# Using 2012 because leap day will be included +VALID_BEG = 2012010100 + +# End time for METplus run +VALID_END = 2012123100 + +# Increment between METplus runs in seconds. Must be >= 60 +VALID_INCREMENT = 86400 + +# run pcp_combine on obs data +OBS_PCP_COMBINE_RUN = {OBS_RUN} + +# method to run pcp_combine on forecast data +# Options are ADD, SUM, SUBTRACT, DERIVE, and USER_DEFINED +OBS_PCP_COMBINE_METHOD = USER_DEFINED + +OBS_PCP_COMBINE_COMMAND = -derive mean {OBS_PCP_COMBINE_INPUT_DIR}/{OBS_PCP_COMBINE_INPUT_TEMPLATE} -field 'name="U_P850_mean"; level="(*,*)"; set_attr_valid = "{valid?fmt=%Y%m%d_%H%M%S}";' -field 'name="U_P200_mean"; level="(*,*)"; set_attr_valid = "{valid?fmt=%Y%m%d_%H%M%S}";' -name U_P850_mean,U_P200_mean + +OBS_PCP_COMBINE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean +OBS_PCP_COMBINE_INPUT_TEMPLATE = ERA_wind_daily_mean_*{valid?fmt=%m%d}.nc + +OBS_PCP_COMBINE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/mean_daily_annual_cycle +OBS_PCP_COMBINE_OUTPUT_TEMPLATE = ERA_wind_daily_annual_{valid?fmt=%m%d}.nc + + +# Configurations for creating OLR mean daily annual cycle obs +# Mean daily annual cycle anomalies are computed for 1979 - 2001 +[mean_daily_annual_cycle_obs_olr] +LOOP_BY = VALID + +# Format of VALID_BEG and VALID_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d%H + +# Start time for METplus run +# Set to one year, since we want a mean daily across all years +# Using 2012 because leap day will be included +VALID_BEG = 2012010100 + +# End time for METplus run +VALID_END = 2012123100 + +# Increment between METplus runs in seconds. Must be >= 60 +VALID_INCREMENT = 86400 + +# run pcp_combine on obs data +OBS_PCP_COMBINE_RUN = {OBS_RUN} + +# method to run pcp_combine on forecast data +# Options are ADD, SUM, SUBTRACT, DERIVE, and USER_DEFINED +OBS_PCP_COMBINE_METHOD = USER_DEFINED + +OBS_PCP_COMBINE_COMMAND = -derive mean {OBS_PCP_COMBINE_INPUT_DIR}/{OBS_PCP_COMBINE_INPUT_TEMPLATE} -field 'name="olr"; level="(*,*)";' + +OBS_PCP_COMBINE_INPUT_DIR = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean +OBS_PCP_COMBINE_INPUT_TEMPLATE = ERA_OLR_daily_mean_*{valid?fmt=%m%d}.nc + +OBS_PCP_COMBINE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/mean_daily_annual_cycle +OBS_PCP_COMBINE_OUTPUT_TEMPLATE = ERA_OLR_daily_annual_{valid?fmt=%m%d}.nc + + +# Configurations for creating U200 and U850 daily mean obs +[daily_mean_obs_wind] +LOOP_BY = VALID + +# Format of VALID_BEG and VALID_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d%H + +# Start time for METplus run +VALID_BEG = 1979010100 + +# End time for METplus run +VALID_END = 2002123100 + +# Increment between METplus runs in seconds. Must be >= 60 +VALID_INCREMENT = 86400 + +# run pcp_combine on obs data +OBS_PCP_COMBINE_RUN = {OBS_RUN} + +# method to run pcp_combine on forecast data +# Options are ADD, SUM, SUBTRACT, DERIVE, and USER_DEFINED +OBS_PCP_COMBINE_METHOD = USER_DEFINED + +OBS_PCP_COMBINE_COMMAND = -derive mean {OBS_PCP_COMBINE_INPUT_DIR}/{OBS_PCP_COMBINE_INPUT_TEMPLATE} -field 'name="U"; level="P850"; set_attr_valid = "{valid?fmt=%Y%m%d_%H%M%S}";' -field 'name="U"; level="P200"; set_attr_valid = "{valid?fmt=%Y%m%d_%H%M%S}";' + +OBS_PCP_COMBINE_INPUT_DIR = /gpfs/fs1/collections/rda/data/ds627.0/ei.oper.an.pl +OBS_PCP_COMBINE_INPUT_TEMPLATE = {valid?fmt=%Y%m}/ei.oper.an.pl.regn128uv.{valid?fmt=%Y%m%d}* + +OBS_PCP_COMBINE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean +OBS_PCP_COMBINE_OUTPUT_TEMPLATE = ERA_wind_daily_mean_{valid?fmt=%Y%m%d}.nc + + +# Configurations for creating mean daily annual cycle obs OLR +[daily_mean_obs_olr] +LOOP_BY = VALID + +# Format of VALID_BEG and VALID_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d%H + +# Start time for METplus run +VALID_BEG = 1979010100 + +# End time for METplus run +VALID_END = 2002123100 + +# Increment between METplus runs in seconds. Must be >= 60 +VALID_INCREMENT = 86400 + +# run pcp_combine on obs data +OBS_PCP_COMBINE_RUN = {OBS_RUN} + +# method to run pcp_combine on forecast data +# Options are ADD, SUM, SUBTRACT, DERIVE, and USER_DEFINED +OBS_PCP_COMBINE_METHOD = USER_DEFINED + +OBS_PCP_COMBINE_COMMAND = -add {OBS_PCP_COMBINE_INPUT_DIR}/{OBS_PCP_COMBINE_INPUT_TEMPLATE} -field 'name="olr"; level="({valid?fmt=%Y%m%d_%H%M%S},*,*)"; file_type=NETCDF_NCCF;' + +OBS_PCP_COMBINE_INPUT_DIR = /glade/u/home/kalb/MJO +OBS_PCP_COMBINE_INPUT_TEMPLATE = olr.1x.7920.nc + +OBS_PCP_COMBINE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean +OBS_PCP_COMBINE_OUTPUT_TEMPLATE = ERA_OLR_daily_mean_{valid?fmt=%Y%m%d}.nc + + +# Creating a file list of the mean daily annual cycle files +# This is run separately since it has different start/end times +[create_mda_filelist] +# Find the files for each lead time +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD + +# Valid Begin and End Times for the CBL File Climatology +VALID_BEG = 2012010100 +VALID_END = 2012123100 +VALID_INCREMENT = 86400 +LEAD_SEQ = 0 + +# Template of filenames to input to the user-script +USER_SCRIPT_INPUT_TEMPLATE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/mean_daily_annual_cycle/ERA_OLR_daily_annual_{valid?fmt=%m%d}.nc,{INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/mean_daily_annual_cycle/ERA_wind_daily_annual_{valid?fmt=%m%d}.nc + +# Name of the file containing the listing of input files +USER_SCRIPT_INPUT_TEMPLATE_LABELS = input_mean_daily_annual_infiles_olr,input_mean_daily_annual_infiles_wind + +# Placeholder command just to build the file list +# This just states that it's building the file list +USER_SCRIPT_COMMAND = echo Populated file list for Mean daily annual cycle Input + + +# Configurations to create anomalies for OLR +[harmonic_anomalies_olr] +# list of strings to loop over for each run time. +# Run the user script once per lead +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD + +# Template of filenames to input to the user-script +USER_SCRIPT_INPUT_TEMPLATE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean/ERA_OLR_daily_mean_{valid?fmt=%Y%m%d}.nc + +# Name of the file containing the listing of input files +# The options are OBS_OLR_INPUT, OBS_U850_INPUT, OBS_U200_INPUT, FCST_OLR_INPUT, FCST_U850_INPUT, and FCST_U200_INPUT +# *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE +USER_SCRIPT_INPUT_TEMPLATE_LABELS = input_daily_mean_infiles + +# Command to run the user script with input configuration file +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py 'METPLUS_FILELIST_INPUT_MEAN_DAILY_ANNUAL_INFILES_OLR' 'olr' 'olr_NA_mean' '{OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Anomaly' 'ERA_OLR_anom' + + +# Configurations to create anomalies for U850 +[harmonic_anomalies_u850] +# list of strings to loop over for each run time. +# Run the user script once per lead +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD + +# Template of filenames to input to the user-script +USER_SCRIPT_INPUT_TEMPLATE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean/ERA_wind_daily_mean_{valid?fmt=%Y%m%d}.nc + +# Name of the file containing the listing of input files +# The options are OBS_OLR_INPUT, OBS_U850_INPUT, OBS_U200_INPUT, FCST_OLR_INPUT, FCST_U850_INPUT, and FCST_U200_INPUT +# *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE +USER_SCRIPT_INPUT_TEMPLATE_LABELS = input_daily_mean_infiles + +# Command to run the user script with input configuration file +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py 'METPLUS_FILELIST_INPUT_MEAN_DAILY_ANNUAL_INFILES_WIND' 'U_P850_mean' 'U_P850_mean' '{OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Anomaly' 'ERA_U850_anom' + + +# Configurations to create anomalies for U200 +[harmonic_anomalies_u200] +# list of strings to loop over for each run time. +# Run the user script once per lead +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD + +# Template of filenames to input to the user-script +USER_SCRIPT_INPUT_TEMPLATE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/ERA/daily_mean/ERA_wind_daily_mean_{valid?fmt=%Y%m%d}.nc + +# Name of the file containing the listing of input files +# The options are OBS_OLR_INPUT, OBS_U850_INPUT, OBS_U200_INPUT, FCST_OLR_INPUT, FCST_U850_INPUT, and FCST_U200_INPUT +# *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE +USER_SCRIPT_INPUT_TEMPLATE_LABELS = input_daily_mean_infiles + +# Command to run the user script with input configuration file +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py 'METPLUS_FILELIST_INPUT_MEAN_DAILY_ANNUAL_INFILES_WIND' 'U_P200_mean' 'U_P200_mean' '{OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Anomaly' 'ERA_U200_anom' + + +# Configurations for regrid_data_plane: Regrid OLR to -15 to 15 latitude +[regrid_obs_olr] +# Run regrid_data_plane on forecast data +OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} + +# If true, process each field individually and write a file for each +# If false, run once per run time passing in all fields specified +REGRID_DATA_PLANE_ONCE_PER_FIELD = False + +# Name of input field to process +OBS_REGRID_DATA_PLANE_VAR1_NAME = olr_anom + +# Level of input field to process +OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "(*,*)" + +# Name of output field to create +OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = OLR_anom + +# input and output data directories for each application in PROCESS_LIST +OBS_REGRID_DATA_PLANE_INPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Anomaly +OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Regrid + +# format of filenames +# Input ERA Interim +OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = ERA_OLR_anom_{lead?fmt=%H%M%S}L_{valid?fmt=%Y%m%d}_{valid?fmt=%H%M%S}V.nc +OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = ERA_OLR_{valid?fmt=%Y%m%d}.nc + + +# Configurations for regrid_data_plane: Regrid u850 to -15 to 15 latitude +[regrid_obs_u850] +# Run regrid_data_plane on forecast data +OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} + +# If true, process each field individually and write a file for each +# If false, run once per run time passing in all fields specified +REGRID_DATA_PLANE_ONCE_PER_FIELD = False + +# Name of input field to process +OBS_REGRID_DATA_PLANE_VAR1_NAME = U_P850_mean_anom + +# Level of input field to process +OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "(*,*)" + +# Name of output field to create +OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = U_P850_anom + +# input and output data directories for each application in PROCESS_LIST +OBS_REGRID_DATA_PLANE_INPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Anomaly +OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Regrid + +# format of filenames +# Input ERA Interim +OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = ERA_U850_anom_{lead?fmt=%H%M%S}L_{valid?fmt=%Y%m%d}_{valid?fmt=%H%M%S}V.nc +OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = ERA_U850_{valid?fmt=%Y%m%d}.nc + + +# Configurations for regrid_data_plane: Regrid u200 to -15 to 15 latitude +[regrid_obs_u200] +# Run regrid_data_plane on forecast data +OBS_REGRID_DATA_PLANE_RUN = {OBS_RUN} + +# If true, process each field individually and write a file for each +# If false, run once per run time passing in all fields specified +REGRID_DATA_PLANE_ONCE_PER_FIELD = False + +# Name of input field to process +OBS_REGRID_DATA_PLANE_VAR1_NAME = U_P200_mean_anom + +# Level of input field to process +OBS_REGRID_DATA_PLANE_VAR1_LEVELS = "(*,*)" + +# Name of output field to create +OBS_REGRID_DATA_PLANE_VAR1_OUTPUT_FIELD_NAME = U_P200_anom + +# input and output data directories for each application in PROCESS_LIST +OBS_REGRID_DATA_PLANE_INPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Anomaly +OBS_REGRID_DATA_PLANE_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Regrid + +# format of filenames +# Input ERA Interim +OBS_REGRID_DATA_PLANE_INPUT_TEMPLATE = ERA_U200_anom_{lead?fmt=%H%M%S}L_{valid?fmt=%Y%m%d}_{valid?fmt=%H%M%S}V.nc +OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE = ERA_U200_{valid?fmt=%Y%m%d}.nc + + +# Configurations for the RMM analysis script +[user_env_vars] +# Whether to Run the model or obs +RUN_OBS = {OBS_RUN} +RUN_FCST = {FCST_RUN} + +# Make OUTPUT_BASE Available to the script +SCRIPT_OUTPUT_BASE = {OUTPUT_BASE} + +# Number of obs per day +OBS_PER_DAY = 1 + +# Variable names for OLR, U850, U200 +OBS_OLR_VAR_NAME = OLR_anom +OBS_U850_VAR_NAME = U_P850_anom +OBS_U200_VAR_NAME = U_P200_anom + +# EOF Filename +OLR_EOF_INPUT_TEXTFILE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/EOF/rmm_olr_eofs.txt +U850_EOF_INPUT_TEXTFILE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/EOF/rmm_u850_eofs.txt +U200_EOF_INPUT_TEXTFILE = {INPUT_BASE}/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/EOF/rmm_u200_eofs.txt + +# Normalization factors for RMM +RMM_OLR_NORM = 15.11623 +RMM_U850_NORM = 1.81355 +RMM_U200_NORM = 4.80978 +PC1_NORM = 8.618352504159244 +PC2_NORM = 8.40736449709697 + +# Output Directory for the plots +# If not set, it this will default to {OUTPUT_BASE}/plots +RMM_PLOT_OUTPUT_DIR = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/plots + +# EOF plot information +EOF_PLOT_OUTPUT_NAME = RMM_EOFs +EOF_PLOT_OUTPUT_FORMAT = png + +# Phase Plot start date, end date, output name, and format +PHASE_PLOT_TIME_BEG = 2002010100 +PHASE_PLOT_TIME_END = 2002123000 +PHASE_PLOT_TIME_FMT = {VALID_TIME_FMT} +OBS_PHASE_PLOT_OUTPUT_NAME = obs_RMM_comp_phase +OBS_PHASE_PLOT_OUTPUT_FORMAT = png + +# Time Series Plot start date, end date, output name, and format +TIMESERIES_PLOT_TIME_BEG = 2002010100 +TIMESERIES_PLOT_TIME_END = 2002123000 +TIMESERIES_PLOT_TIME_FMT = {VALID_TIME_FMT} +OBS_TIMESERIES_PLOT_OUTPUT_NAME = obs_RMM_time_series +OBS_TIMESERIES_PLOT_OUTPUT_FORMAT = png + + +# Configurations for UserScript: Run the RMM Analysis driver +[script_rmm] +# list of strings to loop over for each run time. +# Run the user script once per lead +USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE_PER_LEAD + +# Template of filenames to input to the user-script +USER_SCRIPT_INPUT_TEMPLATE = {OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Regrid/ERA_OLR_{valid?fmt=%Y%m%d}.nc,{OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Regrid/ERA_U850_{valid?fmt=%Y%m%d}.nc,{OUTPUT_BASE}/s2s/UserScript_obsERA_obsOnly_RMM/ERA/Regrid/ERA_U200_{valid?fmt=%Y%m%d}.nc + +# Name of the file containing the listing of input files +# The options are OBS_OLR_INPUT, OBS_U850_INPUT, OBS_U200_INPUT, FCST_OLR_INPUT, FCST_U850_INPUT, and FCST_U200_INPUT +# *** Make sure the order is the same as the order of templates listed in USER_SCRIPT_INPUT_TEMPLATE +USER_SCRIPT_INPUT_TEMPLATE_LABELS = OBS_OLR_INPUT,OBS_U850_INPUT, OBS_U200_INPUT + +# Command to run the user script with input configuration file +USER_SCRIPT_COMMAND = {METPLUS_BASE}/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/RMM_driver.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/RMM_driver.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/RMM_driver.py similarity index 88% rename from parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/RMM_driver.py rename to parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/RMM_driver.py index d33906c9d8..3e3f5b741f 100755 --- a/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_RMM/RMM_driver.py +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/RMM_driver.py @@ -48,11 +48,15 @@ def read_rmm_eofs(olrfile, u850file, u200file): def run_rmm_steps(inlabel, spd, EOF1, EOF2, oplot_dir): - # Get OLR, U850, U200 file listings + # Get OLR, U850, U200 file listings and variable names olr_filetxt = os.environ['METPLUS_FILELIST_'+inlabel+'_OLR_INPUT'] u850_filetxt = os.environ['METPLUS_FILELIST_'+inlabel+'_U850_INPUT'] u200_filetxt = os.environ['METPLUS_FILELIST_'+inlabel+'_U200_INPUT'] + olr_var = os.environ[inlabel+'_OLR_VAR_NAME'] + u850_var = os.environ[inlabel+'_U850_VAR_NAME'] + u200_var = os.environ[inlabel+'_U200_VAR_NAME'] + # Read the listing of OLR, U850, U200 files with open(olr_filetxt) as ol: olr_input_files = ol.read().splitlines() @@ -67,6 +71,18 @@ def run_rmm_steps(inlabel, spd, EOF1, EOF2, oplot_dir): if (u200_input_files[0] == 'file_list'): u200_input_files = u200_input_files[1:] + # Check the input data to make sure it's not all missing + olr_allmissing = all(elem == 'missing' for elem in olr_input_files) + if olr_allmissing: + raise IOError ('No input OLR files were found, check file paths') + u850_allmissing = all(elem == 'missing' for elem in u850_input_files) + if u850_allmissing: + raise IOError('No input U850 files were found, check file paths') + u200_allmissing = all(elem == 'missing' for elem in u200_input_files) + if u200_allmissing: + raise IOError('No input U200 files were found, check file paths') + + # Read OLR, U850, U200 data from file netcdf_reader_olr = read_netcdf.ReadNetCDF() ds_olr = netcdf_reader_olr.read_into_xarray(olr_input_files) @@ -81,7 +97,7 @@ def run_rmm_steps(inlabel, spd, EOF1, EOF2, oplot_dir): time = [] for din in range(len(ds_olr)): colr = ds_olr[din] - ctime = datetime.datetime.strptime(colr['olr'].valid_time,'%Y%m%d_%H%M%S') + ctime = datetime.datetime.strptime(colr[olr_var].valid_time,'%Y%m%d_%H%M%S') time.append(ctime.strftime('%Y-%m-%d')) colr = colr.assign_coords(time=ctime) ds_olr[din] = colr.expand_dims("time") @@ -97,17 +113,17 @@ def run_rmm_steps(inlabel, spd, EOF1, EOF2, oplot_dir): time = np.array(time,dtype='datetime64[D]') everything_olr = xr.concat(ds_olr,"time") - olr = everything_olr['olr'] + olr = everything_olr[olr_var] olr = olr.mean('lat') print(olr.min(), olr.max()) everything_u850 = xr.concat(ds_u850,"time") - u850 = everything_u850['uwnd850'] + u850 = everything_u850[u850_var] u850 = u850.mean('lat') print(u850.min(), u850.max()) everything_u200 = xr.concat(ds_u200,"time") - u200 = everything_u200['uwnd200'] + u200 = everything_u200[u200_var] u200 = u200.mean('lat') print(u200.min(), u200.max()) @@ -186,7 +202,7 @@ def main(): # Determine if doing forecast or obs run_obs_rmm = os.environ.get('RUN_OBS', 'False').lower() - run_fcst_rmm = os.environ.get('FCST_RUN_FCST', 'False').lower() + run_fcst_rmm = os.environ.get('RUN_FCST', 'False').lower() if (run_obs_rmm == 'true'): run_rmm_steps('OBS', spd, EOF1, EOF2, oplot_dir) diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py new file mode 100755 index 0000000000..6c6fe9dad7 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_RMM/compute_harmonic_anomalies.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +import numpy as np +import xarray as xr +import glob +import os +import sys +import datetime +import METreadnc.util.read_netcdf as read_netcdf + +input_mean_daily_annual_infiles_list = os.environ[sys.argv[1]] +dm_var = sys.argv[2] +mda_var = sys.argv[3] +anom_output_dir = sys.argv[4] +anom_output_base = sys.argv[5] +input_daily_mean_infiles_list = os.environ['METPLUS_FILELIST_INPUT_DAILY_MEAN_INFILES'] + +# Environment variables for script +nobs = int(os.environ.get('OBS_PER_DAY',1)) +out_var = dm_var+'_anom' + +# Read the listing of files +with open(input_daily_mean_infiles_list) as idm: + input_daily_mean_infiles = idm.read().splitlines() +if (input_daily_mean_infiles[0] == 'file_list'): + input_daily_mean_infiles = input_daily_mean_infiles[1:] + +with open(input_mean_daily_annual_infiles_list) as imda: + input_mean_daily_annual_infiles = imda.read().splitlines() +if (input_mean_daily_annual_infiles[0] == 'file_list'): + input_mean_daily_annual_infiles = input_mean_daily_annual_infiles[1:] + + +# Read in the data +netcdf_reader = read_netcdf.ReadNetCDF() +dm_orig = netcdf_reader.read_into_xarray(input_daily_mean_infiles) +# Add some needed attributes +dm_list = [] +time_dm = [] +yr_dm = [] +doy_dm = [] +for din in dm_orig: + ctime = datetime.datetime.strptime(din[dm_var].valid_time,'%Y%m%d_%H%M%S') + time_dm.append(ctime.strftime('%Y-%m-%d')) + yr_dm.append(int(ctime.strftime('%Y'))) + doy_dm.append(int(ctime.strftime('%j'))) + din = din.assign_coords(time=ctime) + din = din.expand_dims("time") + dm_list.append(din) +time_dm = np.array(time_dm,dtype='datetime64[D]') +yr_dm = np.array(yr_dm) +doy_dm = np.array(doy_dm) +everything = xr.concat(dm_list,"time") +dm_data = np.array(everything[dm_var]) + +netcdf_reader2 = read_netcdf.ReadNetCDF() +mda_orig = netcdf_reader2.read_into_xarray(input_mean_daily_annual_infiles) +# Add some needed attributes +mda_list = [] +time_mda = [] +for din in mda_orig: + ctime = datetime.datetime.strptime(din[mda_var].valid_time,'%Y%m%d_%H%M%S') + time_mda.append(ctime.strftime('%Y-%m-%d')) + din = din.assign_coords(time=ctime) + din = din.expand_dims("time") + mda_list.append(din) +time_mda = np.array(time_mda,dtype='datetime64[D]') +everything2 = xr.concat(mda_list,"time") +mda_data = np.array(everything2[mda_var]) + +# Harmonic Analysis, first step is Forward Fast Fourier Transform +clmfft = np.fft.rfft(mda_data,axis=0) + +smthfft = np.zeros(clmfft.shape,dtype=complex) +for f in np.arange(0,3): + smthfft[f,:,:] = clmfft[f,:,:] + +clmout = np.fft.irfft(smthfft,axis=0) + +# Subtract the clmout from the data to create anomalies, each year at a time +yrstrt = yr_dm[0] +yrend = yr_dm[-1] +anom = np.zeros(dm_data.shape) + +for y in np.arange(yrstrt,yrend+1,1): + curyr = np.where(yr_dm == y) + dd = doy_dm[curyr] - 1 + ndd = len(curyr[0]) + clmshp = [np.arange(dd[0]*nobs,dd[0]*nobs+ndd,1)] + anom[curyr,:,:] = dm_data[curyr,:,:] - clmout[clmshp,:,:] + +# Assign to an xarray and write output +if not os.path.exists(anom_output_dir): + os.makedirs(anom_output_dir) +for o in np.arange(0,len(dm_orig)): + dm_orig_cur = dm_orig[o] + dout = xr.Dataset({out_var: (("lat", "lon"),anom[o,:,:])}, + coords={"lat": dm_orig_cur.coords['lat'], "lon": dm_orig_cur.coords['lon']}, + attrs=dm_orig_cur.attrs) + dout[out_var].attrs = dm_orig_cur[dm_var].attrs + dout[out_var].attrs['long_name'] = dm_orig_cur[dm_var].attrs['long_name']+' Anomalies' + dout[out_var].attrs['name'] = out_var + + # write to a file + cvtime = datetime.datetime.strptime(dm_orig_cur[dm_var].valid_time,'%Y%m%d_%H%M%S') + citime = datetime.datetime.strptime(dm_orig_cur[dm_var].init_time,'%Y%m%d_%H%M%S') + cltime = (cvtime - citime) + leadmin,leadsec = divmod(cltime.total_seconds(), 60) + leadhr,leadmin = divmod(leadmin,60) + lead_str = str(int(leadhr)).zfill(2)+str(int(leadmin)).zfill(2)+str(int(leadsec)).zfill(2) + dout.to_netcdf(os.path.join(anom_output_dir,anom_output_base+'_'+lead_str+'L_'+cvtime.strftime('%Y%m%d')+'_'+cvtime.strftime('%H%M%S')+'V.nc')) From 4fbb6892f2a92cc495c1fbc96b74f00f36c4ce82 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 16 Nov 2021 14:39:09 -0700 Subject: [PATCH 213/770] Feature 1266 gen ens prod missing ensembles (#1275) --- .../met_tool_wrapper/GenEnsProd/GenEnsProd.py | 31 +++++- .../test_ensemble_stat_wrapper.py | 4 +- metplus/util/met_util.py | 6 +- metplus/wrappers/command_builder.py | 104 +++++++++++++++++- metplus/wrappers/ensemble_stat_wrapper.py | 28 ++--- metplus/wrappers/gen_ens_prod_wrapper.py | 77 +++++-------- parm/met_config/GenEnsProdConfig_wrapped | 4 +- .../GenEnsProd/GenEnsProd.conf | 16 ++- 8 files changed, 193 insertions(+), 77 deletions(-) diff --git a/docs/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.py b/docs/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.py index 1408437518..8109723a90 100644 --- a/docs/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.py +++ b/docs/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.py @@ -9,7 +9,21 @@ # Scientific Objective # -------------------- # -# Generate ensemble products. +# Generate ensemble products. This use case demonstrates how to configure +# the gen_ens_prod tool if you expect that there will occasionally be missing +# ensembles. 7 ensemble paths are specified but only 6 of them exist in the +# sample input data set. The wrapper will mark ensembles that are not found +# with the MISSING keyword in the file-list file that is read by the tool. +# Also, one of the ensembles is listed as the control member. The gen_ens_prod +# application will error and exit if the control member is included in the +# ensemble list, but the GenEnsProd wrapper will automatically remove the +# control member from the ensemble list. This makes it easier to configure +# the tool to change the control member without having to change the ensemble +# list. The number of expected members (defined with GEN_ENS_PROD_N_MEMBERS) +# is 6 (7 members - 1 control member). The actual number of ensemble members +# that will be found in this example is 5 (arw-tom-gep4 is not included). +# The ens.ens_thresh value (defined by GEN_ENS_PROD_ENS_THRESH) is set to 0.8. +# There are ~0.833 (5/6) valid ensemble members so the application will run. ############################################################################## # Datasets @@ -95,6 +109,21 @@ # # * gen_ens_prod_20100101_120000V_ens.nc # +# A file-list file will also be generated in stage/file_lists called: +# +# * 20091231120000_24_gen_ens_prod.txt +# +# It should contain a list of 6 files in {INPUT_BASE} with 1 file marked as +# missing because it was not found:: +# +# file_list +# {INPUT_BASE}/met_test/data/sample_fcst/2009123112/arw-sch-gep2/d01_2009123112_02400.grib +# {INPUT_BASE}/met_test/data/sample_fcst/2009123112/arw-tom-gep3/d01_2009123112_02400.grib +# MISSING/{INPUT_BASE}/met_test/data/sample_fcst/2009123112/arw-tom-gep4/d01_2009123112_02400.grib +# {INPUT_BASE}/met_test/data/sample_fcst/2009123112/arw-fer-gep5/d01_2009123112_02400.grib +# {INPUT_BASE}/met_test/data/sample_fcst/2009123112/arw-sch-gep6/d01_2009123112_02400.grib +# {INPUT_BASE}/met_test/data/sample_fcst/2009123112/arw-tom-gep7/d01_2009123112_02400.grib +# ############################################################################## # Keywords diff --git a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py index 750fe994af..be6ee1acb4 100644 --- a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py +++ b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py @@ -568,10 +568,10 @@ def test_ensemble_stat_single_field(metplus_config, config_overrides, config_file = wrapper.c_dict.get('CONFIG_FILE') out_dir = wrapper.c_dict.get('OUTPUT_DIR') expected_cmds = [(f"{app_path} {verbosity} " - f"{file_list_dir}/20050807000000_12_ensemble.txt " + f"{file_list_dir}/20050807000000_12_ensemble_stat.txt " f"{config_file} -outdir {out_dir}/2005080712"), (f"{app_path} {verbosity} " - f"{file_list_dir}/20050807120000_12_ensemble.txt " + f"{file_list_dir}/20050807120000_12_ensemble_stat.txt " f"{config_file} -outdir {out_dir}/2005080800"), ] diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index 414b9795a4..105740e420 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -2282,7 +2282,11 @@ def format_var_items(field_configs, time_info=None): return var_items def find_var_name_indices(config, data_types, met_tool=None): - data_type_regex = f"{'|'.join(data_types)}|BOTH" + data_type_regex = f"{'|'.join(data_types)}" + + # if data_types includes FCST or OBS, also search for BOTH + if any([item for item in ['FCST', 'OBS'] if item in data_types]): + data_type_regex += '|BOTH' regex_string = f"({data_type_regex})" diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 4384f45307..22b1052721 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -22,6 +22,7 @@ from ..util import do_string_sub, ti_calculate, get_seconds_from_string from ..util import config_metplus from ..util import METConfigInfo as met_config +from ..util import MISSING_DATA_VALUE # pylint:disable=pointless-string-statement '''!@namespace CommandBuilder @@ -659,6 +660,8 @@ def find_exact_file(self, level, data_type, time_info, mandatory=True, # then add it back after the string sub call saved_level = time_info.pop('level', None) + input_must_exist = self.c_dict.get('INPUT_MUST_EXIST', True) + for template in template_list: # perform string substitution filename = do_string_sub(template, @@ -671,9 +674,9 @@ def find_exact_file(self, level, data_type, time_info, mandatory=True, if os.path.sep not in full_path: self.logger.debug(f"{full_path} is not a file path. " "Returning that string.") - if return_list: - full_path = [full_path] - return full_path + check_file_list.append(full_path) + input_must_exist = False + continue self.logger.debug(f"Looking for {data_type}INPUT file {full_path}") @@ -719,7 +722,7 @@ def find_exact_file(self, level, data_type, time_info, mandatory=True, for file_path in check_file_list: # if file doesn't need to exist, skip check - if not self.c_dict.get('INPUT_MUST_EXIST', True): + if not input_must_exist: found_file_list.append(file_path) continue @@ -736,6 +739,9 @@ def find_exact_file(self, level, data_type, time_info, mandatory=True, f"using template {template}") if not mandatory or not self.c_dict.get('MANDATORY', True): self.logger.warning(msg) + if self.c_dict.get(f'{data_type}FILL_MISSING'): + found_file_list.append(f'MISSING{file_path}') + continue else: self.log_error(msg) @@ -843,6 +849,96 @@ def find_file_in_window(self, level, data_type, time_info, mandatory=True, return out + def find_input_files_ensemble(self, time_info): + """! Get a list of all input files and optional control file. + Warn and remove control file if found in ensemble list. Ensure that + if defined, the number of ensemble members (N_MEMBERS) corresponds to + the file list that was found. + + @param time_info dictionary containing timing information + @returns True on success + """ + # get list of ensemble files to process + input_files = self.find_model(time_info, return_list=True) + if not input_files: + self.log_error("Could not find any input files") + return False + + # get control file if requested + if self.c_dict.get('CTRL_INPUT_TEMPLATE'): + ctrl_file = self.find_data(time_info, data_type='CTRL') + + # return if requested control file was not found + if not ctrl_file: + return False + + self.args.append(f'-ctrl {ctrl_file}') + + # check if control file is found in ensemble list + if ctrl_file in input_files: + # warn and remove control file if found + self.logger.warning(f"Control file found in ensemble list: " + f"{ctrl_file}. Removing from list.") + input_files.remove(ctrl_file) + + # compare number of files found to expected number of members + if not self._check_expected_ensembles(input_files): + return False + + # write file that contains list of ensemble files + list_filename = (f"{time_info['init_fmt']}_" + f"{time_info['lead_hours']}_{self.app_name}.txt") + list_file = self.write_list_file(list_filename, input_files) + if not list_file: + self.log_error("Could not write filelist file") + return False + + self.infiles.append(list_file) + + return True + + def _check_expected_ensembles(self, input_files): + """! Helper function for find_input_files_ensemble(). + If number of expected ensemble members was defined in the config, + then ensure that the number of files found correspond to the expected + number. If more files were found, error and return False. If fewer + files were found, fill in input_files list with MISSING to allow valid + threshold check inside MET tool to work properly. + """ + num_expected = self.c_dict['N_MEMBERS'] + + # if expected members count is unset, skip check + if num_expected == MISSING_DATA_VALUE: + return True + + num_found = len(input_files) + + # error and return if more than expected number was found + if num_found > num_expected: + self.log_error( + "Found more files than expected! " + f"Found {num_found} expected {num_expected}. " + "Adjust wildcard expression in template or adjust " + "number of expected members (N_MEMBERS). " + f"Files found: {input_files}" + ) + return False + + # if fewer files found than expected, warn and add fake files + if num_found < num_expected: + self.logger.warning( + f"Found fewer files than expected. " + f"Found {num_found} expected {num_expected}" + ) + # add fake files to list for ens_thresh checking + diff = num_expected - num_found + self.logger.warning(f'Adding {diff} fake files to ' + 'ensure ens_thresh check is accurate') + for _ in range(0, diff, 1): + input_files.append('MISSING') + + return True + def write_list_file(self, filename, file_list, output_dir=None): """! Writes a file containing a list of filenames to the staging dir diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index 652170ffbe..2c532ec05f 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -153,11 +153,18 @@ def create_c_dict(self): elif c_dict['OBS_GRID_INPUT_DATATYPE'] in util.PYTHON_EMBEDDING_TYPES: c_dict['OBS_INPUT_DATATYPE'] = c_dict['OBS_GRID_INPUT_DATATYPE'] - c_dict['N_MEMBERS'] = \ - self.config.getint('config', 'ENSEMBLE_STAT_N_MEMBERS', -1) + c_dict['N_MEMBERS'] = ( + self.config.getint('config', 'ENSEMBLE_STAT_N_MEMBERS') + ) + + # allow multiple files in CommandBuilder.find_data logic + c_dict['ALLOW_MULTIPLE_FILES'] = True + + # not all input files are mandatory to be found + c_dict['MANDATORY'] = False - if c_dict['N_MEMBERS'] < 0: - self.log_error("Must set ENSEMBLE_STAT_N_MEMBERS to a integer > 0") + # fill inputs that are not found with fake path to note it is missing + c_dict['FCST_FILL_MISSING'] = True c_dict['OBS_POINT_INPUT_DIR'] = \ self.config.getdir('OBS_ENSEMBLE_STAT_POINT_INPUT_DIR', '') @@ -177,11 +184,9 @@ def create_c_dict(self): c_dict['FCST_INPUT_DIR'] = \ self.config.getdir('FCST_ENSEMBLE_STAT_INPUT_DIR', '') - # This is a raw string and will be interpreted to generate the - # ensemble member filenames. This may be a list of 1 or n members. - c_dict['FCST_INPUT_TEMPLATE'] = \ - util.getlist(self.config.getraw('filename_templates', - 'FCST_ENSEMBLE_STAT_INPUT_TEMPLATE')) + c_dict['FCST_INPUT_TEMPLATE'] = ( + self.config.getraw('config', 'FCST_ENSEMBLE_STAT_INPUT_TEMPLATE') + ) if not c_dict['FCST_INPUT_TEMPLATE']: self.log_error("Must set FCST_ENSEMBLE_STAT_INPUT_TEMPLATE") @@ -364,12 +369,9 @@ def run_at_time_all_fields(self, time_info): @param time_info dictionary containing timing information """ # get ensemble model files - fcst_file_list = self.find_model_members(time_info) - if not fcst_file_list: + if not self.find_input_files_ensemble(time_info): return - self.infiles.append(fcst_file_list) - # parse var list for ENS fields ensemble_var_list = util.sub_var_list(self.c_dict['ENS_VAR_LIST_TEMP'], time_info) diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index 00c7c58e73..3beaebc66e 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -7,6 +7,7 @@ from ..util import do_string_sub, ti_calculate, get_lead_sequence from ..util import skip_time, parse_var_list, sub_var_list + from . import LoopTimesWrapper class GenEnsProdWrapper(LoopTimesWrapper): @@ -21,8 +22,8 @@ class GenEnsProdWrapper(LoopTimesWrapper): 'METPLUS_CAT_THRESH', 'METPLUS_NC_VAR_STR', 'METPLUS_ENS_FILE_TYPE', - 'METPLUS_ENS_ENS_THRESH', - 'METPLUS_ENS_VLD_THRESH', + 'METPLUS_ENS_THRESH', + 'METPLUS_VLD_THRESH', 'METPLUS_ENS_FIELD', 'METPLUS_NBRHD_PROB_DICT', 'METPLUS_NMEP_SMOOTH_DICT', @@ -65,15 +66,27 @@ def create_c_dict(self): ) # get input template/dir - template is required - c_dict['INPUT_TEMPLATE'] = self.config.getraw( + c_dict['FCST_INPUT_TEMPLATE'] = self.config.getraw( 'config', 'GEN_ENS_PROD_INPUT_TEMPLATE' ) - c_dict['INPUT_DIR'] = self.config.getdir('GEN_ENS_PROD_INPUT_DIR', '') + c_dict['FCST_INPUT_DIR'] = self.config.getdir('GEN_ENS_PROD_INPUT_DIR', + '') - if not c_dict['INPUT_TEMPLATE']: + if not c_dict['FCST_INPUT_TEMPLATE']: self.log_error('GEN_ENS_PROD_INPUT_TEMPLATE must be set') + # not all input files are mandatory to be found + c_dict['MANDATORY'] = False + + # fill inputs that are not found with fake path to note it is missing + c_dict['FCST_FILL_MISSING'] = True + + # number of expected ensemble members + c_dict['N_MEMBERS'] = ( + self.config.getint('config', 'GEN_ENS_PROD_N_MEMBERS') + ) + # get ctrl (control) template/dir - optional c_dict['CTRL_INPUT_TEMPLATE'] = self.config.getraw( 'config', @@ -195,68 +208,30 @@ def run_at_time_once(self, time_info): @param time_info dictionary containing timing information """ + # add config file to arguments + config_file = do_string_sub(self.c_dict['CONFIG_FILE'], **time_info) + self.args.append(f"-config {config_file}") + if not self.find_field_info(time_info): return False - if not self.find_input_files(time_info): + if not self.find_input_files_ensemble(time_info): return False if not self.find_and_check_output_file(time_info): return False - # add config file to arguments - config_file = do_string_sub(self.c_dict['CONFIG_FILE'], **time_info) - self.args.append(f"-config {config_file}") - - if not self.find_ctrl_file(time_info): - return False - # set environment variables that are passed to the MET config self.set_environment_variables(time_info) return self.build() - def find_input_files(self, time_info): - """! Get a list of all input files - - @param time_info dictionary containing timing information - @returns True on success - """ - input_files = self.find_data(time_info, return_list=True) - if not input_files: - self.log_error("Could not find any input files") - return False - - # write file that contains list of ensemble files - list_filename = (f"{time_info['init_fmt']}_" - f"{time_info['lead_hours']}_gen_ens_prod.txt") - list_file = self.write_list_file(list_filename, input_files) - if not list_file: - self.log_error("Could not write filelist file") - return False - - self.infiles.append(list_file) - - return True - - def find_ctrl_file(self, time_info): - """! Find optional ctrl (control) file if requested + def find_field_info(self, time_info): + """! parse var list for ENS fields @param time_info dictionary containing timing information - @returns True on success or if ctrl not requested + @returns True if successful, False if something went wrong """ - if not self.c_dict['CTRL_INPUT_TEMPLATE']: - return True - - input_file = self.find_data(time_info, data_type='CTRL') - if not input_file: - return False - - self.args.append(f'-ctrl {input_file}') - return True - - def find_field_info(self, time_info): - # parse var list for ENS fields ensemble_var_list = sub_var_list(self.c_dict['ENS_VAR_LIST_TEMP'], time_info) all_fields = [] diff --git a/parm/met_config/GenEnsProdConfig_wrapped b/parm/met_config/GenEnsProdConfig_wrapped index 59c794310a..2da107e1d4 100644 --- a/parm/met_config/GenEnsProdConfig_wrapped +++ b/parm/met_config/GenEnsProdConfig_wrapped @@ -53,10 +53,10 @@ ens = { ${METPLUS_ENS_FILE_TYPE} //ens_thresh = - ${METPLUS_ENS_ENS_THRESH} + ${METPLUS_ENS_THRESH} //vld_thresh = - ${METPLUS_ENS_VLD_THRESH} + ${METPLUS_VLD_THRESH} //field = ${METPLUS_ENS_FIELD} diff --git a/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf b/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf index 757db28a04..b545614bde 100644 --- a/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf +++ b/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf @@ -22,12 +22,22 @@ LOOP_ORDER = processes GEN_ENS_PROD_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst +# ensemble gep4 does not exist in sample input data GEN_ENS_PROD_INPUT_TEMPLATE = - {init?fmt=%Y%m%d%H}/*gep*/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib + {init?fmt=%Y%m%d%H}/arw-fer-gep1/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib, + {init?fmt=%Y%m%d%H}/arw-sch-gep2/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib, + {init?fmt=%Y%m%d%H}/arw-tom-gep3/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib, + {init?fmt=%Y%m%d%H}/arw-tom-gep4/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib, + {init?fmt=%Y%m%d%H}/arw-fer-gep5/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib, + {init?fmt=%Y%m%d%H}/arw-sch-gep6/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib, + {init?fmt=%Y%m%d%H}/arw-tom-gep7/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib GEN_ENS_PROD_CTRL_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst GEN_ENS_PROD_CTRL_INPUT_TEMPLATE = - {init?fmt=%Y%m%d%H}/arw-tom-gep3/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib + {init?fmt=%Y%m%d%H}/arw-fer-gep1/d01_{init?fmt=%Y%m%d%H}_{lead?fmt=%3H}00.grib + +# there are 7 ensembles but 1 is used as control, so specify 6 members +GEN_ENS_PROD_N_MEMBERS = 6 GEN_ENS_PROD_OUTPUT_DIR = {OUTPUT_BASE}/gen_ens_prod GEN_ENS_PROD_OUTPUT_TEMPLATE = gen_ens_prod_{valid?fmt=%Y%m%d_%H%M%S}V_ens.nc @@ -78,7 +88,7 @@ ENS_VAR5_THRESH = >=5.0 # GEN_ENS_PROD_CAT_THRESH = # GEN_ENS_PROD_NC_VAR_STR = -# GEN_ENS_PROD_ENS_THRESH = 1.0 +GEN_ENS_PROD_ENS_THRESH = 0.8 # GEN_ENS_PROD_VLD_THRESH = 1.0 # GEN_ENS_PROD_NBRHD_PROB_WIDTH = 5 From fd90ba937b86e4958112dd90f60df04865b198e9 Mon Sep 17 00:00:00 2001 From: lisagoodrich <33230218+lisagoodrich@users.noreply.github.com> Date: Tue, 16 Nov 2021 14:55:57 -0700 Subject: [PATCH 214/770] Feature 1049 statistics list (#1271) * first attempt at a table #1049 * 2nd attempt, smaller, different formatting table #1049 * trying to get rid of git error message by adding this to the index #1049 * alignment #1049 * trying to text wrap in a table #1049 * attempting to create text wrapping in tables #1049 * removing role, trying to wrap text #1049 * removing html break, trying to wrap text #1049 * adding code to wrap text #1049 * removing code to wrap text #1049 * fixing naming typo #1049 * hitting returns in table #1049 * removing typo file, removing line breaks #1049 * attempting a simple table for text wrapping #1049 * attempting to bold header row in simple table #1049 * simple table line break attempt #1049 * simple table line break attempt #2 #1049 * simple table line break attempt #3 #1049 * trying another | #1049 * 2 trying another | #1049 * 3 trying another | #1049 * trying a blank line #1049 * trying reformating grid #1049 * 2 trying reformating grid #1049 * 3 trying reformating grid #1049 * 4 trying reformating grid #1049 * adding in one more table row #1049 * adding more text #1049 * trying glossary format #1049 * 2 trying glossary format #1049 * 3 trying glossary format #1049 * 4 trying glossary format #1049 * adding glossary format 2D objects #1049 * fixing glossary format 2D objects #1049 * adding glossary items #1049 * fixing glossary format #1049 * hopefully all examples are working #1049 * changing reference to tool * removing 2D object examples * removing the unwrapped table * adding ACC into the example * fixing formatting #1049 * fixing formatting take 2 #1049 * fixing formatting take 3 #1049 * bolding and language change #1049 * fixing spacing #1049 * fixing spacing again #1049 * changing title #1049 * trying superscript #1049 * superscript glossary #1049 * removing quotes #1049 * fixing spacing #1049 * Tara has decided to go with the glossary format. Removing table example. #1049 * Adding 2D objects #1049 * 2D objects fix #1049 * 2D objects fix attempt 2 #1049 * 2D objects fix attempt 3 #1049 * 2D objects fix attempt 4 #1049 * 2D objects fix attempt 5 #1049 * 2D objects fix attempt 7 #1049 * 2D objects fix attempt 8 #1049 * ABR added #1049 * adding remaining AB items to list #1049 * adding remaining ACC_ items to list #1049 * ADLAND & AFSS entries #1049 * A entries #1049 * fixing formatting #1049 * fixing formatting #2 #1049 * removing new entries to make it run #1049 * adding a couple back in #1049 * adding a couple more back in #1049 * tool key at the bottom #1049 * spacing changes #1049 * spacing changes another try #1049 * fixing spacing #1049 * fixing spacing attempt 2 #1049 * fixing spacing attempt 3 #1049 * through AREA items #1049 * AMODEL listed twice but it's not #1049 * AMODEL listed twice but it's not #2 #1049 * AMODEL added question marks so it will run #1049 * final A entries #1049 * naming the glossary statistics in hopes of not conflicting with original glossary #1049 * removing glossary statistics space #1049 * Testing multiple glossary names * Per #1067, working on attempts to have multiple glossaries with the same term * Per #1067, working on attempts to have multiple glossaries with the same term * Per #1067, working on attempts to have multiple glossaries with the same term * Per #1067, removing code with attempts at mutiple glossaries * Per #1067, adding back three question marks due to duplicate term * paring down list, adding a table for review * problems with table * still trying to get it to publish * still trying to get a second line in the table * still trying to get a second line in the table 2 * testing different formatting * testing different formatting * trying to comment out the glossary inner workings * formatting again * line breaks in table #1049 * adding in some more for an example #1049 * adding new entries in through AREA #1049 * fixing line breaks #1049 * fixing line breaks #1049 #2 * fixing line breaks and warning messages #1049 * fixing line breaks #1049 * adding AREA_RATIO through ASPECT_DIFF * fixing typo #1049 * fixing typo #1049 * fixing typo #1049 take 2 * adding AXIS_ANG to BCMSE #1049 * adding spacing #1049 * adding spacing removing commas #1049 * removing comma #1049 * BOUNDARY_DIST thru BSS_SMPL #1049 * BOUNDARY_DIST splitting across 2 lines #1049 * cleaning up typos #1049 * calibration thru centriod_dist #1049 * centriod_lat thru centroid_y #1049 * fixing spacing #1049 * removing test glossary #1049 * climo_mean thru crtk_err #1049 * fixing crtk_err spacing #1049 * fixing spacing #1049 * CSI to CURVATURE_Y #1049 * CURVATURE_X & Y spacing #1049 * DEV_CAT to DURATION_DIFF #1049 * EC_VALUE to F #1049 * F_RATE TO FBS #1049 * Fixing spacing #1049 * fcst_clus thru fcst_conv_radius #1049 * removing CTOP_PRS #1049 * fixing the order of tools for FBAR and FBIAS #1049 * fixing spacing #1049 * adding grid-stat to all point-stat entries #1049 * adding fixing spacing #1049 * adding fixing spacing take 2 #1049 * adding fixing spacing take 3 #1049 * adding fixing spacing take 4 #1049 * adding fixing spacing take 5 #1049 * adding fixing spacing take 6 #1049 * adding fixing spacing take 7 #1049 * fixing spacing with a period take 7 #1049 * first attempt fcst_ #1049 * fixing typos #1049 * thru FOBAR #1049 * thru end of F #1049 * g thru h #1049 * i thru intensity #s #1049 * fixing typos #1049 * capturing example for Julie #1049 * thru k #1049 * fixing typos #1049 * thru L #1049 * thru MG #1049 * thru N_ENS #1049 * thru all N #1049 * fixing FBIAS alignment #1049 * fixing ME and MSE alignment #1049 * fixing ME alignment take 2 #1049 * thru OBS_E #1049 * fixing alignment #1049 * fixing alignment n_thresh #1049 * thru OBS_thresh #1049 * thru O #1049 * fixing OOBAR formating #1049 * thru PR_CORR #1049 * commented lines out with line total info #1049 * commented lines out with line total info take 2 #1049 * thru R #1049 * thru SPEED #1049 * thru S #1049 * thru T #1049 * thru U #1049 * thru V #1049 * thru V #1049 * thru Z #1049 * Update statistics_list.rst Tara is testing editing in UI * Update statistics_list.rst Updates through the A's * Update statistics_list.rst Cleaning up the A's * Update statistics_list.rst Standardizing MODE and MTD entries * Update statistics_list.rst Updating B's and C's * Update statistics_list.rst Testing adding TC-Stat and TCST to an entry * Update statistics_list.rst Clean up of a few A-Cs and then update of Ds * Update statistics_list.rst A few clean-ups and Es * Update statistics_list.rst Halfway through Fs... * Update statistics_list.rst A little clean up and the rest of Fs * Update statistics_list.rst G, H, I, Ks * Update statistics_list.rst L, M, Ns * Update statistics_list.rst A little clean-up and Os * Update statistics_list.rst A little clean-up and Os * Update statistics_list.rst Rs * Update statistics_list.rst S and Ts * Update statistics_list.rst The rest of the list * Update statistics_list.rst Removed Attr from Stat Type thru E * Update statistics_list.rst Remove Attr from Statistics Type through Gs * Update statistics_list.rst Remove Attr from Statistic Type through Rs * Update statistics_list.rst Remove Attr from Stat Type to the end * Update statistics_list.rst Cleaned up some Line Type typos * Update statistics_list.rst Still more Attr cleanup Co-authored-by: Julie Prestopnik Co-authored-by: TaraJensen --- docs/Users_Guide/index.rst | 1 + docs/Users_Guide/statistics_list.rst | 2098 ++++++++++++++++++++++++++ docs/_templates/theme_override.css | 16 + 3 files changed, 2115 insertions(+) create mode 100644 docs/Users_Guide/statistics_list.rst create mode 100644 docs/_templates/theme_override.css diff --git a/docs/Users_Guide/index.rst b/docs/Users_Guide/index.rst index acd989ed11..a1ab31578a 100644 --- a/docs/Users_Guide/index.rst +++ b/docs/Users_Guide/index.rst @@ -85,6 +85,7 @@ is sponsored by NSF. quicksearch glossary references + statistics_list .. Indices and tables diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst new file mode 100644 index 0000000000..856f073ee1 --- /dev/null +++ b/docs/Users_Guide/statistics_list.rst @@ -0,0 +1,2098 @@ +****************************** +METplus Database of Statistics +****************************** + + +.. Number of characters per line: + Statistic Name - no more that 32 characters + METplus Name - no more than 17 characters + Statistic Type - no more than 19 characters + METplus Line Type - currently unlimited (approx 33 characters) + + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type + * - Accuracy + - ACC + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + MCTS :raw-html:`
` + NBRCTS :raw-html:`
` + MODE cts + * - Asymptotic Fractions Skill Score + - AFSS + - Neighborhood + - Grid-Stat + - NBRCNT + * - Along track error (nm) + - ALTK_ERR + - Continuous + - TC-Pairs :raw-html:`
` + TC-Stat + - TCMPR :raw-html:`
` + TCST + * - Difference between the axis :raw-html:`
` + angles of two objects (in degrees) + - ANGLE_DIFF + - Diagnostic + - MODE + - MODE + * - Anomaly Correlation :raw-html:`
` + including mean error + - ANOM_CORR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Series-Analysis :raw-html:`
` + Stat-Analysis + - CNT + * - Uncentered Anomaly :raw-html:`
` + Correlation excluding mean :raw-html:`
` + error + - ANOM_CORR :raw-html:`
` _UNCNTR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Series-Analysis :raw-html:`
` + Stat-Analysis + - CNT + * - Object area (in grid squares) + - AREA + - Diagnostic + - MODE :raw-html:`
` + MTD + - MODE obj + * - Forecast object area :raw-html:`
` + divided by the observation :raw-html:`
` + object area (unitless) + - AREA_RATIO + - Diagnostic + - MODE + - MODE obj + * - Area of the object :raw-html:`
` + that meet the object :raw-html:`
` + definition threshold :raw-html:`
` + criteria (in grid squares) + - AREA_THRESH + - Diagnostic + - MODE + - MODE obj + * - Absolute value of :raw-html:`
` + the difference :raw-html:`
` + between the aspect :raw-html:`
` + ratios of two objects :raw-html:`
` + (unitless) + - ASPECT_DIFF + - Diagnostic + - MODE + - MODE obj + * - Object axis angle :raw-html:`
` + (in degrees) + - AXIS_ANG + - Diagnostic + - MODE :raw-html:`
` + MTD + - MTD obj + * - Difference in spatial :raw-html:`
` + axis plane angles + - AXIS_DIFF + - Diagnostic + - MTD + - MTD obj + * - Baddeley’s Delta Metric + - BADDELEY + - Distance Map + - Grid-Stat + - DMAP + * - Bias Adjusted Gilbert :raw-html:`
` + Skill Score + - BAGSS + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Base Rate + - BASER + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Wavelet-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + ECLV :raw-html:`
` + MODE cts :raw-html:`
` + NBRCTCS :raw-html:`
` + PSTD :raw-html:`
` + PJC + * - Bias-corrected mean :raw-html:`
` + squared error + - BCMSE + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Ensemble-Stat + - CNT :raw-html:`
` + SSVAR + * - Minimum distance between :raw-html:`
` + the boundaries of two objects + - BOUNDARY :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Brier Score + - BRIER + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Climatological Brier Score + - BRIERCL + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Brier Skill Score relative :raw-html:`
` + to sample climatology + - BSS + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Brier Skill Score relative :raw-html:`
` + to external climatology + - BSS_SMPL + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Calibration when forecast :raw-html:`
` + is between the ith and :raw-html:`
` + i+1th probability :raw-html:`
` + thresholds (repeated) + - CALIBRATION :raw-html:`
` + _i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Total great circle distance :raw-html:`
` + travelled by the 2D spatial :raw-html:`
` + centroid over the lifetime :raw-html:`
` + of the 3D object + - CDIST :raw-html:`
` + _TRAVELLED + - Diagnostic + - MTD + - MTD 3D obj + * - Distance between two :raw-html:`
` + objects centroids :raw-html:`
` + (in grid units) + - CENTROID :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Latitude of centroid :raw-html:`
` + - CENTROID :raw-html:`
` + _LAT + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Longitude of centroid :raw-html:`
` + - CENTROID :raw-html:`
` + _LON + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Time coordinate of centroid + - CENTROID_T + - Diagnostic + - MTD + - MTD 3D obj + * - X coordinate of centroid :raw-html:`
` + - CENTROID_X + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Y coordinate of centroid :raw-html:`
` + - CENTROID_Y + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Climatological mean value + - CLIMO_MEAN + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Ensemble-Stat + - MPR :raw-html:`
` + ORANK + * - Climatological standard :raw-html:`
` + deviation value + - CLIMO_STDEV + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Ensemble-Stat + - MPR :raw-html:`
` + ORANK + * - Ratio of the difference :raw-html:`
` + between the area of an :raw-html:`
` + object and the area of :raw-html:`
` + its convex hull divided :raw-html:`
` + by the area of the :raw-html:`
` + complex hull (unitless) + - COMPLEXITY + - Diagnostic + - MODE + - MODE obj + * - Ratio of complexities of :raw-html:`
` + two objects defined as :raw-html:`
` + the lesser of the forecast :raw-html:`
` + complexity divided by the :raw-html:`
` + observation complexity or :raw-html:`
` + its reciprocal (unitless) + - COMPLEXITY :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Minimum distance between :raw-html:`
` + the convex hulls of two :raw-html:`
` + objects (in grid units) + - CONVEX_HULL :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Continuous Ranked :raw-html:`
` + Probability Score :raw-html:`
` + (normal dist.) + - CRPS + - Ensemble + - Ensemble-Stat + - ECNT + * - Continuous Ranked :raw-html:`
` + Probability Score :raw-html:`
` + (empirical dist.) + - CRPS_EMP + - Ensemble + - Ensemble-Stat + - ECNT + * - Climatological Continuous :raw-html:`
` + Ranked Probability Score :raw-html:`
` + (normal dist.) + - CRPSCL + - Ensemble + - Ensemble-Stat + - ECNT + * - Climatological Continuous :raw-html:`
` + Ranked Probability Score :raw-html:`
` + (empirical dist.) + - CRPSCL_EMP + - Ensemble + - Ensemble-Stat + - ECNT + * - Continuous Ranked :raw-html:`
` + Probability Skill Score :raw-html:`
` + (normal dist.) + - CRPSS + - Ensemble + - Ensemble-Stat + - ECNT + * - Continuous Ranked :raw-html:`
` + Probability Skill Score :raw-html:`
` + (empirical dist.) + - CRPSS_EMP + - Ensemble + - Ensemble-Stat + - ECNT + * - Cross track error (nm) + - CRTK_ERR + - Continuous + - TC-Pairs :raw-html:`
` + TC-Stat + - TCMPR :raw-html:`
` + TCST + * - Critical Success Index + - CSI + - Categorical + - Point-Stat :raw-html:`
` + MODE cts :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + MODE :raw-html:`
` + MBRCTCS + * - Radius of curvature + - CURVATURE + - Diagnostic + - MODE + - MODE obj + * - Ratio of the curvature + - CURVATURE :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE :raw-html:`
` + _X + - Diagnostic + - MODE + - MODE obj + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE :raw-html:`
` + _Y + - Diagnostic + - MODE + - MODE obj + * - Absolute value of :raw-html:`
` + DIR_ERR (see below) + - DIR_ABSERR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Signed angle between :raw-html:`
` + the directions of the :raw-html:`
` + average forecast and :raw-html:`
` + observed wind vectors + - DIR_ERR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Difference in object :raw-html:`
` + direction of movement + - DIRECTION :raw-html:`
` + _DIFF + - Diagnostic + - MTD + - MTD 3D obj + * - Difference in the :raw-html:`
` + lifetimes of the :raw-html:`
` + two objects + - DURATION :raw-html:`
` + _DIFF + - Diagnostic + - MTD + - MTD 3D obj + * - Expected correct rate :raw-html:`
` + used for MCTS HSS_EC + - EC_VALUE + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTC + * - Extreme Dependency Index + - EDI + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Extreme Dependency Score + - EDS + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Mean of absolute value :raw-html:`
` + of forecast minus :raw-html:`
` + observed gradients + - EGBAR + - Continuous + - Grid-Stat + - GRAD + * - Object end time + - END_TIME + - Diagnostic + - MTD + - MTD 3D obj + * - Difference in object :raw-html:`
` + ending time steps + - END_TIME :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - The unperturbed :raw-html:`
` + ensemble mean value + - ENS_MEAN + - Ensemble + - Ensemble-Stat + - ORANK + * - The PERTURBED ensemble :raw-html:`
` + mean (e.g. with :raw-html:`
` + Observation Error). + - ENS_MEAN :raw-html:`
` + _OERR + - Ensemble + - Ensemble-Stat + - ORANK + * - Standard deviation of :raw-html:`
` + the error + - ESTDEV + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Ensemble-Stat + - CNT :raw-html:`
` + SSVAR + * - Forecast rate/event :raw-html:`
` + frequency + - F_RATE + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - FHO :raw-html:`
` + NBRCNT + * - Mean forecast wind speed + - F_SPEED :raw-html:`
` + _BAR + - Continous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean Forecast Anomaly + - FABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - False alarm ratio + - FAR + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + MODE :raw-html:`
` + NBRCTCS + * - Forecast mean + - FBAR + - Categorical + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + SL1L2 :raw-html:`
` + VCNT + * - Length (speed) of the :raw-html:`
` + average forecast :raw-html:`
` + wind vector + - FBAR :raw-html:`
` + _SPEED + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Frequency Bias + - FBIAS + - Categorical + - Wavelet-Stat :raw-html:`
` + MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + - ISC :raw-html:`
` + MODE :raw-html:`
` + CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + DMAP + * - Fractions Brier Score + - FBS + - Continuous + - Grid-Stat + - NBRCNT + * - Number of forecast :raw-html:`
` + clusters + - fcst_clus + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the cluster forecast :raw-html:`
` + objects + - fcst_clus :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Latitude + - fcst_clus :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Longitude + - fcst_clus :raw-html:`
` + _hull _lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Cluster Convex Hull Points + - fcst_clus :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Starting Index + - fcst_clus :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_clus :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_clus :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Forecast Object Raw :raw-html:`
` + Values + - fcst_obj :raw-html:`
` + _raw + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + forecast objects + - fcst_simp + - Diagnostic + - MODE + - MODE obj + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of all of the simple :raw-html:`
` + forecast objects + - fcst_simp :raw-html:`
` + _bdy + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Latitude + - fcst_simp :raw-html:`
` + _bdy_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Longitude + - fcst_simp :raw-html:`
` + _bdy_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Simple Boundary Points + - fcst_simp :raw-html:`
` + _bdy_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Starting Index + - fcst_simp :raw-html:`
` + _bdy_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary X-Coordinate + - fcst_simp :raw-html:`
` + _bdy_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Y-Coordinate + - fcst_simp :raw-html:`
` + _bdy_y + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the simple forecast :raw-html:`
` + objects + - fcst_simp :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Latitude + - fcst_simp :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Longitude + - fcst_simp :raw-html:`
` + _hull_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Simple Convex Hull Points + - fcst_simp :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Starting Index + - fcst_simp :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_simp :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_simp :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst :raw-html:`
` + _thresh :raw-html:`
` + _length + - Diagnostic + - MODE + - MODE obj + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst_thresh :raw-html:`
` + _length + - Diagnostic + - MODE + - MODE obj + * - Direction of the average :raw-html:`
` + forecast wind vector + - FDIR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Forecast energy squared :raw-html:`
` + for this scale + - FENERGY + - + - Wavelet-Stat + - ISC + * - Mean Forecast Anomaly Squared + - FFABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average of forecast :raw-html:`
` + squared. + - FFBAR + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + SL1L2 + * - Mean of absolute value :raw-html:`
` + of forecast gradients + - FGBAR + - + - Grid-Stat + - GRAD + * - Ratio of forecast and :raw-html:`
` + observed gradients + - FGOG_RATIO + - + - Grid-Stat + - GRAD + * - Count of events in :raw-html:`
` + forecast category i and :raw-html:`
` + observation category j + - Fi_Oj + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTC + * - Forecast mean + - FMEAN + - Continuous + - MODE :raw-html:`
` + Grid-Stat :raw-html:`
` + Point-Stat + - MODE :raw-html:`
` + NBRCTCS :raw-html:`
` + CTS + * - Number of forecast no :raw-html:`
` + and observation no + - FN_ON + - Categorical + - MODE :raw-html:`
` + Grid-Stat :raw-html:`
` + Point-Stat + - MODE :raw-html:`
` + NBRCTC :raw-html:`
` + CTC + * - Number of forecast no :raw-html:`
` + and observation yes + - FN_OY + - Categorical + - MODE :raw-html:`
` + Grid-Stat :raw-html:`
` + Point-Stat + - MODE :raw-html:`
` + NBRCTC :raw-html:`
` + CTC + * - Attributes for pairs of :raw-html:`
` + simple forecast and :raw-html:`
` + observation objects + - FNNN_ONNN + - Categorical + - MODE + - MODE obj + * - Average product of :raw-html:`
` + forecast-climo and :raw-html:`
` + observation-climo :raw-html:`
` + / Mean(f-c)*(o-c) + - FOABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average product of :raw-html:`
` + forecast and observation :raw-html:`
` + / Mean(f*o) + - FOBAR + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + SL1L2 + * - Pratt’s Figure of Merit :raw-html:`
` + from observation to :raw-html:`
` + forecast + - FOM_FO + - Diagnostic + - Grid-Stat + - DMAP + * - Maximum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MAX + - Diagnostic + - Grid-Stat + - DMAP + * - Mean of FOM_FO :raw-html:`
` + and FOM_OF :raw-html:`
` + - FOM_MEAN + - Diagnostic + - Grid-Stat + - DMAP + * - Minimum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MIN + - Diagnostic + - Grid-Stat + - DMAP + * - Pratt’s Figure of Merit :raw-html:`
` + from forecast to :raw-html:`
` + observation + - FOM_OF + - Diagnostic + - Grid-Stat + - DMAP + * - Number of tied forecast :raw-html:`
` + ranks used in computing :raw-html:`
` + Kendall’s tau statistic + - FRANK_TIES + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Root mean square forecast :raw-html:`
` + wind speed + - FS_RMS + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Fractions Skill Score :raw-html:`
` + - FSS + - Neighborhood + - Grid-Stat + - NBRCNT + * - Standard deviation of the :raw-html:`
` + error + - FSTDEV + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + VCNT + * - Number of forecast events + - FY + - Categorical + - Grid-Stat + - DMAP + * - Number of forecast yes :raw-html:`
` + and observation no + - FY_ON + - Categorical + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + CTC :raw-html:`
` + NBRCTC + * - Number of forecast yes :raw-html:`
` + and observation yes + - FY_OY + - Categorical + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + CTC :raw-html:`
` + NBRCTC + * - Distance between the :raw-html:`
` + forecast and Best track :raw-html:`
` + genesis events (km) + - GEN_DIST + - Diagnostic + - TC-Gen + - GENMPR + * - Forecast minus Best track :raw-html:`
` + genesis time in HHMMSS :raw-html:`
` + format + - GEN_TDIFF + - Diagnostic + - TC-Gen + - GENMPR + * - Gerrity Score and :raw-html:`
` + bootstrap confidence limits + - GER + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTS + * - Gilbert Skill Score + - GSS + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + MODE + * - Hit rate + - H_RATE + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - FHO + * - Hausdorff Distance + - HAUSDORFF + - Diagnostic + - Grid-Stat + - DMAP + * - Hanssen and Kuipers :raw-html:`
` + Discriminant + - HK + - Categorical + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE cts :raw-html:`
` + MCTS :raw-html:`
` + CTS :raw-html:`
` + NBRCTS + * - Heidke Skill Score + - HSS + - Categorical + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE cts :raw-html:`
` + MCTS :raw-html:`
` + CTS :raw-html:`
` + NBRCTS + * - Heidke Skill Score :raw-html:`
` + user-specific expected :raw-html:`
` + correct + - HSS_EC + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTS + * - Ignorance Score + - IGN + - Ensemble + - Ensemble-Stat + - ECNT + * - Best track genesis minus :raw-html:`
` + forecast initialization :raw-html:`
` + time in HHMMSS format + - INIT_TDIFF + - Diagnostic + - TC-Gen + - GENMPR + * - 10th, 25th, 50th, 75th, :raw-html:`
` + 90th, and user-specified :raw-html:`
` + percentiles of :raw-html:`
` + intensity of the raw :raw-html:`
` + field within the :raw-html:`
` + object or time slice + - INTENSITY :raw-html:`
` + _10, _25, :raw-html:`
` + _50, _75, :raw-html:`
` + _90, _NN + - Diagnostic + - MODE + - MODE obj + * - Sum of the intensities of :raw-html:`
` + the raw field within the :raw-html:`
` + object (variable units) + - INTENSITY :raw-html:`
` + _SUM + - Diagnostics + - MODE + - MODE obj + * - Total interest for this :raw-html:`
` + object pair + - INTEREST + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 3D obj :raw-html:`
` + MODE obj + * - Intersection area of two :raw-html:`
` + objects (in grid squares) + - INTERSECT :raw-html:`
` + ION_AREA + - Diagnostic + - MODE + - MODE obj + * - Ratio of intersection area :raw-html:`
` + to the lesser of the :raw-html:`
` + forecast and observation :raw-html:`
` + object areas (unitless) + - INTERSECT :raw-html:`
` + ION_OVER :raw-html:`
` + _AREA + - Diagnostic + - MODE + - MODE obj + * - “Volume” of object :raw-html:`
` + intersection + - INTERSECT :raw-html:`
` + ION_VOLUME + - Diagnostic + - MTD + - MTD 3D obj + * - Interquartile Range :raw-html:`
` + - IQR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - The intensity scale :raw-html:`
` + skill score + - ISC + - + - Wavelet-Stat + - ISC + * - The scale at which all :raw-html:`
` + information following :raw-html:`
` + applies + - ISCALE + - + - Wavelet-Stat + - ISC + * - Kendall’s tau statistic + - KT_CORR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Dimension of the latitude + - LAT + - Diagnostic + - MODE + - MODE obj + * - Length of the :raw-html:`
` + enclosing rectangle + - LENGTH + - Diagnostic + - MODE + - MODE obj + * - Likelihood when forecast :raw-html:`
` + is between the ith and :raw-html:`
` + i+1th probability :raw-html:`
` + thresholds repeated + - LIKELIHOOD :raw-html:`
` + _i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Logarithm of the Odds Ratio + - LODDS + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Dimension of the longitude + - LON + - Diagnostic + - MODE + - MODE obj + * - The Median Absolute :raw-html:`
` + Deviation + - MAD + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Mean absolute error + - MAE + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT :raw-html:`
` + SAL1L2 :raw-html:`
` + SL1L2 + * - Magnitude & :raw-html:`
` + Multiplicative bias + - MBIAS + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT + * - The Mean Error + - ME + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - ECNT :raw-html:`
` + SSVAR :raw-html:`
` + CNT + * - The Mean Error of the :raw-html:`
` + PERTURBED ensemble mean + - ME_OERR + - Continuous + - Ensemble-Stat + - ECNT + * - The square of the :raw-html:`
` + mean error (bias) + - ME2 + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Mean-error Distance from :raw-html:`
` + observation to forecast + - MED_FO + - Distance + - Grid-Stat + - DMAP + * - Maximum of MED_FO :raw-html:`
` + and MED_OF + - MED_MAX + - Distance + - Grid-Stat + - DMAP + * - Mean of MED_FO :raw-html:`
` + and MED_OF + - MED_MEAN + - Distance + - Grid-Stat + - DMAP + * - Minimum of MED_FO :raw-html:`
` + and MED_OF + - MED_MIN + - Distance + - Grid-Stat + - DMAP + * - Mean-error Distance from :raw-html:`
` + forecast to observation + - MED_OF + - Distance + - Grid-Stat + - DMAP + * - Mean of maximum of :raw-html:`
` + absolute values of :raw-html:`
` + forecast and observed :raw-html:`
` + gradients + - MGBAR + - + - Grid-Stat + - GRAD + * - Mean squared error + - MSE + - Continuous + - Ensemble-Stat :raw-html:`
` + Wavelet-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + ISC :raw-html:`
` + CNT :raw-html:`
` + * - The mean squared error :raw-html:`
` + skill + - MSESS + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Mean squared length of :raw-html:`
` + the vector difference :raw-html:`
` + between the forecast :raw-html:`
` + and observed winds + - MSVE + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Dimension of the :raw-html:`
` + contingency table & the :raw-html:`
` + total number of :raw-html:`
` + categories in each :raw-html:`
` + dimension + - N_CAT + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - MCTC :raw-html:`
` + MCTS + * - Number of cluster objects + - N_CLUS + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + forecast objects + - N_FCST_SIMP + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + observation objects + - N_OBS_SIMP + - Diagnostic + - MODE + - MODE obj + * - Observation rate + - O_RATE + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - NBRCNT :raw-html:`
` + FHO + * - Mean observed wind speed + - O_SPEED_BAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean Observation Anomaly + - OABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average observed value :raw-html:`
` + - OBAR + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` . + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + SL1L2 :raw-html:`
` + VCNT + * - Length (speed) of the :raw-html:`
` + average observed wind :raw-html:`
` + vector + - OBAR_SPEED + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Number of observed :raw-html:`
` + clusters + - obs_clus + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all of :raw-html:`
` + the cluster observation :raw-html:`
` + objects + - obs_clus :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Latitude + - obs_clus :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Longitude + - obs_clus :raw-html:`
` + _hull_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Cluster Convex Hull Points + - obs_clus :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Starting Index + - obs_clus :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - obs_clus :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - obs_clus :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + observation objects + - obs_simp + - Diagnostic + - MODE + - MODE obj + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of the simple observation :raw-html:`
` + objects + - obs_simp :raw-html:`
` + _bdy + - Diagnostic + - MODE + - MODE obj + * - Observation Simple :raw-html:`
` + Boundary Point Latitude + - obs_simp :raw-html:`
` + _bdy_lat + - Diagnostic + - MODE + - MODE obj + * - Observation Simple :raw-html:`
` + Boundary Point Longitude + - obs_simp :raw-html:`
` + _bdy_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Simple Boundary Points + - obs_simp :raw-html:`
` + _bdy_npts + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of the :raw-html:`
` + simple observation objects + - obs_simp :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Simple Convex Hull Points + - obs_simp :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Odds Ratio + - ODDS + - Categorical + - MODE :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + CTS :raw-html:`
` + NBRCTS + * - Direction of the average :raw-html:`
` + observed wind vector + - ODIR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Observed energy squared :raw-html:`
` + for this scale + - OENERGY + - + - Wavelet-Stat + - ISC + * - Mean of absolute value :raw-html:`
` + of observed gradients + - OGBAR + - + - Grid-Stat + - GRAD + * - Number of observation :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds + - ON_i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PTC + * - Number of observation :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds + - ON_TP_i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Mean Squared :raw-html:`
` + Observation Anomaly + - OOABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - SAL1L2 + * - Average of observation :raw-html:`
` + squared + - OOBAR + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + SL1L2 :raw-html:`
` + * - Number of tied observation :raw-html:`
` + ranks used in computing :raw-html:`
` + Kendall’s tau statistic + - ORANK_TIES + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Odds Ratio Skill Score + - ORSS + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Root mean square observed :raw-html:`
` + wind speed + - OS_RMS + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Standard deviation :raw-html:`
` + of observations + - OSTDEV + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + VCNT + * - Number of observation :raw-html:`
` + events + - OY + - Categorical + - Grid-Stat + - DMAP + * - Number of observation yes :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds + - OY_i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PTC + * - Number of observation yes :raw-html:`
` + when forecast is between :raw-html:`
` + the ith and i+1th :raw-html:`
` + probability thresholds :raw-html:`
` + as a proportion of the :raw-html:`
` + total OY (repeated) + - OY_TP_i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Ratio of the nth percentile :raw-html:`
` + (INTENSITY_NN column) of :raw-html:`
` + intensity of the two :raw-html:`
` + objects + - PERCENTILE :raw-html:`
` + _INTENSITY :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Probability Integral :raw-html:`
` + Transform + - PIT + - Ensemble + - Ensemble-Stat + - ORANK + * - Probability of false :raw-html:`
` + detection + - PODF + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS + * - Probability of detecting no + - PODN + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + MODE + * - Probability of detecting :raw-html:`
` + yes + - PODY + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + MODE + - CTS :raw-html:`
` + NBRCTCS :raw-html:`
` + MODE + * - Probability of detecting :raw-html:`
` + yes when forecast is :raw-html:`
` + greater than the ith :raw-html:`
` + probability thresholds + - PODY_i + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - PRC + * - Probability of false :raw-html:`
` + detection + - POFD + - Categorical + - MODE :raw-html:`
` + Grid-Stat + - MODE :raw-html:`
` + NBRCTCS + * - Probability of false :raw-html:`
` + detection when forecast is :raw-html:`
` + greater than the ith :raw-html:`
` + probability thresholds + - POFD_i + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - PRC + * - Pearson correlation :raw-html:`
` + coefficient + - PR_CORR + - Continuous + - Ensemble-Stat :raw-html:`
` + Point-Stat :raw-html:`
` + Grid-Stat + - SSVAR :raw-html:`
` + CNT :raw-html:`
` + * - Rank of the observation + - RANK + - Ensemble + - Ensemble-Stat + - ORANK + * - Count of observations :raw-html:`
` + with the i-th rank + - RANK_i + - Ensemble + - Ensemble-Stat + - RHIST + * - Number of ranks used in :raw-html:`
` + computing Kendall’s tau :raw-html:`
` + statistic + - RANKS + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Refinement when forecast :raw-html:`
` + is between the ith and :raw-html:`
` + i+1th probability :raw-html:`
` + thresholds (repeated) + - REFINEMENT :raw-html:`
` + _i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PJC + * - Reliability + - RELIABILITY + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Number of times the i-th :raw-html:`
` + ensemble member’s value :raw-html:`
` + was closest to the :raw-html:`
` + observation (repeated). :raw-html:`
` + When n members tie, :raw-html:`
` + 1/n is assigned to each :raw-html:`
` + member. + - RELP_i + - Ensemble + - Ensemble-Stat + - RELP + * - Resolution + - RESOLUTION + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Root mean squared error + - RMSE + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat :raw-html:`
` + Ensemble-Stat :raw-html:`
` + - CNT :raw-html:`
` + ECNT :raw-html:`
` + SSVAR + * - Root Mean Square Error :raw-html:`
` + of the PERTURBED :raw-html:`
` + ensemble mean + - RMSE_OERR + - Continuous + - Ensemble-Stat + - ECNT + * - Root mean squared forecast :raw-html:`
` + anomaly + - RMSFA + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Root mean squared :raw-html:`
` + observation anomaly + - RMSOA + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Square root of MSVE + - RMSVE + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Area under the receiver :raw-html:`
` + operating characteristic :raw-html:`
` + curve + - ROC_AUC + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Mean of the Brier Scores :raw-html:`
` + for each RPS threshold + - RPS + - Ensemble + - Ensemble-Stat + - RPS + * - Mean of the reliabilities :raw-html:`
` + for each RPS threshold + - RPS_REL + - Ensemble + - Ensemble-Stat + - RPS + * - Mean of the resolutions :raw-html:`
` + for each RPS threshold + - RPS_RES + - Ensemble + - Ensemble-Stat + - RPS + * - Mean of the uncertainties :raw-html:`
` + for each RPS threshold + - RPS_UNC + - Ensemble + - Ensemble-Stat + - RPS + * - Ranked Probability Skill :raw-html:`
` + Score relative to external :raw-html:`
` + climatology + - RPSS + - Ensemble + - Ensemble-Stat + - RPS + * - Ranked Probability Skill :raw-html:`
` + Score relative to sample :raw-html:`
` + climatology + - RPSS_SMPL + - Ensemble + - Ensemble-Stat + - RPS + * - S1 score + - S1 + - Continuous + - Grid-Stat + - GRAD + * - S1 score with respect to :raw-html:`
` + observed gradient + - S1_OG + - Continuous + - Grid-Stat + - GRAD + * - Symmetric Extremal :raw-html:`
` + Dependency Index + - SEDI + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Symmetric Extreme :raw-html:`
` + Dependency Score + - SEDS + - Categorical + - Point-Stat :raw-html:`
` + Grid-Stat + - CTS :raw-html:`
` + NBRCTS + * - Scatter Index + - SI + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Spearman’s rank :raw-html:`
` + correlation coefficient + - SP_CORR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - CNT + * - Spatial distance between :raw-html:`
` + (𝑥,𝑦)(x,y) coordinates of :raw-html:`
` + object spacetime centroid + - SPACE :raw-html:`
` + _CENTROID :raw-html:`
` + _DIST + - Diagnostics + - MTD + - MTD 3D obs + * - Absolute value of SPEED_ERR + - SPEED :raw-html:`
` + _ABSERR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Difference in object speeds + - SPEED_DELTA + - Diagnostics + - MTD + - MTD 3D obs + * - Difference between the :raw-html:`
` + length of the average :raw-html:`
` + forecast wind vector and :raw-html:`
` + the average observed wind :raw-html:`
` + vector (in the sense F - O) + - SPEED_ERR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Standard deviation :raw-html:`
` + of the mean of the :raw-html:`
` + UNPERTURBED ensemble + - SPREAD + - Ensemble + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK + * - Standard deviation :raw-html:`
` + of the mean of the :raw-html:`
` + PERTURBED ensemble + - SPREAD_OERR + - Ensemble + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK + * - Standard Deviation :raw-html:`
` + of unperturbed ensemble :raw-html:`
` + variance and the :raw-html:`
` + observation error variance + - SPREAD_PLUS :raw-html:`
` + _OERR + - Ensemble + - Ensemble-Stat + - ECNT :raw-html:`
` + ORANK + * - Difference in object :raw-html:`
` + starting time steps + - START_TIME :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Symmetric difference of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - SYMMETRIC :raw-html:`
` + _DIFF + - Diagnostics + - MODE + - MODE obj + * - Difference in t index of :raw-html:`
` + object spacetime centroid + - TIME :raw-html:`
` + _CENTROID :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Track error of adeck :raw-html:`
` + relative to bdeck (nm) + - TK_ERR + - Continuous + - TC-Pairs + - PROBRIRW + * - Track error of adeck :raw-html:`
` + relative to bdeck (nm) + - TK_ERR + - Continuous + - TC-Pairs + - TCMPR + * - Mean U-component :raw-html:`
` + Forecast Anomaly + - UFABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean U-component + - UFBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Uniform Fractions Skill :raw-html:`
` + Score + - UFSS + - Neighborhood + - Grid-Stat + - NBRCNT + * - Variability of :raw-html:`
` + Observations + - UNCERTAINTY + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - PSTD + * - Union area of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - UNION_AREA + - Diagnostic + - MODE + - MODE obj + * - Mean U-component :raw-html:`
` + Observation Anomaly + - UOABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean U-component :raw-html:`
` + Observation + - UOBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean U-component :raw-html:`
` + Squared :raw-html:`
` + Forecast Anomaly :raw-html:`
` + plus Squared :raw-html:`
` + Observation :raw-html:`
` + Anomaly + - UVFFABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean U-component :raw-html:`
` + Squared :raw-html:`
` + Forecast :raw-html:`
` + plus Squared :raw-html:`
` + Observation + - UVFFBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean((uf-uc)*(uo-uc)+ :raw-html:`
` + (vf-vc)*(vo-vc)) + - UVFOABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uf*uo+vf*vo) + - UVFOBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean((uo-uc)²+(vo-vc)²) + - UVOOABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(uo²+vo²) + - UVOOBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Economic value of the :raw-html:`
` + base rate + - VALUE_BASER + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - ECLV + * - Relative value for the :raw-html:`
` + ith Cost/Loss ratio + - VALUE_i + - Probability + - Point-Stat :raw-html:`
` + Grid-Stat + - ECLV + * - Maximum variance + - VAR_MAX + - Ensemble + - Ensemble-Stat + - SSVAR + * - Average variance + - VAR_MEAN + - Ensemble + - Ensemble-Stat + - SSVAR + * - Minimum variance + - VAR_MIN + - Ensemble + - Ensemble-Stat + - SSVAR + * - Direction of the vector :raw-html:`
` + difference between the :raw-html:`
` + average forecast and :raw-html:`
` + average wind vectors + - VDIFF_DIR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Length (speed) of the :raw-html:`
` + vector difference between :raw-html:`
` + the average forecast and :raw-html:`
` + average observed wind :raw-html:`
` + vectors + - VDIFF_SPEED + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VCNT + * - Mean(vf-vc) + - VFABAR + - Continous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(vf) + - VFBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Mean(vo-vc) + - VOABAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VAL1L2 + * - Mean(vo) + - VOBAR + - Continuous + - Point-Stat :raw-html:`
` + Grid-Stat + - VL1L2 + * - Integer count of the :raw-html:`
` + number of 3D “cells” :raw-html:`
` + in an object + - VOLUME + - Diagnostic + - MTD + - MTD 3D obj + * - Forecast object volume :raw-html:`
` + divided by observation :raw-html:`
` + object volume + - VOLUME :raw-html:`
` + _RATIO + - Diagnostic + - MTD + - MTD 3D obj + * - Width of the enclosing :raw-html:`
` + rectangle (in grid units) + - WIDTH + - Diagnostic + - MODE + - MODE obj + * - X component of :raw-html:`
` + object velocity + - X_DOT + - Diagnostic + - MTD + - MTD 3D obj + * - X component position :raw-html:`
` + error (nm) + - X_ERR + - Diagnostic + - TC-Pairs + - PROBRIRW + * - X component position :raw-html:`
` + error (nm) + - X_ERR + - Diagnostic + - TC-Pairs + - TCMPR + * - y component of :raw-html:`
` + object velocity + - Y_DOT + - Diagnostic + - MTD + - MTD 3D obj + * - Y component position :raw-html:`
` + error (nm) + - Y_ERR + - Diagnostic + - TC-Pairs + - PROBRIRW :raw-html:`
` + TCMPR + * - Zhu’s Measure from :raw-html:`
` + observation to forecast + - ZHU_FO + - Diagnostic + - Grid-Stat + - DMAP + * - Maximum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MAX + - Diagnostic + - Grid-Stat + - DMAP + * - Mean of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MEAN + - Diagnostic + - Grid-Stat + - DMAP + * - Minimum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MIN + - Diagnostic + - Grid-Stat + - DMAP + * - Zhu’s Measure from :raw-html:`
` + forecast to observation + - ZHU_OF + - Diagnostic + - Grid-Stat + - DMAP diff --git a/docs/_templates/theme_override.css b/docs/_templates/theme_override.css new file mode 100644 index 0000000000..ab16bba14c --- /dev/null +++ b/docs/_templates/theme_override.css @@ -0,0 +1,16 @@ +/* Fix missing line-wrapping with Sphinx-RTD theme */ +/* https://github.com/platformio/platformio-docs/issues/5 */ + +/* override table width restrictions */ +@media screen and (min-width: 767px) { + + .wy-table-responsive table td { + /* !important prevents the common CSS stylesheets from overriding + this as on RTD they are loaded after this stylesheet */ + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + } +} \ No newline at end of file From 5ecb8df37f09b0ad46458ec718808587cd3dda37 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 15:43:07 -0700 Subject: [PATCH 215/770] creating a separate list for diagnostics, in progesss --- docs/Users_Guide/diagnostics_list.rst | 215 ++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 docs/Users_Guide/diagnostics_list.rst diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst new file mode 100644 index 0000000000..057801ec25 --- /dev/null +++ b/docs/Users_Guide/diagnostics_list.rst @@ -0,0 +1,215 @@ +******************** +Diagnostics Database +******************** + + +.. Number of characters per line: + Statistic Name - no more that 32 characters + METplus Name - no more than 17 characters + Statistic Type - no more than 19 characters + METplus Line Type - currently unlimited (approx 33 characters) + + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type + * - Difference between the axis :raw-html:`
` + angles of two objects (in degrees) + - ANGLE_DIFF + - Diagnostic + - MODE + - MODE + * - Object area (in grid squares) + - AREA + - Diagnostic + - MODE :raw-html:`
` + MTD + - MODE obj + * - Forecast object area :raw-html:`
` + divided by the observation :raw-html:`
` + object area (unitless) + - AREA_RATIO + - Diagnostic + - MODE + - MODE obj + * - Area of the object :raw-html:`
` + that meet the object :raw-html:`
` + definition threshold :raw-html:`
` + criteria (in grid squares) + - AREA_THRESH + - Diagnostic + - MODE + - MODE obj + * - Absolute value of :raw-html:`
` + the difference :raw-html:`
` + between the aspect :raw-html:`
` + ratios of two objects :raw-html:`
` + (unitless) + - ASPECT_DIFF + - Diagnostic + - MODE + - MODE obj + * - Object axis angle :raw-html:`
` + (in degrees) + - AXIS_ANG + - Diagnostic + - MODE :raw-html:`
` + MTD + - MTD obj + * - Difference in spatial :raw-html:`
` + axis plane angles + - AXIS_DIFF + - Diagnostic + - MTD + - MTD obj + * - Minimum distance between :raw-html:`
` + the boundaries of two objects + - BOUNDARY :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Total great circle distance :raw-html:`
` + travelled by the 2D spatial :raw-html:`
` + centroid over the lifetime :raw-html:`
` + of the 3D object + - CDIST :raw-html:`
` + _TRAVELLED + - Diagnostic + - MTD + - MTD 3D obj + * - Distance between two :raw-html:`
` + objects centroids :raw-html:`
` + (in grid units) + - CENTROID :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Latitude of centroid :raw-html:`
` + - CENTROID :raw-html:`
` + _LAT + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Longitude of centroid :raw-html:`
` + - CENTROID :raw-html:`
` + _LON + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Time coordinate of centroid + - CENTROID_T + - Diagnostic + - MTD + - MTD 3D obj + * - X coordinate of centroid :raw-html:`
` + - CENTROID_X + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Y coordinate of centroid :raw-html:`
` + - CENTROID_Y + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Ratio of the difference :raw-html:`
` + between the area of an :raw-html:`
` + object and the area of :raw-html:`
` + its convex hull divided :raw-html:`
` + by the area of the :raw-html:`
` + complex hull (unitless) + - COMPLEXITY + - Diagnostic + - MODE + - MODE obj + * - Ratio of complexities of :raw-html:`
` + two objects defined as :raw-html:`
` + the lesser of the forecast :raw-html:`
` + complexity divided by the :raw-html:`
` + observation complexity or :raw-html:`
` + its reciprocal (unitless) + - COMPLEXITY :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Minimum distance between :raw-html:`
` + the convex hulls of two :raw-html:`
` + objects (in grid units) + - CONVEX_HULL :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Radius of curvature + - CURVATURE + - Diagnostic + - MODE + - MODE obj + * - Ratio of the curvature + - CURVATURE :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE :raw-html:`
` + _X + - Diagnostic + - MODE + - MODE obj + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE :raw-html:`
` + _Y + - Diagnostic + - MODE + - MODE obj + * - Difference in object :raw-html:`
` + direction of movement + - DIRECTION :raw-html:`
` + _DIFF + - Diagnostic + - MTD + - MTD 3D obj + * - Difference in the :raw-html:`
` + lifetimes of the :raw-html:`
` + two objects + - DURATION :raw-html:`
` + _DIFF + - Diagnostic + - MTD + - MTD 3D obj + * - Object end time + - END_TIME + - Diagnostic + - MTD + - MTD 3D obj + * - Difference in object :raw-html:`
` + ending time steps + - END_TIME :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj From 5977d6b48bf2045254aa368658f20d71c9a85806 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 15:45:28 -0700 Subject: [PATCH 216/770] removing diagnostics from statistics, in progess #1049 --- docs/Users_Guide/statistics_list.rst | 192 +-------------------------- 1 file changed, 2 insertions(+), 190 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 856f073ee1..ffb34160fc 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -46,12 +46,6 @@ METplus Database of Statistics TC-Stat - TCMPR :raw-html:`
` TCST - * - Difference between the axis :raw-html:`
` - angles of two objects (in degrees) - - ANGLE_DIFF - - Diagnostic - - MODE - - MODE * - Anomaly Correlation :raw-html:`
` including mean error - ANOM_CORR @@ -64,56 +58,14 @@ METplus Database of Statistics * - Uncentered Anomaly :raw-html:`
` Correlation excluding mean :raw-html:`
` error - - ANOM_CORR :raw-html:`
` _UNCNTR + - ANOM_CORR :raw-html:`
` + _UNCNTR - Continuous - Point-Stat :raw-html:`
` Grid-Stat :raw-html:`
` Series-Analysis :raw-html:`
` Stat-Analysis - CNT - * - Object area (in grid squares) - - AREA - - Diagnostic - - MODE :raw-html:`
` - MTD - - MODE obj - * - Forecast object area :raw-html:`
` - divided by the observation :raw-html:`
` - object area (unitless) - - AREA_RATIO - - Diagnostic - - MODE - - MODE obj - * - Area of the object :raw-html:`
` - that meet the object :raw-html:`
` - definition threshold :raw-html:`
` - criteria (in grid squares) - - AREA_THRESH - - Diagnostic - - MODE - - MODE obj - * - Absolute value of :raw-html:`
` - the difference :raw-html:`
` - between the aspect :raw-html:`
` - ratios of two objects :raw-html:`
` - (unitless) - - ASPECT_DIFF - - Diagnostic - - MODE - - MODE obj - * - Object axis angle :raw-html:`
` - (in degrees) - - AXIS_ANG - - Diagnostic - - MODE :raw-html:`
` - MTD - - MTD obj - * - Difference in spatial :raw-html:`
` - axis plane angles - - AXIS_DIFF - - Diagnostic - - MTD - - MTD obj * - Baddeley’s Delta Metric - BADDELEY - Distance Map @@ -149,13 +101,6 @@ METplus Database of Statistics Ensemble-Stat - CNT :raw-html:`
` SSVAR - * - Minimum distance between :raw-html:`
` - the boundaries of two objects - - BOUNDARY :raw-html:`
` - _DIST - - Diagnostic - - MODE - - MODE obj * - Brier Score - BRIER - Probability @@ -192,58 +137,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - PJC - * - Total great circle distance :raw-html:`
` - travelled by the 2D spatial :raw-html:`
` - centroid over the lifetime :raw-html:`
` - of the 3D object - - CDIST :raw-html:`
` - _TRAVELLED - - Diagnostic - - MTD - - MTD 3D obj - * - Distance between two :raw-html:`
` - objects centroids :raw-html:`
` - (in grid units) - - CENTROID :raw-html:`
` - _DIST - - Diagnostic - - MODE - - MODE obj - * - Latitude of centroid :raw-html:`
` - - CENTROID :raw-html:`
` - _LAT - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Longitude of centroid :raw-html:`
` - - CENTROID :raw-html:`
` - _LON - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Time coordinate of centroid - - CENTROID_T - - Diagnostic - - MTD - - MTD 3D obj - * - X coordinate of centroid :raw-html:`
` - - CENTROID_X - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Y coordinate of centroid :raw-html:`
` - - CENTROID_Y - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj * - Climatological mean value - CLIMO_MEAN - Continuous @@ -261,35 +154,6 @@ METplus Database of Statistics Ensemble-Stat - MPR :raw-html:`
` ORANK - * - Ratio of the difference :raw-html:`
` - between the area of an :raw-html:`
` - object and the area of :raw-html:`
` - its convex hull divided :raw-html:`
` - by the area of the :raw-html:`
` - complex hull (unitless) - - COMPLEXITY - - Diagnostic - - MODE - - MODE obj - * - Ratio of complexities of :raw-html:`
` - two objects defined as :raw-html:`
` - the lesser of the forecast :raw-html:`
` - complexity divided by the :raw-html:`
` - observation complexity or :raw-html:`
` - its reciprocal (unitless) - - COMPLEXITY :raw-html:`
` - _RATIO - - Diagnostic - - MODE - - MODE obj - * - Minimum distance between :raw-html:`
` - the convex hulls of two :raw-html:`
` - objects (in grid units) - - CONVEX_HULL :raw-html:`
` - _DIST - - Diagnostic - - MODE - - MODE obj * - Continuous Ranked :raw-html:`
` Probability Score :raw-html:`
` (normal dist.) @@ -348,31 +212,6 @@ METplus Database of Statistics - CTS :raw-html:`
` MODE :raw-html:`
` MBRCTCS - * - Radius of curvature - - CURVATURE - - Diagnostic - - MODE - - MODE obj - * - Ratio of the curvature - - CURVATURE :raw-html:`
` - _RATIO - - Diagnostic - - MODE - - MODE obj - * - Center of curvature :raw-html:`
` - (in grid coordinates) - - CURVATURE :raw-html:`
` - _X - - Diagnostic - - MODE - - MODE obj - * - Center of curvature :raw-html:`
` - (in grid coordinates) - - CURVATURE :raw-html:`
` - _Y - - Diagnostic - - MODE - - MODE obj * - Absolute value of :raw-html:`
` DIR_ERR (see below) - DIR_ABSERR @@ -389,21 +228,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Difference in object :raw-html:`
` - direction of movement - - DIRECTION :raw-html:`
` - _DIFF - - Diagnostic - - MTD - - MTD 3D obj - * - Difference in the :raw-html:`
` - lifetimes of the :raw-html:`
` - two objects - - DURATION :raw-html:`
` - _DIFF - - Diagnostic - - MTD - - MTD 3D obj * - Expected correct rate :raw-html:`
` used for MCTS HSS_EC - EC_VALUE @@ -432,18 +256,6 @@ METplus Database of Statistics - Continuous - Grid-Stat - GRAD - * - Object end time - - END_TIME - - Diagnostic - - MTD - - MTD 3D obj - * - Difference in object :raw-html:`
` - ending time steps - - END_TIME :raw-html:`
` - _DELTA - - Diagnostic - - MTD - - MTD 3D obj * - The unperturbed :raw-html:`
` ensemble mean value - ENS_MEAN From 8d1f889bbc0648a9a308b520e68732c3854460e6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 15:45:54 -0700 Subject: [PATCH 217/770] adding diagnostics_list to TOC #1049 --- docs/Users_Guide/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/index.rst b/docs/Users_Guide/index.rst index a1ab31578a..8e1573c265 100644 --- a/docs/Users_Guide/index.rst +++ b/docs/Users_Guide/index.rst @@ -86,6 +86,7 @@ is sponsored by NSF. glossary references statistics_list + diagnostics_list .. Indices and tables From 8bd67b3e00c1455c6c48fabb7321906032ffa1a3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 16:11:44 -0700 Subject: [PATCH 218/770] separating lists --- docs/Users_Guide/diagnostics_list.rst | 575 ++++++++++++++++++++++++++ docs/Users_Guide/statistics_list.rst | 575 -------------------------- 2 files changed, 575 insertions(+), 575 deletions(-) diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst index 057801ec25..82048b4597 100644 --- a/docs/Users_Guide/diagnostics_list.rst +++ b/docs/Users_Guide/diagnostics_list.rst @@ -213,3 +213,578 @@ Diagnostics Database - Diagnostic - MTD - MTD 3D obj + * - Number of forecast :raw-html:`
` + clusters + - fcst_clus + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the cluster forecast :raw-html:`
` + objects + - fcst_clus :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Latitude + - fcst_clus :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Longitude + - fcst_clus :raw-html:`
` + _hull _lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Cluster Convex Hull Points + - fcst_clus :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Starting Index + - fcst_clus :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_clus :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_clus :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Forecast Object Raw :raw-html:`
` + Values + - fcst_obj :raw-html:`
` + _raw + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + forecast objects + - fcst_simp + - Diagnostic + - MODE + - MODE obj + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of all of the simple :raw-html:`
` + forecast objects + - fcst_simp :raw-html:`
` + _bdy + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Latitude + - fcst_simp :raw-html:`
` + _bdy_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Longitude + - fcst_simp :raw-html:`
` + _bdy_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Simple Boundary Points + - fcst_simp :raw-html:`
` + _bdy_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Starting Index + - fcst_simp :raw-html:`
` + _bdy_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary X-Coordinate + - fcst_simp :raw-html:`
` + _bdy_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Y-Coordinate + - fcst_simp :raw-html:`
` + _bdy_y + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the simple forecast :raw-html:`
` + objects + - fcst_simp :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Latitude + - fcst_simp :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Longitude + - fcst_simp :raw-html:`
` + _hull_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Simple Convex Hull Points + - fcst_simp :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Starting Index + - fcst_simp :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_simp :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_simp :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst :raw-html:`
` + _thresh :raw-html:`
` + _length + - Diagnostic + - MODE + - MODE obj + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst_thresh :raw-html:`
` + _length + - Diagnostic + - MODE + - MODE obj + * - Pratt’s Figure of Merit :raw-html:`
` + from observation to :raw-html:`
` + forecast + - FOM_FO + - Diagnostic + - Grid-Stat + - DMAP + * - Maximum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MAX + - Diagnostic + - Grid-Stat + - DMAP + * - Mean of FOM_FO :raw-html:`
` + and FOM_OF :raw-html:`
` + - FOM_MEAN + - Diagnostic + - Grid-Stat + - DMAP + * - Minimum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MIN + - Diagnostic + - Grid-Stat + - DMAP + * - Pratt’s Figure of Merit :raw-html:`
` + from forecast to :raw-html:`
` + observation + - FOM_OF + - Diagnostic + - Grid-Stat + - DMAP + * - Distance between the :raw-html:`
` + forecast and Best track :raw-html:`
` + genesis events (km) + - GEN_DIST + - Diagnostic + - TC-Gen + - GENMPR + * - Forecast minus Best track :raw-html:`
` + genesis time in HHMMSS :raw-html:`
` + format + - GEN_TDIFF + - Diagnostic + - TC-Gen + - GENMPR + * - Hausdorff Distance + - HAUSDORFF + - Diagnostic + - Grid-Stat + - DMAP + * - Best track genesis minus :raw-html:`
` + forecast initialization :raw-html:`
` + time in HHMMSS format + - INIT_TDIFF + - Diagnostic + - TC-Gen + - GENMPR + * - 10th, 25th, 50th, 75th, :raw-html:`
` + 90th, and user-specified :raw-html:`
` + percentiles of :raw-html:`
` + intensity of the raw :raw-html:`
` + field within the :raw-html:`
` + object or time slice + - INTENSITY :raw-html:`
` + _10, _25, :raw-html:`
` + _50, _75, :raw-html:`
` + _90, _NN + - Diagnostic + - MODE + - MODE obj + * - Sum of the intensities of :raw-html:`
` + the raw field within the :raw-html:`
` + object (variable units) + - INTENSITY :raw-html:`
` + _SUM + - Diagnostics + - MODE + - MODE obj + * - Total interest for this :raw-html:`
` + object pair + - INTEREST + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 3D obj :raw-html:`
` + MODE obj + * - Intersection area of two :raw-html:`
` + objects (in grid squares) + - INTERSECT :raw-html:`
` + ION_AREA + - Diagnostic + - MODE + - MODE obj + * - Ratio of intersection area :raw-html:`
` + to the lesser of the :raw-html:`
` + forecast and observation :raw-html:`
` + object areas (unitless) + - INTERSECT :raw-html:`
` + ION_OVER :raw-html:`
` + _AREA + - Diagnostic + - MODE + - MODE obj + * - “Volume” of object :raw-html:`
` + intersection + - INTERSECT :raw-html:`
` + ION_VOLUME + - Diagnostic + - MTD + - MTD 3D obj + * - Dimension of the latitude + - LAT + - Diagnostic + - MODE + - MODE obj + * - Length of the :raw-html:`
` + enclosing rectangle + - LENGTH + - Diagnostic + - MODE + - MODE obj + * - Dimension of the longitude + - LON + - Diagnostic + - MODE + - MODE obj + * - Number of cluster objects + - N_CLUS + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + forecast objects + - N_FCST_SIMP + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + observation objects + - N_OBS_SIMP + - Diagnostic + - MODE + - MODE obj + * - Number of observed :raw-html:`
` + clusters + - obs_clus + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all of :raw-html:`
` + the cluster observation :raw-html:`
` + objects + - obs_clus :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Latitude + - obs_clus :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Longitude + - obs_clus :raw-html:`
` + _hull_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Cluster Convex Hull Points + - obs_clus :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Starting Index + - obs_clus :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - obs_clus :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - obs_clus :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + observation objects + - obs_simp + - Diagnostic + - MODE + - MODE obj + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of the simple observation :raw-html:`
` + objects + - obs_simp :raw-html:`
` + _bdy + - Diagnostic + - MODE + - MODE obj + * - Observation Simple :raw-html:`
` + Boundary Point Latitude + - obs_simp :raw-html:`
` + _bdy_lat + - Diagnostic + - MODE + - MODE obj + * - Observation Simple :raw-html:`
` + Boundary Point Longitude + - obs_simp :raw-html:`
` + _bdy_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Simple Boundary Points + - obs_simp :raw-html:`
` + _bdy_npts + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of the :raw-html:`
` + simple observation objects + - obs_simp :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Simple Convex Hull Points + - obs_simp :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Ratio of the nth percentile :raw-html:`
` + (INTENSITY_NN column) of :raw-html:`
` + intensity of the two :raw-html:`
` + objects + - PERCENTILE :raw-html:`
` + _INTENSITY :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Spatial distance between :raw-html:`
` + (𝑥,𝑦)(x,y) coordinates of :raw-html:`
` + object spacetime centroid + - SPACE :raw-html:`
` + _CENTROID :raw-html:`
` + _DIST + - Diagnostics + - MTD + - MTD 3D obs + * - Difference in object speeds + - SPEED_DELTA + - Diagnostics + - MTD + - MTD 3D obs + * - Difference in object :raw-html:`
` + starting time steps + - START_TIME :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Symmetric difference of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - SYMMETRIC :raw-html:`
` + _DIFF + - Diagnostics + - MODE + - MODE obj + * - Difference in t index of :raw-html:`
` + object spacetime centroid + - TIME :raw-html:`
` + _CENTROID :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Union area of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - UNION_AREA + - Diagnostic + - MODE + - MODE obj + * - Integer count of the :raw-html:`
` + number of 3D “cells” :raw-html:`
` + in an object + - VOLUME + - Diagnostic + - MTD + - MTD 3D obj + * - Forecast object volume :raw-html:`
` + divided by observation :raw-html:`
` + object volume + - VOLUME :raw-html:`
` + _RATIO + - Diagnostic + - MTD + - MTD 3D obj + * - Width of the enclosing :raw-html:`
` + rectangle (in grid units) + - WIDTH + - Diagnostic + - MODE + - MODE obj + * - X component of :raw-html:`
` + object velocity + - X_DOT + - Diagnostic + - MTD + - MTD 3D obj + * - X component position :raw-html:`
` + error (nm) + - X_ERR + - Diagnostic + - TC-Pairs + - PROBRIRW + * - X component position :raw-html:`
` + error (nm) + - X_ERR + - Diagnostic + - TC-Pairs + - TCMPR + * - y component of :raw-html:`
` + object velocity + - Y_DOT + - Diagnostic + - MTD + - MTD 3D obj + * - Y component position :raw-html:`
` + error (nm) + - Y_ERR + - Diagnostic + - TC-Pairs + - PROBRIRW :raw-html:`
` + TCMPR + * - Zhu’s Measure from :raw-html:`
` + observation to forecast + - ZHU_FO + - Diagnostic + - Grid-Stat + - DMAP + * - Maximum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MAX + - Diagnostic + - Grid-Stat + - DMAP + * - Mean of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MEAN + - Diagnostic + - Grid-Stat + - DMAP + * - Minimum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MIN + - Diagnostic + - Grid-Stat + - DMAP + * - Zhu’s Measure from :raw-html:`
` + forecast to observation + - ZHU_OF + - Diagnostic + - Grid-Stat + - DMAP diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index ffb34160fc..c8a34e8d5e 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -345,193 +345,6 @@ METplus Database of Statistics - Continuous - Grid-Stat - NBRCNT - * - Number of forecast :raw-html:`
` - clusters - - fcst_clus - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of all :raw-html:`
` - of the cluster forecast :raw-html:`
` - objects - - fcst_clus :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point Latitude - - fcst_clus :raw-html:`
` - _hull_lat - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point Longitude - - fcst_clus :raw-html:`
` - _hull _lon - - Diagnostic - - MODE - - MODE obj - * - Number of Forecast :raw-html:`
` - Cluster Convex Hull Points - - fcst_clus :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Starting Index - - fcst_clus :raw-html:`
` - _hull_start - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point X-Coordinate - - fcst_clus :raw-html:`
` - _hull_x - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point Y-Coordinate - - fcst_clus :raw-html:`
` - _hull_y - - Diagnostic - - MODE - - MODE obj - * - Forecast Object Raw :raw-html:`
` - Values - - fcst_obj :raw-html:`
` - _raw - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - forecast objects - - fcst_simp - - Diagnostic - - MODE - - MODE obj - * - Number of points used :raw-html:`
` - to define the boundaries :raw-html:`
` - of all of the simple :raw-html:`
` - forecast objects - - fcst_simp :raw-html:`
` - _bdy - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Latitude - - fcst_simp :raw-html:`
` - _bdy_lat - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Longitude - - fcst_simp :raw-html:`
` - _bdy_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Forecast :raw-html:`
` - Simple Boundary Points - - fcst_simp :raw-html:`
` - _bdy_npts - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Starting Index - - fcst_simp :raw-html:`
` - _bdy_start - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary X-Coordinate - - fcst_simp :raw-html:`
` - _bdy_x - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Y-Coordinate - - fcst_simp :raw-html:`
` - _bdy_y - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of all :raw-html:`
` - of the simple forecast :raw-html:`
` - objects - - fcst_simp :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point Latitude - - fcst_simp :raw-html:`
` - _hull_lat - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point Longitude - - fcst_simp :raw-html:`
` - _hull_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Forecast :raw-html:`
` - Simple Convex Hull Points - - fcst_simp :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Starting Index - - fcst_simp :raw-html:`
` - _hull_start - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point X-Coordinate - - fcst_simp :raw-html:`
` - _hull_x - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point Y-Coordinate - - fcst_simp :raw-html:`
` - _hull_y - - Diagnostic - - MODE - - MODE obj - * - Number of thresholds :raw-html:`
` - applied to the forecast - - fcst :raw-html:`
` - _thresh :raw-html:`
` - _length - - Diagnostic - - MODE - - MODE obj - * - Number of thresholds :raw-html:`
` - applied to the forecast - - fcst_thresh :raw-html:`
` - _length - - Diagnostic - - MODE - - MODE obj * - Direction of the average :raw-html:`
` forecast wind vector - FDIR @@ -635,38 +448,6 @@ METplus Database of Statistics Grid-Stat - SSVAR :raw-html:`
` SL1L2 - * - Pratt’s Figure of Merit :raw-html:`
` - from observation to :raw-html:`
` - forecast - - FOM_FO - - Diagnostic - - Grid-Stat - - DMAP - * - Maximum of FOM_FO :raw-html:`
` - and FOM_OF - - FOM_MAX - - Diagnostic - - Grid-Stat - - DMAP - * - Mean of FOM_FO :raw-html:`
` - and FOM_OF :raw-html:`
` - - FOM_MEAN - - Diagnostic - - Grid-Stat - - DMAP - * - Minimum of FOM_FO :raw-html:`
` - and FOM_OF - - FOM_MIN - - Diagnostic - - Grid-Stat - - DMAP - * - Pratt’s Figure of Merit :raw-html:`
` - from forecast to :raw-html:`
` - observation - - FOM_OF - - Diagnostic - - Grid-Stat - - DMAP * - Number of tied forecast :raw-html:`
` ranks used in computing :raw-html:`
` Kendall’s tau statistic @@ -722,20 +503,6 @@ METplus Database of Statistics - MODE :raw-html:`
` CTC :raw-html:`
` NBRCTC - * - Distance between the :raw-html:`
` - forecast and Best track :raw-html:`
` - genesis events (km) - - GEN_DIST - - Diagnostic - - TC-Gen - - GENMPR - * - Forecast minus Best track :raw-html:`
` - genesis time in HHMMSS :raw-html:`
` - format - - GEN_TDIFF - - Diagnostic - - TC-Gen - - GENMPR * - Gerrity Score and :raw-html:`
` bootstrap confidence limits - GER @@ -758,11 +525,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - FHO - * - Hausdorff Distance - - HAUSDORFF - - Diagnostic - - Grid-Stat - - DMAP * - Hanssen and Kuipers :raw-html:`
` Discriminant - HK @@ -797,66 +559,6 @@ METplus Database of Statistics - Ensemble - Ensemble-Stat - ECNT - * - Best track genesis minus :raw-html:`
` - forecast initialization :raw-html:`
` - time in HHMMSS format - - INIT_TDIFF - - Diagnostic - - TC-Gen - - GENMPR - * - 10th, 25th, 50th, 75th, :raw-html:`
` - 90th, and user-specified :raw-html:`
` - percentiles of :raw-html:`
` - intensity of the raw :raw-html:`
` - field within the :raw-html:`
` - object or time slice - - INTENSITY :raw-html:`
` - _10, _25, :raw-html:`
` - _50, _75, :raw-html:`
` - _90, _NN - - Diagnostic - - MODE - - MODE obj - * - Sum of the intensities of :raw-html:`
` - the raw field within the :raw-html:`
` - object (variable units) - - INTENSITY :raw-html:`
` - _SUM - - Diagnostics - - MODE - - MODE obj - * - Total interest for this :raw-html:`
` - object pair - - INTEREST - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 3D obj :raw-html:`
` - MODE obj - * - Intersection area of two :raw-html:`
` - objects (in grid squares) - - INTERSECT :raw-html:`
` - ION_AREA - - Diagnostic - - MODE - - MODE obj - * - Ratio of intersection area :raw-html:`
` - to the lesser of the :raw-html:`
` - forecast and observation :raw-html:`
` - object areas (unitless) - - INTERSECT :raw-html:`
` - ION_OVER :raw-html:`
` - _AREA - - Diagnostic - - MODE - - MODE obj - * - “Volume” of object :raw-html:`
` - intersection - - INTERSECT :raw-html:`
` - ION_VOLUME - - Diagnostic - - MTD - - MTD 3D obj * - Interquartile Range :raw-html:`
` - IQR - Continuous @@ -882,17 +584,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - CNT - * - Dimension of the latitude - - LAT - - Diagnostic - - MODE - - MODE obj - * - Length of the :raw-html:`
` - enclosing rectangle - - LENGTH - - Diagnostic - - MODE - - MODE obj * - Likelihood when forecast :raw-html:`
` is between the ith and :raw-html:`
` i+1th probability :raw-html:`
` @@ -910,11 +601,6 @@ METplus Database of Statistics Grid-Stat - CTS :raw-html:`
` NBRCTS - * - Dimension of the longitude - - LON - - Diagnostic - - MODE - - MODE obj * - The Median Absolute :raw-html:`
` Deviation - MAD @@ -1036,23 +722,6 @@ METplus Database of Statistics Grid-Stat - MCTC :raw-html:`
` MCTS - * - Number of cluster objects - - N_CLUS - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - forecast objects - - N_FCST_SIMP - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - observation objects - - N_OBS_SIMP - - Diagnostic - - MODE - - MODE obj * - Observation rate - O_RATE - Categorical @@ -1090,114 +759,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Number of observed :raw-html:`
` - clusters - - obs_clus - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of all of :raw-html:`
` - the cluster observation :raw-html:`
` - objects - - obs_clus :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point Latitude - - obs_clus :raw-html:`
` - _hull_lat - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point Longitude - - obs_clus :raw-html:`
` - _hull_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Observation :raw-html:`
` - Cluster Convex Hull Points - - obs_clus :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Starting Index - - obs_clus :raw-html:`
` - _hull_start - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point X-Coordinate - - obs_clus :raw-html:`
` - _hull_x - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point Y-Coordinate - - obs_clus :raw-html:`
` - _hull_y - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - observation objects - - obs_simp - - Diagnostic - - MODE - - MODE obj - * - Number of points used :raw-html:`
` - to define the boundaries :raw-html:`
` - of the simple observation :raw-html:`
` - objects - - obs_simp :raw-html:`
` - _bdy - - Diagnostic - - MODE - - MODE obj - * - Observation Simple :raw-html:`
` - Boundary Point Latitude - - obs_simp :raw-html:`
` - _bdy_lat - - Diagnostic - - MODE - - MODE obj - * - Observation Simple :raw-html:`
` - Boundary Point Longitude - - obs_simp :raw-html:`
` - _bdy_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Observation :raw-html:`
` - Simple Boundary Points - - obs_simp :raw-html:`
` - _bdy_npts - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of the :raw-html:`
` - simple observation objects - - obs_simp :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Number of Observation :raw-html:`
` - Simple Convex Hull Points - - obs_simp :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj * - Odds Ratio - ODDS - Categorical @@ -1318,16 +879,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - PJC - * - Ratio of the nth percentile :raw-html:`
` - (INTENSITY_NN column) of :raw-html:`
` - intensity of the two :raw-html:`
` - objects - - PERCENTILE :raw-html:`
` - _INTENSITY :raw-html:`
` - _RATIO - - Diagnostic - - MODE - - MODE obj * - Probability Integral :raw-html:`
` Transform - PIT @@ -1569,15 +1120,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - CNT - * - Spatial distance between :raw-html:`
` - (𝑥,𝑦)(x,y) coordinates of :raw-html:`
` - object spacetime centroid - - SPACE :raw-html:`
` - _CENTROID :raw-html:`
` - _DIST - - Diagnostics - - MTD - - MTD 3D obs * - Absolute value of SPEED_ERR - SPEED :raw-html:`
` _ABSERR @@ -1585,11 +1127,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Difference in object speeds - - SPEED_DELTA - - Diagnostics - - MTD - - MTD 3D obs * - Difference between the :raw-html:`
` length of the average :raw-html:`
` forecast wind vector and :raw-html:`
` @@ -1626,29 +1163,6 @@ METplus Database of Statistics - Ensemble-Stat - ECNT :raw-html:`
` ORANK - * - Difference in object :raw-html:`
` - starting time steps - - START_TIME :raw-html:`
` - _DELTA - - Diagnostic - - MTD - - MTD 3D obj - * - Symmetric difference of :raw-html:`
` - two objects :raw-html:`
` - (in grid squares) - - SYMMETRIC :raw-html:`
` - _DIFF - - Diagnostics - - MODE - - MODE obj - * - Difference in t index of :raw-html:`
` - object spacetime centroid - - TIME :raw-html:`
` - _CENTROID :raw-html:`
` - _DELTA - - Diagnostic - - MTD - - MTD 3D obj * - Track error of adeck :raw-html:`
` relative to bdeck (nm) - TK_ERR @@ -1687,13 +1201,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - PSTD - * - Union area of :raw-html:`
` - two objects :raw-html:`
` - (in grid squares) - - UNION_AREA - - Diagnostic - - MODE - - MODE obj * - Mean U-component :raw-html:`
` Observation Anomaly - UOABAR @@ -1826,85 +1333,3 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 - * - Integer count of the :raw-html:`
` - number of 3D “cells” :raw-html:`
` - in an object - - VOLUME - - Diagnostic - - MTD - - MTD 3D obj - * - Forecast object volume :raw-html:`
` - divided by observation :raw-html:`
` - object volume - - VOLUME :raw-html:`
` - _RATIO - - Diagnostic - - MTD - - MTD 3D obj - * - Width of the enclosing :raw-html:`
` - rectangle (in grid units) - - WIDTH - - Diagnostic - - MODE - - MODE obj - * - X component of :raw-html:`
` - object velocity - - X_DOT - - Diagnostic - - MTD - - MTD 3D obj - * - X component position :raw-html:`
` - error (nm) - - X_ERR - - Diagnostic - - TC-Pairs - - PROBRIRW - * - X component position :raw-html:`
` - error (nm) - - X_ERR - - Diagnostic - - TC-Pairs - - TCMPR - * - y component of :raw-html:`
` - object velocity - - Y_DOT - - Diagnostic - - MTD - - MTD 3D obj - * - Y component position :raw-html:`
` - error (nm) - - Y_ERR - - Diagnostic - - TC-Pairs - - PROBRIRW :raw-html:`
` - TCMPR - * - Zhu’s Measure from :raw-html:`
` - observation to forecast - - ZHU_FO - - Diagnostic - - Grid-Stat - - DMAP - * - Maximum of ZHU_FO :raw-html:`
` - and ZHU_OF - - ZHU_MAX - - Diagnostic - - Grid-Stat - - DMAP - * - Mean of ZHU_FO :raw-html:`
` - and ZHU_OF - - ZHU_MEAN - - Diagnostic - - Grid-Stat - - DMAP - * - Minimum of ZHU_FO :raw-html:`
` - and ZHU_OF - - ZHU_MIN - - Diagnostic - - Grid-Stat - - DMAP - * - Zhu’s Measure from :raw-html:`
` - forecast to observation - - ZHU_OF - - Diagnostic - - Grid-Stat - - DMAP From 44f3cf4911bc49f617e29c8afec2c46bf2d3b085 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 16:19:43 -0700 Subject: [PATCH 219/770] fixing typos #1049 --- docs/Users_Guide/diagnostics_list.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst index 82048b4597..df4a10f039 100644 --- a/docs/Users_Guide/diagnostics_list.rst +++ b/docs/Users_Guide/diagnostics_list.rst @@ -13,7 +13,7 @@ Diagnostics Database .. role:: raw-html(raw) :format: html -.. list-table:: Statistics List +.. list-table:: Diagnostics List :widths: auto :header-rows: 1 @@ -476,7 +476,7 @@ Diagnostics Database object (variable units) - INTENSITY :raw-html:`
` _SUM - - Diagnostics + - Diagnostic - MODE - MODE obj * - Total interest for this :raw-html:`
` @@ -668,12 +668,12 @@ Diagnostics Database - SPACE :raw-html:`
` _CENTROID :raw-html:`
` _DIST - - Diagnostics + - Diagnostic - MTD - MTD 3D obs * - Difference in object speeds - SPEED_DELTA - - Diagnostics + - Diagnostic - MTD - MTD 3D obs * - Difference in object :raw-html:`
` @@ -688,7 +688,7 @@ Diagnostics Database (in grid squares) - SYMMETRIC :raw-html:`
` _DIFF - - Diagnostics + - Diagnostic - MODE - MODE obj * - Difference in t index of :raw-html:`
` From bc17ad73f2173354e2bd468b85d9f02485058ea1 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 16:36:49 -0700 Subject: [PATCH 220/770] moving blank statistic type from statistics to diagnostics #1049 --- docs/Users_Guide/diagnostics_list.rst | 53 ++++++++++++++++++++++++++- docs/Users_Guide/statistics_list.rst | 51 -------------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst index df4a10f039..1cfc6dc562 100644 --- a/docs/Users_Guide/diagnostics_list.rst +++ b/docs/Users_Guide/diagnostics_list.rst @@ -227,7 +227,7 @@ Diagnostics Database _hull - Diagnostic - MODE - - MODE obj + - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Latitude - fcst_clus :raw-html:`
` @@ -400,6 +400,24 @@ Diagnostics Database - Diagnostic - MODE - MODE obj + * - Forecast energy squared :raw-html:`
` + for this scale + - FENERGY + - + - Wavelet-Stat + - ISC + * - Mean of absolute value :raw-html:`
` + of forecast gradients + - FGBAR + - + - Grid-Stat + - GRAD + * - Ratio of forecast and :raw-html:`
` + observed gradients + - FGOG_RATIO + - + - Grid-Stat + - GRAD * - Pratt’s Figure of Merit :raw-html:`
` from observation to :raw-html:`
` forecast @@ -511,6 +529,19 @@ Diagnostics Database - Diagnostic - MTD - MTD 3D obj + * - The intensity scale :raw-html:`
` + skill score + - ISC + - + - Wavelet-Stat + - ISC + * - The scale at which all :raw-html:`
` + information following :raw-html:`
` + applies + - ISCALE + - + - Wavelet-Stat + - ISC * - Dimension of the latitude - LAT - Diagnostic @@ -527,6 +558,14 @@ Diagnostics Database - Diagnostic - MODE - MODE obj + * - Mean of maximum of :raw-html:`
` + absolute values of :raw-html:`
` + forecast and observed :raw-html:`
` + gradients + - MGBAR + - + - Grid-Stat + - GRAD * - Number of cluster objects - N_CLUS - Diagnostic @@ -652,6 +691,18 @@ Diagnostics Database - Diagnostic - MODE - MODE obj + * - Observed energy squared :raw-html:`
` + for this scale + - OENERGY + - + - Wavelet-Stat + - ISC + * - Mean of absolute value :raw-html:`
` + of observed gradients + - OGBAR + - + - Grid-Stat + - GRAD * - Ratio of the nth percentile :raw-html:`
` (INTENSITY_NN column) of :raw-html:`
` intensity of the two :raw-html:`
` diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index c8a34e8d5e..3834ec8a90 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -352,12 +352,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Forecast energy squared :raw-html:`
` - for this scale - - FENERGY - - - - Wavelet-Stat - - ISC * - Mean Forecast Anomaly Squared - FFABAR - Continuous @@ -373,18 +367,6 @@ METplus Database of Statistics Grid-Stat - SSVAR :raw-html:`
` SL1L2 - * - Mean of absolute value :raw-html:`
` - of forecast gradients - - FGBAR - - - - Grid-Stat - - GRAD - * - Ratio of forecast and :raw-html:`
` - observed gradients - - FGOG_RATIO - - - - Grid-Stat - - GRAD * - Count of events in :raw-html:`
` forecast category i and :raw-html:`
` observation category j @@ -565,19 +547,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - CNT - * - The intensity scale :raw-html:`
` - skill score - - ISC - - - - Wavelet-Stat - - ISC - * - The scale at which all :raw-html:`
` - information following :raw-html:`
` - applies - - ISCALE - - - - Wavelet-Stat - - ISC * - Kendall’s tau statistic - KT_CORR - Continuous @@ -677,14 +646,6 @@ METplus Database of Statistics - Distance - Grid-Stat - DMAP - * - Mean of maximum of :raw-html:`
` - absolute values of :raw-html:`
` - forecast and observed :raw-html:`
` - gradients - - MGBAR - - - - Grid-Stat - - GRAD * - Mean squared error - MSE - Continuous @@ -775,18 +736,6 @@ METplus Database of Statistics - Point-Stat :raw-html:`
` Grid-Stat - VCNT - * - Observed energy squared :raw-html:`
` - for this scale - - OENERGY - - - - Wavelet-Stat - - ISC - * - Mean of absolute value :raw-html:`
` - of observed gradients - - OGBAR - - - - Grid-Stat - - GRAD * - Number of observation :raw-html:`
` when forecast is between :raw-html:`
` the ith and i+1th :raw-html:`
` From 9e0f7e33f0e80459b6f665144a6d912b273c7947 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 16 Nov 2021 16:46:58 -0700 Subject: [PATCH 221/770] Feature 1263 v4.1.0 beta4 (#1277) --- docs/Users_Guide/release-notes.rst | 26 ++++++++++++++++++++++++++ metplus/VERSION | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index 5e4eb32a35..c2b53db183 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -33,6 +33,32 @@ When applicable, release notes are followed by the GitHub issue number which describes the bugfix, enhancement, or new feature: https://github.com/dtcenter/METplus/issues +METplus Version 4.1.0-beta4 Release Notes (2021-11-16) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Enhancements: + + * **Create an Amazon AMI containing all METplus components** (`#506 `_) + * Added support for setting a dictionary value for time_summary.width (`#1252 `_) + * Added support for setting obs_quality_inc/exc in PointStat (`#1213 `_) + * Properly handle list values that include square braces (`#1212 `_) + * Reorganize the Cryosphere and Marine and Coastal use case categories into one group (`#1200 `_) + * Update wrapped MET config files to reference MET_TMP_DIR in tmp value (`#1101 `_) + * CyclonePlotter, create options to format output grid area to user-desired area (`#1091 `_) + * CyclonePlotter, connected lines run over the Prime Meridian (`#1000 `_) + * Add harmonic pre-processing to the RMM use case (`#1019 `_) + +* New Wrappers: + + * **IODA2NC** (`#1203 `_) + * **GenEnsProd** (`#1180 `_, `#1266 `_) + +* New Use Cases: + + * **IODA2NC** (`#1204 `_) + * **GenEnsProd** (`#1180 `_, `#1266 `_) + + METplus Version 4.1.0-beta3 Release Notes (2021-10-06) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/metplus/VERSION b/metplus/VERSION index 594faefc69..a08b93653b 100644 --- a/metplus/VERSION +++ b/metplus/VERSION @@ -1 +1 @@ -4.1.0-beta4-dev \ No newline at end of file +4.1.0-beta4 \ No newline at end of file From 801cc94e099bb1f01e0cd8dc0f39a23a6b1d2748 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 16 Nov 2021 16:47:51 -0700 Subject: [PATCH 222/770] update version to note development towards beta5 --- metplus/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metplus/VERSION b/metplus/VERSION index a08b93653b..bcc7104f60 100644 --- a/metplus/VERSION +++ b/metplus/VERSION @@ -1 +1 @@ -4.1.0-beta4 \ No newline at end of file +4.1.0-beta5-dev \ No newline at end of file From f22e419c33d88f6130436559f71bb380127fc83f Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 16:49:06 -0700 Subject: [PATCH 223/770] fixing errors #1049 --- docs/Users_Guide/diagnostics_list.rst | 4 ++-- docs/Users_Guide/statistics_list.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst index 1cfc6dc562..e15c280c97 100644 --- a/docs/Users_Guide/diagnostics_list.rst +++ b/docs/Users_Guide/diagnostics_list.rst @@ -13,7 +13,7 @@ Diagnostics Database .. role:: raw-html(raw) :format: html -.. list-table:: Diagnostics List +.. list-table:: TEST List :widths: auto :header-rows: 1 @@ -529,7 +529,7 @@ Diagnostics Database - Diagnostic - MTD - MTD 3D obj - * - The intensity scale :raw-html:`
` + * - The intensity scale :raw-html:`
` skill score - ISC - diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 3834ec8a90..a14b5c5cdb 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -529,7 +529,7 @@ METplus Database of Statistics CTS :raw-html:`
` NBRCTS * - Heidke Skill Score :raw-html:`
` - user-specific expected :raw-html:`
` + user-specific expected :raw-html:`
` correct - HSS_EC - Categorical From 2eb0ee7f17a132fbcd7cf78f9656799728366557 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 17:10:29 -0700 Subject: [PATCH 224/770] updating table name #1049 --- docs/Users_Guide/diagnostics_list.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst index e15c280c97..393aedc6b5 100644 --- a/docs/Users_Guide/diagnostics_list.rst +++ b/docs/Users_Guide/diagnostics_list.rst @@ -13,7 +13,7 @@ Diagnostics Database .. role:: raw-html(raw) :format: html -.. list-table:: TEST List +.. list-table:: Diagnostics List :widths: auto :header-rows: 1 From 028cfcd83765979a56a9392f6b90ac7ae3b96670 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Tue, 16 Nov 2021 17:10:47 -0700 Subject: [PATCH 225/770] fixing typo #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index a14b5c5cdb..8531227c83 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -290,7 +290,7 @@ METplus Database of Statistics * - Mean forecast wind speed - F_SPEED :raw-html:`
` _BAR - - Continous + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VL1L2 @@ -1260,7 +1260,7 @@ METplus Database of Statistics - VCNT * - Mean(vf-vc) - VFABAR - - Continous + - Continuous - Point-Stat :raw-html:`
` Grid-Stat - VAL1L2 From 33ba9acf1625a24cee2ea727d6778de6c8986984 Mon Sep 17 00:00:00 2001 From: jprestop Date: Wed, 17 Nov 2021 08:24:23 -0700 Subject: [PATCH 226/770] Feature 934 release stage doc (#1235) * Per #934 add stages of the METplus release cycle. * Per #934, adding link to descriptions of the release cycle in the User's Guide. * Per #934, made corrections * Per #934, changed Beta and Release Candidate (rc) from bold to subsubsections. * Update index.rst Co-authored-by: Julie Prestopnik --- docs/Release_Guide/index.rst | 54 ++++++++++++++++++++++++++++-- docs/Users_Guide/release-notes.rst | 5 +++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/docs/Release_Guide/index.rst b/docs/Release_Guide/index.rst index 3f14c7882f..160439a6b1 100644 --- a/docs/Release_Guide/index.rst +++ b/docs/Release_Guide/index.rst @@ -2,10 +2,60 @@ Release Guide ============= -This METplus Release Guide provides detailed instructions for creating software releases from the METplus component repositories. +This METplus Release Guide provides detailed instructions for METplus +developers for creating software releases for the METplus component +repositories. **This Release Guide is intended for developers creating +releases and is not intended for users of the software.** + +.. _releaseCycleStages: + +Stages of the METplus Release Cycle +=================================== + +Development Release +------------------- + +Beta +^^^^ + +Beta releases are a pre-release of the software to give a larger group of +users the opportunity to test the recently incorporated new features, +enhancements, and bug fixes. Beta releases allow for continued +development and bug fixes before an official release. There are many +possible configurations of hardware and software that exist and installation +of beta releases allow for testing of potential conflicts. + +Release Candidate (rc) +^^^^^^^^^^^^^^^^^^^^^^ + +A release candidate is a version of the software that is nearly ready for +official release but may still have a few bugs. At this stage, all product +features have been designed, coded, and tested through one or more beta +cycles with no known bugs. It is code complete, meaning that no entirely +new source code will be added to this release. There may still be source +code changes to fix bugs, changes to documentation, and changes to test +cases or utilities. + +Official Release +---------------- + +An official release is a stable release and is basically the release +candidate, which has passed all tests. It is the version of the code that +has been tested as thoroughly as possible and is reliable enough to be +used in production. + +Bugfix Release +-------------- + +A bugfix release introduces no new features, but fixes bugs in previous +official releases and targets the most critical bugs affecting users. + +Instructions Summary +==================== + Instructions are provided for three types of software releases: -#. **Official Release** (e.g. vX.Y.Z) from the develop branch +#. **Official Release** (e.g. vX.Y.Z) from the develop branch (becomes the new main_vX.Y branch) #. **Bugfix Release** (e.g. vX.Y.Z) from the corresponding main_vX.Y branch diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index c2b53db183..00078ee855 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -1,6 +1,11 @@ METplus Release Notes ===================== +Users can view the :ref:`releaseCycleStages` section of +the Release Guide for descriptions of the development releases (including +beta releases and release candidates), official releases, and bugfix +releases for the METplus Components. + METplus Components Release Note Links ------------------------------------- From b7cc8760d7ce45e731955767819fe5d261bb0a41 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 10:20:02 -0700 Subject: [PATCH 227/770] putting both tables into one chapter --- docs/Users_Guide/diagnostics_list.rst | 3 +- docs/Users_Guide/statistics_list.rst | 853 +++++++++++++++++++++++++- 2 files changed, 850 insertions(+), 6 deletions(-) diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst index 393aedc6b5..d93455aa60 100644 --- a/docs/Users_Guide/diagnostics_list.rst +++ b/docs/Users_Guide/diagnostics_list.rst @@ -1,6 +1,5 @@ -******************** Diagnostics Database -******************** +==================== .. Number of characters per line: diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8531227c83..fa528755bb 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1,6 +1,6 @@ -****************************** -METplus Database of Statistics -****************************** +******************************** +METplus Statistics & Diagnostics +******************************** .. Number of characters per line: @@ -9,6 +9,8 @@ METplus Database of Statistics Statistic Type - no more than 19 characters METplus Line Type - currently unlimited (approx 33 characters) +Statistics Database +=================== .. role:: raw-html(raw) :format: html @@ -1281,4 +1283,847 @@ METplus Database of Statistics - Continuous - Point-Stat :raw-html:`
` Grid-Stat - - VL1L2 + - VL1L2 + + +Diagnostics Database +==================== + + +.. Number of characters per line: + Statistic Name - no more that 32 characters + METplus Name - no more than 17 characters + Statistic Type - no more than 19 characters + METplus Line Type - currently unlimited (approx 33 characters) + + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Diagnostics List + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type + * - Difference between the axis :raw-html:`
` + angles of two objects (in degrees) + - ANGLE_DIFF + - Diagnostic + - MODE + - MODE + * - Object area (in grid squares) + - AREA + - Diagnostic + - MODE :raw-html:`
` + MTD + - MODE obj + * - Forecast object area :raw-html:`
` + divided by the observation :raw-html:`
` + object area (unitless) + - AREA_RATIO + - Diagnostic + - MODE + - MODE obj + * - Area of the object :raw-html:`
` + that meet the object :raw-html:`
` + definition threshold :raw-html:`
` + criteria (in grid squares) + - AREA_THRESH + - Diagnostic + - MODE + - MODE obj + * - Absolute value of :raw-html:`
` + the difference :raw-html:`
` + between the aspect :raw-html:`
` + ratios of two objects :raw-html:`
` + (unitless) + - ASPECT_DIFF + - Diagnostic + - MODE + - MODE obj + * - Object axis angle :raw-html:`
` + (in degrees) + - AXIS_ANG + - Diagnostic + - MODE :raw-html:`
` + MTD + - MTD obj + * - Difference in spatial :raw-html:`
` + axis plane angles + - AXIS_DIFF + - Diagnostic + - MTD + - MTD obj + * - Minimum distance between :raw-html:`
` + the boundaries of two objects + - BOUNDARY :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Total great circle distance :raw-html:`
` + travelled by the 2D spatial :raw-html:`
` + centroid over the lifetime :raw-html:`
` + of the 3D object + - CDIST :raw-html:`
` + _TRAVELLED + - Diagnostic + - MTD + - MTD 3D obj + * - Distance between two :raw-html:`
` + objects centroids :raw-html:`
` + (in grid units) + - CENTROID :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Latitude of centroid :raw-html:`
` + - CENTROID :raw-html:`
` + _LAT + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Longitude of centroid :raw-html:`
` + - CENTROID :raw-html:`
` + _LON + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Time coordinate of centroid + - CENTROID_T + - Diagnostic + - MTD + - MTD 3D obj + * - X coordinate of centroid :raw-html:`
` + - CENTROID_X + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Y coordinate of centroid :raw-html:`
` + - CENTROID_Y + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 2D & 3D obj :raw-html:`
` + MODE obj + * - Ratio of the difference :raw-html:`
` + between the area of an :raw-html:`
` + object and the area of :raw-html:`
` + its convex hull divided :raw-html:`
` + by the area of the :raw-html:`
` + complex hull (unitless) + - COMPLEXITY + - Diagnostic + - MODE + - MODE obj + * - Ratio of complexities of :raw-html:`
` + two objects defined as :raw-html:`
` + the lesser of the forecast :raw-html:`
` + complexity divided by the :raw-html:`
` + observation complexity or :raw-html:`
` + its reciprocal (unitless) + - COMPLEXITY :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Minimum distance between :raw-html:`
` + the convex hulls of two :raw-html:`
` + objects (in grid units) + - CONVEX_HULL :raw-html:`
` + _DIST + - Diagnostic + - MODE + - MODE obj + * - Radius of curvature + - CURVATURE + - Diagnostic + - MODE + - MODE obj + * - Ratio of the curvature + - CURVATURE :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE :raw-html:`
` + _X + - Diagnostic + - MODE + - MODE obj + * - Center of curvature :raw-html:`
` + (in grid coordinates) + - CURVATURE :raw-html:`
` + _Y + - Diagnostic + - MODE + - MODE obj + * - Difference in object :raw-html:`
` + direction of movement + - DIRECTION :raw-html:`
` + _DIFF + - Diagnostic + - MTD + - MTD 3D obj + * - Difference in the :raw-html:`
` + lifetimes of the :raw-html:`
` + two objects + - DURATION :raw-html:`
` + _DIFF + - Diagnostic + - MTD + - MTD 3D obj + * - Object end time + - END_TIME + - Diagnostic + - MTD + - MTD 3D obj + * - Difference in object :raw-html:`
` + ending time steps + - END_TIME :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Number of forecast :raw-html:`
` + clusters + - fcst_clus + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the cluster forecast :raw-html:`
` + objects + - fcst_clus :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Latitude + - fcst_clus :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Longitude + - fcst_clus :raw-html:`
` + _hull _lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Cluster Convex Hull Points + - fcst_clus :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Starting Index + - fcst_clus :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_clus :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_clus :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Forecast Object Raw :raw-html:`
` + Values + - fcst_obj :raw-html:`
` + _raw + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + forecast objects + - fcst_simp + - Diagnostic + - MODE + - MODE obj + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of all of the simple :raw-html:`
` + forecast objects + - fcst_simp :raw-html:`
` + _bdy + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Latitude + - fcst_simp :raw-html:`
` + _bdy_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Longitude + - fcst_simp :raw-html:`
` + _bdy_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Simple Boundary Points + - fcst_simp :raw-html:`
` + _bdy_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Starting Index + - fcst_simp :raw-html:`
` + _bdy_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary X-Coordinate + - fcst_simp :raw-html:`
` + _bdy_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple :raw-html:`
` + Boundary Y-Coordinate + - fcst_simp :raw-html:`
` + _bdy_y + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all :raw-html:`
` + of the simple forecast :raw-html:`
` + objects + - fcst_simp :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Latitude + - fcst_simp :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Longitude + - fcst_simp :raw-html:`
` + _hull_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Forecast :raw-html:`
` + Simple Convex Hull Points + - fcst_simp :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Starting Index + - fcst_simp :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point X-Coordinate + - fcst_simp :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Forecast Simple Convex :raw-html:`
` + Hull Point Y-Coordinate + - fcst_simp :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst :raw-html:`
` + _thresh :raw-html:`
` + _length + - Diagnostic + - MODE + - MODE obj + * - Number of thresholds :raw-html:`
` + applied to the forecast + - fcst_thresh :raw-html:`
` + _length + - Diagnostic + - MODE + - MODE obj + * - Forecast energy squared :raw-html:`
` + for this scale + - FENERGY + - + - Wavelet-Stat + - ISC + * - Mean of absolute value :raw-html:`
` + of forecast gradients + - FGBAR + - + - Grid-Stat + - GRAD + * - Ratio of forecast and :raw-html:`
` + observed gradients + - FGOG_RATIO + - + - Grid-Stat + - GRAD + * - Pratt’s Figure of Merit :raw-html:`
` + from observation to :raw-html:`
` + forecast + - FOM_FO + - Diagnostic + - Grid-Stat + - DMAP + * - Maximum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MAX + - Diagnostic + - Grid-Stat + - DMAP + * - Mean of FOM_FO :raw-html:`
` + and FOM_OF :raw-html:`
` + - FOM_MEAN + - Diagnostic + - Grid-Stat + - DMAP + * - Minimum of FOM_FO :raw-html:`
` + and FOM_OF + - FOM_MIN + - Diagnostic + - Grid-Stat + - DMAP + * - Pratt’s Figure of Merit :raw-html:`
` + from forecast to :raw-html:`
` + observation + - FOM_OF + - Diagnostic + - Grid-Stat + - DMAP + * - Distance between the :raw-html:`
` + forecast and Best track :raw-html:`
` + genesis events (km) + - GEN_DIST + - Diagnostic + - TC-Gen + - GENMPR + * - Forecast minus Best track :raw-html:`
` + genesis time in HHMMSS :raw-html:`
` + format + - GEN_TDIFF + - Diagnostic + - TC-Gen + - GENMPR + * - Hausdorff Distance + - HAUSDORFF + - Diagnostic + - Grid-Stat + - DMAP + * - Best track genesis minus :raw-html:`
` + forecast initialization :raw-html:`
` + time in HHMMSS format + - INIT_TDIFF + - Diagnostic + - TC-Gen + - GENMPR + * - 10th, 25th, 50th, 75th, :raw-html:`
` + 90th, and user-specified :raw-html:`
` + percentiles of :raw-html:`
` + intensity of the raw :raw-html:`
` + field within the :raw-html:`
` + object or time slice + - INTENSITY :raw-html:`
` + _10, _25, :raw-html:`
` + _50, _75, :raw-html:`
` + _90, _NN + - Diagnostic + - MODE + - MODE obj + * - Sum of the intensities of :raw-html:`
` + the raw field within the :raw-html:`
` + object (variable units) + - INTENSITY :raw-html:`
` + _SUM + - Diagnostic + - MODE + - MODE obj + * - Total interest for this :raw-html:`
` + object pair + - INTEREST + - Diagnostic + - MTD :raw-html:`
` + MODE + - MTD 3D obj :raw-html:`
` + MODE obj + * - Intersection area of two :raw-html:`
` + objects (in grid squares) + - INTERSECT :raw-html:`
` + ION_AREA + - Diagnostic + - MODE + - MODE obj + * - Ratio of intersection area :raw-html:`
` + to the lesser of the :raw-html:`
` + forecast and observation :raw-html:`
` + object areas (unitless) + - INTERSECT :raw-html:`
` + ION_OVER :raw-html:`
` + _AREA + - Diagnostic + - MODE + - MODE obj + * - “Volume” of object :raw-html:`
` + intersection + - INTERSECT :raw-html:`
` + ION_VOLUME + - Diagnostic + - MTD + - MTD 3D obj + * - The intensity scale :raw-html:`
` + skill score + - ISC + - + - Wavelet-Stat + - ISC + * - The scale at which all :raw-html:`
` + information following :raw-html:`
` + applies + - ISCALE + - + - Wavelet-Stat + - ISC + * - Dimension of the latitude + - LAT + - Diagnostic + - MODE + - MODE obj + * - Length of the :raw-html:`
` + enclosing rectangle + - LENGTH + - Diagnostic + - MODE + - MODE obj + * - Dimension of the longitude + - LON + - Diagnostic + - MODE + - MODE obj + * - Mean of maximum of :raw-html:`
` + absolute values of :raw-html:`
` + forecast and observed :raw-html:`
` + gradients + - MGBAR + - + - Grid-Stat + - GRAD + * - Number of cluster objects + - N_CLUS + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + forecast objects + - N_FCST_SIMP + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + observation objects + - N_OBS_SIMP + - Diagnostic + - MODE + - MODE obj + * - Number of observed :raw-html:`
` + clusters + - obs_clus + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of all of :raw-html:`
` + the cluster observation :raw-html:`
` + objects + - obs_clus :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Latitude + - obs_clus :raw-html:`
` + _hull_lat + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Longitude + - obs_clus :raw-html:`
` + _hull_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Cluster Convex Hull Points + - obs_clus :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Starting Index + - obs_clus :raw-html:`
` + _hull_start + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point X-Coordinate + - obs_clus :raw-html:`
` + _hull_x + - Diagnostic + - MODE + - MODE obj + * - Observation Cluster Convex :raw-html:`
` + Hull Point Y-Coordinate + - obs_clus :raw-html:`
` + _hull_y + - Diagnostic + - MODE + - MODE obj + * - Number of simple :raw-html:`
` + observation objects + - obs_simp + - Diagnostic + - MODE + - MODE obj + * - Number of points used :raw-html:`
` + to define the boundaries :raw-html:`
` + of the simple observation :raw-html:`
` + objects + - obs_simp :raw-html:`
` + _bdy + - Diagnostic + - MODE + - MODE obj + * - Observation Simple :raw-html:`
` + Boundary Point Latitude + - obs_simp :raw-html:`
` + _bdy_lat + - Diagnostic + - MODE + - MODE obj + * - Observation Simple :raw-html:`
` + Boundary Point Longitude + - obs_simp :raw-html:`
` + _bdy_lon + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Simple Boundary Points + - obs_simp :raw-html:`
` + _bdy_npts + - Diagnostic + - MODE + - MODE obj + * - Number of points used to :raw-html:`
` + define the hull of the :raw-html:`
` + simple observation objects + - obs_simp :raw-html:`
` + _hull + - Diagnostic + - MODE + - MODE obj + * - Number of Observation :raw-html:`
` + Simple Convex Hull Points + - obs_simp :raw-html:`
` + _hull_npts + - Diagnostic + - MODE + - MODE obj + * - Observed energy squared :raw-html:`
` + for this scale + - OENERGY + - + - Wavelet-Stat + - ISC + * - Mean of absolute value :raw-html:`
` + of observed gradients + - OGBAR + - + - Grid-Stat + - GRAD + * - Ratio of the nth percentile :raw-html:`
` + (INTENSITY_NN column) of :raw-html:`
` + intensity of the two :raw-html:`
` + objects + - PERCENTILE :raw-html:`
` + _INTENSITY :raw-html:`
` + _RATIO + - Diagnostic + - MODE + - MODE obj + * - Spatial distance between :raw-html:`
` + (𝑥,𝑦)(x,y) coordinates of :raw-html:`
` + object spacetime centroid + - SPACE :raw-html:`
` + _CENTROID :raw-html:`
` + _DIST + - Diagnostic + - MTD + - MTD 3D obs + * - Difference in object speeds + - SPEED_DELTA + - Diagnostic + - MTD + - MTD 3D obs + * - Difference in object :raw-html:`
` + starting time steps + - START_TIME :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Symmetric difference of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - SYMMETRIC :raw-html:`
` + _DIFF + - Diagnostic + - MODE + - MODE obj + * - Difference in t index of :raw-html:`
` + object spacetime centroid + - TIME :raw-html:`
` + _CENTROID :raw-html:`
` + _DELTA + - Diagnostic + - MTD + - MTD 3D obj + * - Union area of :raw-html:`
` + two objects :raw-html:`
` + (in grid squares) + - UNION_AREA + - Diagnostic + - MODE + - MODE obj + * - Integer count of the :raw-html:`
` + number of 3D “cells” :raw-html:`
` + in an object + - VOLUME + - Diagnostic + - MTD + - MTD 3D obj + * - Forecast object volume :raw-html:`
` + divided by observation :raw-html:`
` + object volume + - VOLUME :raw-html:`
` + _RATIO + - Diagnostic + - MTD + - MTD 3D obj + * - Width of the enclosing :raw-html:`
` + rectangle (in grid units) + - WIDTH + - Diagnostic + - MODE + - MODE obj + * - X component of :raw-html:`
` + object velocity + - X_DOT + - Diagnostic + - MTD + - MTD 3D obj + * - X component position :raw-html:`
` + error (nm) + - X_ERR + - Diagnostic + - TC-Pairs + - PROBRIRW + * - X component position :raw-html:`
` + error (nm) + - X_ERR + - Diagnostic + - TC-Pairs + - TCMPR + * - y component of :raw-html:`
` + object velocity + - Y_DOT + - Diagnostic + - MTD + - MTD 3D obj + * - Y component position :raw-html:`
` + error (nm) + - Y_ERR + - Diagnostic + - TC-Pairs + - PROBRIRW :raw-html:`
` + TCMPR + * - Zhu’s Measure from :raw-html:`
` + observation to forecast + - ZHU_FO + - Diagnostic + - Grid-Stat + - DMAP + * - Maximum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MAX + - Diagnostic + - Grid-Stat + - DMAP + * - Mean of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MEAN + - Diagnostic + - Grid-Stat + - DMAP + * - Minimum of ZHU_FO :raw-html:`
` + and ZHU_OF + - ZHU_MIN + - Diagnostic + - Grid-Stat + - DMAP + * - Zhu’s Measure from :raw-html:`
` + forecast to observation + - ZHU_OF + - Diagnostic + - Grid-Stat + - DMAP + From 7923178021cb0feac15074e5150bdae2958394dd Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 10:27:40 -0700 Subject: [PATCH 228/770] removing diagnostics since it's going back into the statistics chapter #1049 --- docs/Users_Guide/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/Users_Guide/index.rst b/docs/Users_Guide/index.rst index 8e1573c265..a1ab31578a 100644 --- a/docs/Users_Guide/index.rst +++ b/docs/Users_Guide/index.rst @@ -86,7 +86,6 @@ is sponsored by NSF. glossary references statistics_list - diagnostics_list .. Indices and tables From 9fdb8451dbdf4266c67878676ed9c1d3eccf5659 Mon Sep 17 00:00:00 2001 From: lisagoodrich <33230218+lisagoodrich@users.noreply.github.com> Date: Wed, 17 Nov 2021 10:46:34 -0700 Subject: [PATCH 229/770] Delete diagnostics_list.rst This table will be added to the statistics page as a separate table. #1049 --- docs/Users_Guide/diagnostics_list.rst | 840 -------------------------- 1 file changed, 840 deletions(-) delete mode 100644 docs/Users_Guide/diagnostics_list.rst diff --git a/docs/Users_Guide/diagnostics_list.rst b/docs/Users_Guide/diagnostics_list.rst deleted file mode 100644 index d93455aa60..0000000000 --- a/docs/Users_Guide/diagnostics_list.rst +++ /dev/null @@ -1,840 +0,0 @@ -Diagnostics Database -==================== - - -.. Number of characters per line: - Statistic Name - no more that 32 characters - METplus Name - no more than 17 characters - Statistic Type - no more than 19 characters - METplus Line Type - currently unlimited (approx 33 characters) - - -.. role:: raw-html(raw) - :format: html - -.. list-table:: Diagnostics List - :widths: auto - :header-rows: 1 - - * - Statistics :raw-html:`
` - Long Name - - METplus Name - - Statistic Type - - Tools - - METplus :raw-html:`
` - Line Type - * - Difference between the axis :raw-html:`
` - angles of two objects (in degrees) - - ANGLE_DIFF - - Diagnostic - - MODE - - MODE - * - Object area (in grid squares) - - AREA - - Diagnostic - - MODE :raw-html:`
` - MTD - - MODE obj - * - Forecast object area :raw-html:`
` - divided by the observation :raw-html:`
` - object area (unitless) - - AREA_RATIO - - Diagnostic - - MODE - - MODE obj - * - Area of the object :raw-html:`
` - that meet the object :raw-html:`
` - definition threshold :raw-html:`
` - criteria (in grid squares) - - AREA_THRESH - - Diagnostic - - MODE - - MODE obj - * - Absolute value of :raw-html:`
` - the difference :raw-html:`
` - between the aspect :raw-html:`
` - ratios of two objects :raw-html:`
` - (unitless) - - ASPECT_DIFF - - Diagnostic - - MODE - - MODE obj - * - Object axis angle :raw-html:`
` - (in degrees) - - AXIS_ANG - - Diagnostic - - MODE :raw-html:`
` - MTD - - MTD obj - * - Difference in spatial :raw-html:`
` - axis plane angles - - AXIS_DIFF - - Diagnostic - - MTD - - MTD obj - * - Minimum distance between :raw-html:`
` - the boundaries of two objects - - BOUNDARY :raw-html:`
` - _DIST - - Diagnostic - - MODE - - MODE obj - * - Total great circle distance :raw-html:`
` - travelled by the 2D spatial :raw-html:`
` - centroid over the lifetime :raw-html:`
` - of the 3D object - - CDIST :raw-html:`
` - _TRAVELLED - - Diagnostic - - MTD - - MTD 3D obj - * - Distance between two :raw-html:`
` - objects centroids :raw-html:`
` - (in grid units) - - CENTROID :raw-html:`
` - _DIST - - Diagnostic - - MODE - - MODE obj - * - Latitude of centroid :raw-html:`
` - - CENTROID :raw-html:`
` - _LAT - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Longitude of centroid :raw-html:`
` - - CENTROID :raw-html:`
` - _LON - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Time coordinate of centroid - - CENTROID_T - - Diagnostic - - MTD - - MTD 3D obj - * - X coordinate of centroid :raw-html:`
` - - CENTROID_X - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Y coordinate of centroid :raw-html:`
` - - CENTROID_Y - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 2D & 3D obj :raw-html:`
` - MODE obj - * - Ratio of the difference :raw-html:`
` - between the area of an :raw-html:`
` - object and the area of :raw-html:`
` - its convex hull divided :raw-html:`
` - by the area of the :raw-html:`
` - complex hull (unitless) - - COMPLEXITY - - Diagnostic - - MODE - - MODE obj - * - Ratio of complexities of :raw-html:`
` - two objects defined as :raw-html:`
` - the lesser of the forecast :raw-html:`
` - complexity divided by the :raw-html:`
` - observation complexity or :raw-html:`
` - its reciprocal (unitless) - - COMPLEXITY :raw-html:`
` - _RATIO - - Diagnostic - - MODE - - MODE obj - * - Minimum distance between :raw-html:`
` - the convex hulls of two :raw-html:`
` - objects (in grid units) - - CONVEX_HULL :raw-html:`
` - _DIST - - Diagnostic - - MODE - - MODE obj - * - Radius of curvature - - CURVATURE - - Diagnostic - - MODE - - MODE obj - * - Ratio of the curvature - - CURVATURE :raw-html:`
` - _RATIO - - Diagnostic - - MODE - - MODE obj - * - Center of curvature :raw-html:`
` - (in grid coordinates) - - CURVATURE :raw-html:`
` - _X - - Diagnostic - - MODE - - MODE obj - * - Center of curvature :raw-html:`
` - (in grid coordinates) - - CURVATURE :raw-html:`
` - _Y - - Diagnostic - - MODE - - MODE obj - * - Difference in object :raw-html:`
` - direction of movement - - DIRECTION :raw-html:`
` - _DIFF - - Diagnostic - - MTD - - MTD 3D obj - * - Difference in the :raw-html:`
` - lifetimes of the :raw-html:`
` - two objects - - DURATION :raw-html:`
` - _DIFF - - Diagnostic - - MTD - - MTD 3D obj - * - Object end time - - END_TIME - - Diagnostic - - MTD - - MTD 3D obj - * - Difference in object :raw-html:`
` - ending time steps - - END_TIME :raw-html:`
` - _DELTA - - Diagnostic - - MTD - - MTD 3D obj - * - Number of forecast :raw-html:`
` - clusters - - fcst_clus - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of all :raw-html:`
` - of the cluster forecast :raw-html:`
` - objects - - fcst_clus :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point Latitude - - fcst_clus :raw-html:`
` - _hull_lat - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point Longitude - - fcst_clus :raw-html:`
` - _hull _lon - - Diagnostic - - MODE - - MODE obj - * - Number of Forecast :raw-html:`
` - Cluster Convex Hull Points - - fcst_clus :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Starting Index - - fcst_clus :raw-html:`
` - _hull_start - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point X-Coordinate - - fcst_clus :raw-html:`
` - _hull_x - - Diagnostic - - MODE - - MODE obj - * - Forecast Cluster Convex :raw-html:`
` - Hull Point Y-Coordinate - - fcst_clus :raw-html:`
` - _hull_y - - Diagnostic - - MODE - - MODE obj - * - Forecast Object Raw :raw-html:`
` - Values - - fcst_obj :raw-html:`
` - _raw - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - forecast objects - - fcst_simp - - Diagnostic - - MODE - - MODE obj - * - Number of points used :raw-html:`
` - to define the boundaries :raw-html:`
` - of all of the simple :raw-html:`
` - forecast objects - - fcst_simp :raw-html:`
` - _bdy - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Latitude - - fcst_simp :raw-html:`
` - _bdy_lat - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Longitude - - fcst_simp :raw-html:`
` - _bdy_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Forecast :raw-html:`
` - Simple Boundary Points - - fcst_simp :raw-html:`
` - _bdy_npts - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Starting Index - - fcst_simp :raw-html:`
` - _bdy_start - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary X-Coordinate - - fcst_simp :raw-html:`
` - _bdy_x - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple :raw-html:`
` - Boundary Y-Coordinate - - fcst_simp :raw-html:`
` - _bdy_y - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of all :raw-html:`
` - of the simple forecast :raw-html:`
` - objects - - fcst_simp :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point Latitude - - fcst_simp :raw-html:`
` - _hull_lat - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point Longitude - - fcst_simp :raw-html:`
` - _hull_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Forecast :raw-html:`
` - Simple Convex Hull Points - - fcst_simp :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Starting Index - - fcst_simp :raw-html:`
` - _hull_start - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point X-Coordinate - - fcst_simp :raw-html:`
` - _hull_x - - Diagnostic - - MODE - - MODE obj - * - Forecast Simple Convex :raw-html:`
` - Hull Point Y-Coordinate - - fcst_simp :raw-html:`
` - _hull_y - - Diagnostic - - MODE - - MODE obj - * - Number of thresholds :raw-html:`
` - applied to the forecast - - fcst :raw-html:`
` - _thresh :raw-html:`
` - _length - - Diagnostic - - MODE - - MODE obj - * - Number of thresholds :raw-html:`
` - applied to the forecast - - fcst_thresh :raw-html:`
` - _length - - Diagnostic - - MODE - - MODE obj - * - Forecast energy squared :raw-html:`
` - for this scale - - FENERGY - - - - Wavelet-Stat - - ISC - * - Mean of absolute value :raw-html:`
` - of forecast gradients - - FGBAR - - - - Grid-Stat - - GRAD - * - Ratio of forecast and :raw-html:`
` - observed gradients - - FGOG_RATIO - - - - Grid-Stat - - GRAD - * - Pratt’s Figure of Merit :raw-html:`
` - from observation to :raw-html:`
` - forecast - - FOM_FO - - Diagnostic - - Grid-Stat - - DMAP - * - Maximum of FOM_FO :raw-html:`
` - and FOM_OF - - FOM_MAX - - Diagnostic - - Grid-Stat - - DMAP - * - Mean of FOM_FO :raw-html:`
` - and FOM_OF :raw-html:`
` - - FOM_MEAN - - Diagnostic - - Grid-Stat - - DMAP - * - Minimum of FOM_FO :raw-html:`
` - and FOM_OF - - FOM_MIN - - Diagnostic - - Grid-Stat - - DMAP - * - Pratt’s Figure of Merit :raw-html:`
` - from forecast to :raw-html:`
` - observation - - FOM_OF - - Diagnostic - - Grid-Stat - - DMAP - * - Distance between the :raw-html:`
` - forecast and Best track :raw-html:`
` - genesis events (km) - - GEN_DIST - - Diagnostic - - TC-Gen - - GENMPR - * - Forecast minus Best track :raw-html:`
` - genesis time in HHMMSS :raw-html:`
` - format - - GEN_TDIFF - - Diagnostic - - TC-Gen - - GENMPR - * - Hausdorff Distance - - HAUSDORFF - - Diagnostic - - Grid-Stat - - DMAP - * - Best track genesis minus :raw-html:`
` - forecast initialization :raw-html:`
` - time in HHMMSS format - - INIT_TDIFF - - Diagnostic - - TC-Gen - - GENMPR - * - 10th, 25th, 50th, 75th, :raw-html:`
` - 90th, and user-specified :raw-html:`
` - percentiles of :raw-html:`
` - intensity of the raw :raw-html:`
` - field within the :raw-html:`
` - object or time slice - - INTENSITY :raw-html:`
` - _10, _25, :raw-html:`
` - _50, _75, :raw-html:`
` - _90, _NN - - Diagnostic - - MODE - - MODE obj - * - Sum of the intensities of :raw-html:`
` - the raw field within the :raw-html:`
` - object (variable units) - - INTENSITY :raw-html:`
` - _SUM - - Diagnostic - - MODE - - MODE obj - * - Total interest for this :raw-html:`
` - object pair - - INTEREST - - Diagnostic - - MTD :raw-html:`
` - MODE - - MTD 3D obj :raw-html:`
` - MODE obj - * - Intersection area of two :raw-html:`
` - objects (in grid squares) - - INTERSECT :raw-html:`
` - ION_AREA - - Diagnostic - - MODE - - MODE obj - * - Ratio of intersection area :raw-html:`
` - to the lesser of the :raw-html:`
` - forecast and observation :raw-html:`
` - object areas (unitless) - - INTERSECT :raw-html:`
` - ION_OVER :raw-html:`
` - _AREA - - Diagnostic - - MODE - - MODE obj - * - “Volume” of object :raw-html:`
` - intersection - - INTERSECT :raw-html:`
` - ION_VOLUME - - Diagnostic - - MTD - - MTD 3D obj - * - The intensity scale :raw-html:`
` - skill score - - ISC - - - - Wavelet-Stat - - ISC - * - The scale at which all :raw-html:`
` - information following :raw-html:`
` - applies - - ISCALE - - - - Wavelet-Stat - - ISC - * - Dimension of the latitude - - LAT - - Diagnostic - - MODE - - MODE obj - * - Length of the :raw-html:`
` - enclosing rectangle - - LENGTH - - Diagnostic - - MODE - - MODE obj - * - Dimension of the longitude - - LON - - Diagnostic - - MODE - - MODE obj - * - Mean of maximum of :raw-html:`
` - absolute values of :raw-html:`
` - forecast and observed :raw-html:`
` - gradients - - MGBAR - - - - Grid-Stat - - GRAD - * - Number of cluster objects - - N_CLUS - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - forecast objects - - N_FCST_SIMP - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - observation objects - - N_OBS_SIMP - - Diagnostic - - MODE - - MODE obj - * - Number of observed :raw-html:`
` - clusters - - obs_clus - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of all of :raw-html:`
` - the cluster observation :raw-html:`
` - objects - - obs_clus :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point Latitude - - obs_clus :raw-html:`
` - _hull_lat - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point Longitude - - obs_clus :raw-html:`
` - _hull_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Observation :raw-html:`
` - Cluster Convex Hull Points - - obs_clus :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Starting Index - - obs_clus :raw-html:`
` - _hull_start - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point X-Coordinate - - obs_clus :raw-html:`
` - _hull_x - - Diagnostic - - MODE - - MODE obj - * - Observation Cluster Convex :raw-html:`
` - Hull Point Y-Coordinate - - obs_clus :raw-html:`
` - _hull_y - - Diagnostic - - MODE - - MODE obj - * - Number of simple :raw-html:`
` - observation objects - - obs_simp - - Diagnostic - - MODE - - MODE obj - * - Number of points used :raw-html:`
` - to define the boundaries :raw-html:`
` - of the simple observation :raw-html:`
` - objects - - obs_simp :raw-html:`
` - _bdy - - Diagnostic - - MODE - - MODE obj - * - Observation Simple :raw-html:`
` - Boundary Point Latitude - - obs_simp :raw-html:`
` - _bdy_lat - - Diagnostic - - MODE - - MODE obj - * - Observation Simple :raw-html:`
` - Boundary Point Longitude - - obs_simp :raw-html:`
` - _bdy_lon - - Diagnostic - - MODE - - MODE obj - * - Number of Observation :raw-html:`
` - Simple Boundary Points - - obs_simp :raw-html:`
` - _bdy_npts - - Diagnostic - - MODE - - MODE obj - * - Number of points used to :raw-html:`
` - define the hull of the :raw-html:`
` - simple observation objects - - obs_simp :raw-html:`
` - _hull - - Diagnostic - - MODE - - MODE obj - * - Number of Observation :raw-html:`
` - Simple Convex Hull Points - - obs_simp :raw-html:`
` - _hull_npts - - Diagnostic - - MODE - - MODE obj - * - Observed energy squared :raw-html:`
` - for this scale - - OENERGY - - - - Wavelet-Stat - - ISC - * - Mean of absolute value :raw-html:`
` - of observed gradients - - OGBAR - - - - Grid-Stat - - GRAD - * - Ratio of the nth percentile :raw-html:`
` - (INTENSITY_NN column) of :raw-html:`
` - intensity of the two :raw-html:`
` - objects - - PERCENTILE :raw-html:`
` - _INTENSITY :raw-html:`
` - _RATIO - - Diagnostic - - MODE - - MODE obj - * - Spatial distance between :raw-html:`
` - (𝑥,𝑦)(x,y) coordinates of :raw-html:`
` - object spacetime centroid - - SPACE :raw-html:`
` - _CENTROID :raw-html:`
` - _DIST - - Diagnostic - - MTD - - MTD 3D obs - * - Difference in object speeds - - SPEED_DELTA - - Diagnostic - - MTD - - MTD 3D obs - * - Difference in object :raw-html:`
` - starting time steps - - START_TIME :raw-html:`
` - _DELTA - - Diagnostic - - MTD - - MTD 3D obj - * - Symmetric difference of :raw-html:`
` - two objects :raw-html:`
` - (in grid squares) - - SYMMETRIC :raw-html:`
` - _DIFF - - Diagnostic - - MODE - - MODE obj - * - Difference in t index of :raw-html:`
` - object spacetime centroid - - TIME :raw-html:`
` - _CENTROID :raw-html:`
` - _DELTA - - Diagnostic - - MTD - - MTD 3D obj - * - Union area of :raw-html:`
` - two objects :raw-html:`
` - (in grid squares) - - UNION_AREA - - Diagnostic - - MODE - - MODE obj - * - Integer count of the :raw-html:`
` - number of 3D “cells” :raw-html:`
` - in an object - - VOLUME - - Diagnostic - - MTD - - MTD 3D obj - * - Forecast object volume :raw-html:`
` - divided by observation :raw-html:`
` - object volume - - VOLUME :raw-html:`
` - _RATIO - - Diagnostic - - MTD - - MTD 3D obj - * - Width of the enclosing :raw-html:`
` - rectangle (in grid units) - - WIDTH - - Diagnostic - - MODE - - MODE obj - * - X component of :raw-html:`
` - object velocity - - X_DOT - - Diagnostic - - MTD - - MTD 3D obj - * - X component position :raw-html:`
` - error (nm) - - X_ERR - - Diagnostic - - TC-Pairs - - PROBRIRW - * - X component position :raw-html:`
` - error (nm) - - X_ERR - - Diagnostic - - TC-Pairs - - TCMPR - * - y component of :raw-html:`
` - object velocity - - Y_DOT - - Diagnostic - - MTD - - MTD 3D obj - * - Y component position :raw-html:`
` - error (nm) - - Y_ERR - - Diagnostic - - TC-Pairs - - PROBRIRW :raw-html:`
` - TCMPR - * - Zhu’s Measure from :raw-html:`
` - observation to forecast - - ZHU_FO - - Diagnostic - - Grid-Stat - - DMAP - * - Maximum of ZHU_FO :raw-html:`
` - and ZHU_OF - - ZHU_MAX - - Diagnostic - - Grid-Stat - - DMAP - * - Mean of ZHU_FO :raw-html:`
` - and ZHU_OF - - ZHU_MEAN - - Diagnostic - - Grid-Stat - - DMAP - * - Minimum of ZHU_FO :raw-html:`
` - and ZHU_OF - - ZHU_MIN - - Diagnostic - - Grid-Stat - - DMAP - * - Zhu’s Measure from :raw-html:`
` - forecast to observation - - ZHU_OF - - Diagnostic - - Grid-Stat - - DMAP From d6231c9ca2ac938db9b1026298f289930ed5c300 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 11:01:09 -0700 Subject: [PATCH 230/770] test. breaking out directories A-B and C-D #1049 --- docs/Users_Guide/statistics_list.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index fa528755bb..cdd104fa2b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -15,7 +15,7 @@ Statistics Database .. role:: raw-html(raw) :format: html -.. list-table:: Statistics List +.. list-table:: Statistics List A-B :widths: auto :header-rows: 1 @@ -129,6 +129,14 @@ Statistics Database - Point-Stat :raw-html:`
` Grid-Stat - PSTD + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List C-D + :widths: auto + :header-rows: 1 + * - Calibration when forecast :raw-html:`
` is between the ith and :raw-html:`
` i+1th probability :raw-html:`
` From fb5eacf89e5e34cbdddbd3cdd31eb0775c4ccef6 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 11:17:10 -0700 Subject: [PATCH 231/770] testing with section names #1049 --- docs/Users_Guide/statistics_list.rst | 32 ++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index cdd104fa2b..8e8e659684 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -12,6 +12,9 @@ METplus Statistics & Diagnostics Statistics Database =================== +Statistics List A-B +___________________ + .. role:: raw-html(raw) :format: html @@ -130,13 +133,23 @@ Statistics Database Grid-Stat - PSTD +Statistics List C-E +___________________ + .. role:: raw-html(raw) :format: html -.. list-table:: Statistics List C-D +.. list-table:: Statistics List C-E :widths: auto :header-rows: 1 - + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Calibration when forecast :raw-html:`
` is between the ith and :raw-html:`
` i+1th probability :raw-html:`
` @@ -289,6 +302,21 @@ Statistics Database Ensemble-Stat - CNT :raw-html:`
` SSVAR + +Statistics List F +_________________ + +.. list-table:: Statistics List F + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Forecast rate/event :raw-html:`
` frequency - F_RATE From 95cb9b7c7927918837922c3441b65d5f2c2c2d1b Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 11:37:22 -0700 Subject: [PATCH 232/770] alphbetical sections for statistics #1049 --- docs/Users_Guide/statistics_list.rst | 77 +++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 8e8e659684..cd1e14d0ad 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -523,6 +523,25 @@ _________________ - MODE :raw-html:`
` CTC :raw-html:`
` NBRCTC + +Statistics List G-M +___________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List G-M + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type + * - Gerrity Score and :raw-html:`
` bootstrap confidence limits - GER @@ -709,7 +728,25 @@ _________________ - Continuous - Point-Stat :raw-html:`
` Grid-Stat - - VCNT + - VCNT + +Statistics List N-O +___________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List N-O + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Dimension of the :raw-html:`
` contingency table & the :raw-html:`
` total number of :raw-html:`
` @@ -866,6 +903,25 @@ _________________ - Point-Stat :raw-html:`
` Grid-Stat - PJC + + +Statistics List P-R +___________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List P-R + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Probability Integral :raw-html:`
` Transform - PIT @@ -1067,6 +1123,25 @@ _________________ - Ensemble - Ensemble-Stat - RPS + + + Statistics List S-Z +___________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List S-Z + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - S1 score - S1 - Continuous From 37146873ed1d7a358ac54f370df4a047265077bf Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 11:46:53 -0700 Subject: [PATCH 233/770] making U-Z statistics list #1049 --- docs/Users_Guide/statistics_list.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index cd1e14d0ad..eee949b2dc 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1125,13 +1125,13 @@ ___________________ - RPS - Statistics List S-Z + Statistics List S-T ___________________ .. role:: raw-html(raw) :format: html -.. list-table:: Statistics List S-Z +.. list-table:: Statistics List S-T :widths: auto :header-rows: 1 @@ -1237,6 +1237,24 @@ ___________________ - Continuous - TC-Pairs - TCMPR + +Statistics List U-Z +___________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Statistics List U-Z + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Mean U-component :raw-html:`
` Forecast Anomaly - UFABAR From 1156cb0823e8cb6e167f2c76cfdde40d9f3d3cf3 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 11:55:19 -0700 Subject: [PATCH 234/770] alpabetical breaks for diagnostics list #1049 --- docs/Users_Guide/statistics_list.rst | 101 ++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index eee949b2dc..bf734624e6 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1425,11 +1425,13 @@ Diagnostics Database Statistic Type - no more than 19 characters METplus Line Type - currently unlimited (approx 33 characters) +Diagnostics List A-B +____________________ .. role:: raw-html(raw) :format: html -.. list-table:: Diagnostics List +.. list-table:: Diagnostics List A-B :widths: auto :header-rows: 1 @@ -1496,6 +1498,24 @@ Diagnostics Database - Diagnostic - MODE - MODE obj + +Diagnostics List C-E +____________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Diagnostics List C-E + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Total great circle distance :raw-html:`
` travelled by the 2D spatial :raw-html:`
` centroid over the lifetime :raw-html:`
` @@ -1629,6 +1649,24 @@ Diagnostics Database - Diagnostic - MTD - MTD 3D obj + +Diagnostics List F +__________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Diagnostics List F + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Number of forecast :raw-html:`
` clusters - fcst_clus @@ -1865,7 +1903,26 @@ Diagnostics Database - FOM_OF - Diagnostic - Grid-Stat - - DMAP + - DMAP + + +Diagnostics List G-L +____________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Diagnostics List G-L + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Distance between the :raw-html:`
` forecast and Best track :raw-html:`
` genesis events (km) @@ -1974,6 +2031,25 @@ Diagnostics Database - Diagnostic - MODE - MODE obj + + +Diagnostics List M-O +____________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Diagnostics List M-O + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Mean of maximum of :raw-html:`
` absolute values of :raw-html:`
` forecast and observed :raw-html:`
` @@ -2118,7 +2194,26 @@ Diagnostics Database - OGBAR - - Grid-Stat - - GRAD + - GRAD + + +Diagnostics List P-Z +____________________ + +.. role:: raw-html(raw) + :format: html + +.. list-table:: Diagnostics List P-Z + :widths: auto + :header-rows: 1 + + * - Statistics :raw-html:`
` + Long Name + - METplus Name + - Statistic Type + - Tools + - METplus :raw-html:`
` + Line Type * - Ratio of the nth percentile :raw-html:`
` (INTENSITY_NN column) of :raw-html:`
` intensity of the two :raw-html:`
` From 6350fa1f3f5553524e2721b2a741c6c7849e4ffe Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 12:03:22 -0700 Subject: [PATCH 235/770] trying to fix errors #1049 --- docs/Users_Guide/statistics_list.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index bf734624e6..cfe77d1924 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1237,6 +1237,7 @@ ___________________ - Continuous - TC-Pairs - TCMPR + Statistics List U-Z ___________________ From 4a2360982c8e4320d29cc61c20b7ecc3b57b9023 Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 12:10:11 -0700 Subject: [PATCH 236/770] trying to fix errors take 2 #1049 --- docs/Users_Guide/statistics_list.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index cfe77d1924..70d5578b26 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1126,7 +1126,7 @@ ___________________ Statistics List S-T -___________________ +____________________ .. role:: raw-html(raw) :format: html @@ -1237,6 +1237,7 @@ ___________________ - Continuous - TC-Pairs - TCMPR + Statistics List U-Z From cf9723ee24f4f5875b9f2cb1fcc93d8205c2d33a Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Wed, 17 Nov 2021 13:34:13 -0700 Subject: [PATCH 237/770] trying to fix errors take 3 #1049 --- docs/Users_Guide/statistics_list.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 70d5578b26..144eb4ea27 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -12,6 +12,7 @@ METplus Statistics & Diagnostics Statistics Database =================== + Statistics List A-B ___________________ @@ -907,7 +908,8 @@ ___________________ Statistics List P-R ___________________ - + + .. role:: raw-html(raw) :format: html @@ -1127,7 +1129,8 @@ ___________________ Statistics List S-T ____________________ - + + .. role:: raw-html(raw) :format: html From df51d9e5c897ff9701505816c9126e353044850f Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Wed, 17 Nov 2021 14:06:46 -0700 Subject: [PATCH 238/770] Removed a unnecessary space #1049 --- docs/Users_Guide/statistics_list.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 144eb4ea27..0ff118650b 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1127,8 +1127,8 @@ ___________________ - RPS - Statistics List S-T -____________________ +Statistics List S-T +___________________ .. role:: raw-html(raw) From 9dd39098aae2c9da5250c473aaf0639ed367b22d Mon Sep 17 00:00:00 2001 From: Lisa Goodrich Date: Thu, 18 Nov 2021 10:14:35 -0700 Subject: [PATCH 239/770] Making all METplus Names CAPITAL LETTERS #1049 --- docs/Users_Guide/statistics_list.rst | 158 +++++++++++++-------------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/docs/Users_Guide/statistics_list.rst b/docs/Users_Guide/statistics_list.rst index 0ff118650b..7a0969eb58 100644 --- a/docs/Users_Guide/statistics_list.rst +++ b/docs/Users_Guide/statistics_list.rst @@ -1674,7 +1674,7 @@ __________________ Line Type * - Number of forecast :raw-html:`
` clusters - - fcst_clus + - FCST_CLUS - Diagnostic - MODE - MODE obj @@ -1682,63 +1682,63 @@ __________________ define the hull of all :raw-html:`
` of the cluster forecast :raw-html:`
` objects - - fcst_clus :raw-html:`
` - _hull + - FCST_CLUS :raw-html:`
` + _HULL - Diagnostic - MODE - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Latitude - - fcst_clus :raw-html:`
` - _hull_lat + - FCST_CLUS :raw-html:`
` + _HULL_LAT - Diagnostic - MODE - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Longitude - - fcst_clus :raw-html:`
` - _hull _lon + - FCST_CLUS :raw-html:`
` + _HULL _LON - Diagnostic - MODE - MODE obj * - Number of Forecast :raw-html:`
` Cluster Convex Hull Points - - fcst_clus :raw-html:`
` - _hull_npts + - FCST_CLUS :raw-html:`
` + _HULL_NPTS - Diagnostic - MODE - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Starting Index - - fcst_clus :raw-html:`
` - _hull_start + - FCST_CLUS :raw-html:`
` + _HULL_START - Diagnostic - MODE - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point X-Coordinate - - fcst_clus :raw-html:`
` - _hull_x + - FCST_CLUS :raw-html:`
` + _HULL_X - Diagnostic - MODE - MODE obj * - Forecast Cluster Convex :raw-html:`
` Hull Point Y-Coordinate - - fcst_clus :raw-html:`
` - _hull_y + - FCST_CLUS :raw-html:`
` + _HULL_Y - Diagnostic - MODE - MODE obj * - Forecast Object Raw :raw-html:`
` Values - - fcst_obj :raw-html:`
` - _raw + - FCST_OBJ :raw-html:`
` + _RAW - Diagnostic - MODE - MODE obj * - Number of simple :raw-html:`
` forecast objects - - fcst_simp + - FCST_SIMP - Diagnostic - MODE - MODE obj @@ -1746,50 +1746,50 @@ __________________ to define the boundaries :raw-html:`
` of all of the simple :raw-html:`
` forecast objects - - fcst_simp :raw-html:`
` - _bdy + - FCST_SIMP :raw-html:`
` + _BDY - Diagnostic - MODE - MODE obj * - Forecast Simple :raw-html:`
` Boundary Latitude - - fcst_simp :raw-html:`
` - _bdy_lat + - FCST_SIMP :raw-html:`
` + _BDY_LAT - Diagnostic - MODE - MODE obj * - Forecast Simple :raw-html:`
` Boundary Longitude - - fcst_simp :raw-html:`
` - _bdy_lon + - FCST_SIMP :raw-html:`
` + _BDY_LON - Diagnostic - MODE - MODE obj * - Number of Forecast :raw-html:`
` Simple Boundary Points - - fcst_simp :raw-html:`
` - _bdy_npts + - FCST_SIMP :raw-html:`
` + _BDY_NPTS - Diagnostic - MODE - MODE obj * - Forecast Simple :raw-html:`
` Boundary Starting Index - - fcst_simp :raw-html:`
` - _bdy_start + - FCST_SIMP :raw-html:`
` + _BDY_START - Diagnostic - MODE - MODE obj * - Forecast Simple :raw-html:`
` Boundary X-Coordinate - - fcst_simp :raw-html:`
` - _bdy_x + - FCST_SIMP :raw-html:`
` + _BDY_X - Diagnostic - MODE - MODE obj * - Forecast Simple :raw-html:`
` Boundary Y-Coordinate - - fcst_simp :raw-html:`
` - _bdy_y + - FCST_SIMP :raw-html:`
` + _BDY_Y - Diagnostic - MODE - MODE obj @@ -1797,65 +1797,65 @@ __________________ define the hull of all :raw-html:`
` of the simple forecast :raw-html:`
` objects - - fcst_simp :raw-html:`
` - _hull + - FCST_SIMP :raw-html:`
` + _HULL - Diagnostic - MODE - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point Latitude - - fcst_simp :raw-html:`
` - _hull_lat + - FCST_SIMP :raw-html:`
` + _HULL_LAT - Diagnostic - MODE - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point Longitude - - fcst_simp :raw-html:`
` - _hull_lon + - FCST_SIMP :raw-html:`
` + _HULL_LON - Diagnostic - MODE - MODE obj * - Number of Forecast :raw-html:`
` Simple Convex Hull Points - - fcst_simp :raw-html:`
` - _hull_npts + - FCST_SIMP :raw-html:`
` + _HULL_NPTS - Diagnostic - MODE - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Starting Index - - fcst_simp :raw-html:`
` - _hull_start + - FCST_SIMP :raw-html:`
` + _HULL_START - Diagnostic - MODE - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point X-Coordinate - - fcst_simp :raw-html:`
` - _hull_x + - FCST_SIMP :raw-html:`
` + _HULL_X - Diagnostic - MODE - MODE obj * - Forecast Simple Convex :raw-html:`
` Hull Point Y-Coordinate - - fcst_simp :raw-html:`
` - _hull_y + - FCST_SIMP :raw-html:`
` + _HULL_Y - Diagnostic - MODE - MODE obj * - Number of thresholds :raw-html:`
` applied to the forecast - - fcst :raw-html:`
` - _thresh :raw-html:`
` - _length + - FCST :raw-html:`
` + _THRESH :raw-html:`
` + _LENGTH - Diagnostic - MODE - MODE obj * - Number of thresholds :raw-html:`
` applied to the forecast - - fcst_thresh :raw-html:`
` - _length + - FCST_THRESH :raw-html:`
` + _LENGTH - Diagnostic - MODE - MODE obj @@ -2082,7 +2082,7 @@ ____________________ - MODE obj * - Number of observed :raw-html:`
` clusters - - obs_clus + - OBS_CLUS - Diagnostic - MODE - MODE obj @@ -2090,56 +2090,56 @@ ____________________ define the hull of all of :raw-html:`
` the cluster observation :raw-html:`
` objects - - obs_clus :raw-html:`
` - _hull + - OBS_CLUS :raw-html:`
` + _HULL - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Latitude - - obs_clus :raw-html:`
` - _hull_lat + - OBS_CLUS :raw-html:`
` + _HULL_LAT - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Longitude - - obs_clus :raw-html:`
` - _hull_lon + - OBS_CLUS :raw-html:`
` + _HULL_LON - Diagnostic - MODE - MODE obj * - Number of Observation :raw-html:`
` Cluster Convex Hull Points - - obs_clus :raw-html:`
` - _hull_npts + - OBS_CLUS :raw-html:`
` + _HULL_NPTS - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Starting Index - - obs_clus :raw-html:`
` - _hull_start + - OBS_CLUS :raw-html:`
` + _HULL_START - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point X-Coordinate - - obs_clus :raw-html:`
` - _hull_x + - OBS_CLUS :raw-html:`
` + _HULL_X - Diagnostic - MODE - MODE obj * - Observation Cluster Convex :raw-html:`
` Hull Point Y-Coordinate - - obs_clus :raw-html:`
` - _hull_y + - OBS_CLUS :raw-html:`
` + _HULL_Y - Diagnostic - MODE - MODE obj * - Number of simple :raw-html:`
` observation objects - - obs_simp + - OBS_SIMP - Diagnostic - MODE - MODE obj @@ -2147,44 +2147,44 @@ ____________________ to define the boundaries :raw-html:`
` of the simple observation :raw-html:`
` objects - - obs_simp :raw-html:`
` - _bdy + - OBS_SIMP :raw-html:`
` + _BDY - Diagnostic - MODE - MODE obj * - Observation Simple :raw-html:`
` Boundary Point Latitude - - obs_simp :raw-html:`
` - _bdy_lat + - OBS_SIMP :raw-html:`
` + _BDY_LAT - Diagnostic - MODE - MODE obj * - Observation Simple :raw-html:`
` Boundary Point Longitude - - obs_simp :raw-html:`
` - _bdy_lon + - OBS_SIMP :raw-html:`
` + _BDY_LON - Diagnostic - MODE - MODE obj * - Number of Observation :raw-html:`
` Simple Boundary Points - - obs_simp :raw-html:`
` - _bdy_npts + - OBS_SIMP :raw-html:`
` + _BDY_NPTS - Diagnostic - MODE - MODE obj * - Number of points used to :raw-html:`
` define the hull of the :raw-html:`
` simple observation objects - - obs_simp :raw-html:`
` - _hull + - OBS_SIMP :raw-html:`
` + _HULL - Diagnostic - MODE - MODE obj * - Number of Observation :raw-html:`
` Simple Convex Hull Points - - obs_simp :raw-html:`
` - _hull_npts + - OBS_SIMP :raw-html:`
` + _HULL_NPTS - Diagnostic - MODE - MODE obj From 5d319799ef97668c097842a64500d3423d2d429d Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 6 Dec 2021 10:00:56 -0700 Subject: [PATCH 240/770] Feature 344 met util refactor (#1292) --- .github/jobs/run_diff_docker.py | 7 +- docs/Contributors_Guide/basic_components.rst | 16 +- .../test_string_template_substitution.py | 22 +- .../pytests/ascii2nc/test_ascii2nc_wrapper.py | 7 +- .../command_builder/test_command_builder.py | 183 +-- .../config_metplus/test_config_metplus.py | 964 ++++++++++- .../test_ensemble_stat_wrapper.py | 13 +- .../gen_ens_prod/test_gen_ens_prod_wrapper.py | 179 +-- .../grid_stat/test_grid_stat_wrapper.py | 12 +- .../pytests/ioda2nc/test_ioda2nc_wrapper.py | 6 +- .../pytests/met_config/test_met_config.py | 59 + .../test_met_dictionary_info.py | 29 - .../pytests/met_util/test_met_util.py | 1095 +------------ .../pytests/mode/test_mode_wrapper.py | 12 +- .../pytests/mtd/test_mtd_wrapper.py | 27 - .../pytests/pb2nc/test_pb2nc_wrapper.py | 8 +- .../point_stat/test_point_stat_wrapper.py | 26 +- .../pytests/tc_gen/test_tc_gen_wrapper.py | 6 +- metplus/util/__init__.py | 3 +- metplus/util/config_metplus.py | 1421 ++++++++++++++--- metplus/util/constants.py | 2 + {ci => metplus}/util/diff_util.py | 0 metplus/util/met_config.py | 710 ++++++++ metplus/util/met_dictionary_info.py | 110 -- metplus/util/met_util.py | 1243 +------------- metplus/util/string_template_substitution.py | 32 +- metplus/wrappers/ascii2nc_wrapper.py | 112 +- metplus/wrappers/command_builder.py | 803 +--------- metplus/wrappers/compare_gridded_wrapper.py | 27 +- metplus/wrappers/ensemble_stat_wrapper.py | 131 +- metplus/wrappers/extract_tiles_wrapper.py | 5 +- metplus/wrappers/gen_ens_prod_wrapper.py | 6 +- metplus/wrappers/grid_diag_wrapper.py | 7 +- metplus/wrappers/grid_stat_wrapper.py | 47 +- metplus/wrappers/ioda2nc_wrapper.py | 6 +- metplus/wrappers/make_plots_wrapper.py | 3 +- metplus/wrappers/mode_wrapper.py | 219 +-- metplus/wrappers/mtd_wrapper.py | 33 +- metplus/wrappers/pb2nc_wrapper.py | 90 +- metplus/wrappers/pcp_combine_wrapper.py | 5 +- metplus/wrappers/point_stat_wrapper.py | 54 +- metplus/wrappers/regrid_data_plane_wrapper.py | 8 +- metplus/wrappers/series_analysis_wrapper.py | 64 +- metplus/wrappers/stat_analysis_wrapper.py | 11 +- metplus/wrappers/tc_gen_wrapper.py | 12 +- metplus/wrappers/tc_pairs_wrapper.py | 76 +- metplus/wrappers/tc_stat_wrapper.py | 137 +- metplus/wrappers/tcrmw_wrapper.py | 158 +- 48 files changed, 3995 insertions(+), 4211 deletions(-) create mode 100644 internal_tests/pytests/met_config/test_met_config.py delete mode 100644 internal_tests/pytests/met_dictionary_info/test_met_dictionary_info.py create mode 100644 metplus/util/constants.py rename {ci => metplus}/util/diff_util.py (100%) create mode 100644 metplus/util/met_config.py delete mode 100644 metplus/util/met_dictionary_info.py diff --git a/.github/jobs/run_diff_docker.py b/.github/jobs/run_diff_docker.py index c6c3937434..85a3246a6a 100755 --- a/.github/jobs/run_diff_docker.py +++ b/.github/jobs/run_diff_docker.py @@ -13,9 +13,9 @@ import shutil GITHUB_WORKSPACE = os.environ.get('GITHUB_WORKSPACE') -# add ci/util to sys path to get diff utility +# add util directory to sys path to get diff utility diff_util_dir = os.path.join(GITHUB_WORKSPACE, - 'ci', + 'metplus', 'util') sys.path.insert(0, diff_util_dir) from diff_util import compare_dir @@ -23,9 +23,6 @@ TRUTH_DIR = '/data/truth' OUTPUT_DIR = '/data/output' DIFF_DIR = '/data/diff' -# DIFF_DIR = os.path.join(GITHUB_WORKSPACE, -# 'artifact', -# 'diff') def copy_diff_output(diff_files): """! Loop through difference output and copy files diff --git a/docs/Contributors_Guide/basic_components.rst b/docs/Contributors_Guide/basic_components.rst index e2a29ab574..86d3d57323 100644 --- a/docs/Contributors_Guide/basic_components.rst +++ b/docs/Contributors_Guide/basic_components.rst @@ -274,7 +274,7 @@ should be set. Add Support for MET Dictionary ------------------------------ -The handle_met_config_dict function can be used to easily set a MET config +The add_met_config_dict function can be used to easily set a MET config dictionary variable. The function takes 2 arguments: * dict_name: Name of the MET dictionary variable, i.e. distance_map. @@ -285,7 +285,7 @@ dictionary variable. The function takes 2 arguments: :: - self.handle_met_config_dict('fcst_genesis', { + self.add_met_config_dict('fcst_genesis', { 'vmax_thresh': 'thresh', 'mslp_thresh': 'thresh', }) @@ -319,7 +319,7 @@ a function is typically used to handle it. For example, this function is in CompareGriddedWrapper and is used by GridStat, PointStat, and EnsembleStat:: def handle_climo_cdf_dict(self): - self.handle_met_config_dict('climo_cdf', { + self.add_met_config_dict('climo_cdf', { 'cdf_bins': ('float', None, None, ['CLIMO_CDF_BINS']), 'center_bins': 'bool', 'write_bins': 'bool', @@ -333,26 +333,26 @@ the nickname 'CLIMO_CDF_BINS' allows the user to set the variable GRID_STAT_CLIMO_CDF_BINS instead. There are many MET config dictionaries that only contain beg and end to define -a window. A function in CommandBuilder called handle_met_config_window can be +a window. A function in CommandBuilder called add_met_config_window can be used to easily set these variable by only supplying the name of the MET dictionary variable. :: - def handle_met_config_window(self, dict_name): + def add_met_config_window(self, dict_name): """! Handle a MET config window dictionary. It is assumed that the dictionary only contains 'beg' and 'end' entries that are integers. @param dict_name name of MET dictionary """ - self.handle_met_config_dict(dict_name, { + self.add_met_config_dict(dict_name, { 'beg': 'int', 'end': 'int', }) This can be called from any wrapper, i.e. TCGen:: - self.handle_met_config_window('fcst_hr_window') + self.add_met_config_window('fcst_hr_window') This will check if TC_GEN_FCST_HR_WINDOW_BEGIN (or TC_GEN_FCST_HR_WINDOW_BEG) and TC_GEN_FCST_HR_WINDOW_END are set and override fcst_hr_window.beg and/or @@ -383,5 +383,5 @@ handle_climo_dict, handle_mask, and handle_interp_dict. if uses_field: items['field'] = ('string', 'remove_quotes') - self.handle_met_config_dict('interp', items) + self.add_met_config_dict('interp', items) diff --git a/internal_tests/pytests/StringTemplateSubstitution/test_string_template_substitution.py b/internal_tests/pytests/StringTemplateSubstitution/test_string_template_substitution.py index c45bba3820..b19a6c45f4 100644 --- a/internal_tests/pytests/StringTemplateSubstitution/test_string_template_substitution.py +++ b/internal_tests/pytests/StringTemplateSubstitution/test_string_template_substitution.py @@ -1,10 +1,11 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import pytest import logging import datetime +import os -from metplus.util import do_string_sub, parse_template +from metplus.util import do_string_sub, parse_template, get_time_from_file from metplus.util import get_tags,format_one_time_item, format_hms from metplus.util import add_to_dict, populate_match_dict, get_fmt_info @@ -595,3 +596,20 @@ def test_do_string_sub_no_recurse_no_missing(templ, expected_filename): basin=basin_regex, cyclone=cyclone_regex) assert(filename == expected_filename) + +@pytest.mark.parametrize( + 'filepath, template, expected_result', [ + (os.getcwd(), 'file.{valid?fmt=%Y%m%d%H}.ext', None), + ('file.2019020104.ext', 'file.{valid?fmt=%Y%m%d%H}.ext', datetime.datetime(2019, 2, 1, 4)), + ('filename.2019020104.ext', 'file.{valid?fmt=%Y%m%d%H}.ext', None), + ('file.2019020104.ext.gz', 'file.{valid?fmt=%Y%m%d%H}.ext', datetime.datetime(2019, 2, 1, 4)), + ('filename.2019020104.ext.gz', 'file.{valid?fmt=%Y%m%d%H}.ext', None), + ] +) +def test_get_time_from_file(filepath, template, expected_result): + result = get_time_from_file(filepath, template) + + if result is None: + assert expected_result is None + else: + assert result['valid'] == expected_result diff --git a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py index 8db28911a4..b7c3b72767 100644 --- a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py +++ b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py @@ -173,7 +173,12 @@ def test_ascii2nc_wrapper(metplus_config, config_overrides, assert(all_commands[0][0] == expected_cmd) env_vars = all_commands[0][1] - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # check that environment variables were set properly + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert (match is not None) diff --git a/internal_tests/pytests/command_builder/test_command_builder.py b/internal_tests/pytests/command_builder/test_command_builder.py index 29fede8943..355cb66b87 100644 --- a/internal_tests/pytests/command_builder/test_command_builder.py +++ b/internal_tests/pytests/command_builder/test_command_builder.py @@ -10,7 +10,7 @@ import datetime from metplus.wrappers.command_builder import CommandBuilder from metplus.util import time_util -from metplus.util import METConfigInfo as met_config +from metplus.util import METConfig @pytest.mark.parametrize( @@ -380,19 +380,6 @@ def test_handle_description(metplus_config, config_overrides, expected_value): cbw.handle_description() assert cbw.env_var_dict.get('METPLUS_DESC', '') == expected_value -@pytest.mark.parametrize( - 'input, output', [ - ('', 'NONE'), - ('NONE', 'NONE'), - ('FCST', 'FCST'), - ('OBS', 'OBS'), - ('G002', '"G002"'), - ] -) -def test_format_regrid_to_grid(metplus_config, input, output): - cbw = CommandBuilder(metplus_config()) - assert cbw.format_regrid_to_grid(input) == output - @pytest.mark.parametrize( 'config_overrides, set_to_grid, expected_dict', [ ({}, True, {'REGRID_TO_GRID': 'NONE'}), @@ -499,25 +486,29 @@ def test_handle_regrid_new(metplus_config, config_overrides, expected_output): True, 'test_string_1 = value_1;'), ] ) -def test_set_met_config_string(metplus_config, mp_config_name, met_config_name, +def test_add_met_config_string(metplus_config, mp_config_name, met_config_name, c_dict_key, remove_quotes, expected_output): cbw = CommandBuilder(metplus_config()) # set some config variables to test cbw.config.set('config', 'TEST_STRING_1', 'value_1') - c_dict = {} + extra_args = {} + if remove_quotes: + extra_args['remove_quotes'] = True - cbw.set_met_config_string(c_dict, - mp_config_name, - met_config_name, - c_dict_key=c_dict_key, - remove_quotes=remove_quotes) key = c_dict_key if key is None: - key = met_config_name.upper() + key = met_config_name + key = key.upper() + + cbw.add_met_config(name=met_config_name, + data_type='string', + env_var_name=key, + metplus_configs=[mp_config_name], + extra_args=extra_args) - assert c_dict.get(key, '') == expected_output + assert cbw.env_var_dict.get(f'METPLUS_{key}', '') == expected_output @pytest.mark.parametrize( 'mp_config_name,met_config_name,c_dict_key,uppercase,expected_output, is_ok', [ @@ -547,7 +538,7 @@ def test_set_met_config_string(metplus_config, mp_config_name, met_config_name, True, '', False), ] ) -def test_set_met_config_bool(metplus_config, mp_config_name, met_config_name, +def test_add_met_config_bool(metplus_config, mp_config_name, met_config_name, c_dict_key, uppercase, expected_output, is_ok): cbw = CommandBuilder(metplus_config()) @@ -556,18 +547,22 @@ def test_set_met_config_bool(metplus_config, mp_config_name, met_config_name, cbw.config.set('config', 'TEST_BOOL_3', False) cbw.config.set('config', 'TEST_BOOL_4', 'chicken') - c_dict = {} + extra_args = {} + if not uppercase: + extra_args['uppercase'] = False - cbw.set_met_config_bool(c_dict, - mp_config_name, - met_config_name, - c_dict_key=c_dict_key, - uppercase=uppercase) key = c_dict_key if key is None: - key = met_config_name.upper() + key = met_config_name + key = key.upper() + + cbw.add_met_config(name=met_config_name, + data_type='bool', + env_var_name=key, + metplus_configs=[mp_config_name], + extra_args=extra_args) - assert c_dict.get(key, '') == expected_output + assert cbw.env_var_dict.get(f'METPLUS_{key}', '') == expected_output assert cbw.isOK == is_ok # int @@ -590,7 +585,7 @@ def test_set_met_config_bool(metplus_config, mp_config_name, met_config_name, '', False), ] ) -def test_set_met_config_int(metplus_config, mp_config_name, met_config_name, +def test_add_met_config_int(metplus_config, mp_config_name, met_config_name, c_dict_key, expected_output, is_ok): cbw = CommandBuilder(metplus_config()) @@ -599,17 +594,17 @@ def test_set_met_config_int(metplus_config, mp_config_name, met_config_name, cbw.config.set('config', 'TEST_INT_3', -4) cbw.config.set('config', 'TEST_INT_4', 'chicken') - c_dict = {} - - cbw.set_met_config_int(c_dict, - mp_config_name, - met_config_name, - c_dict_key=c_dict_key) key = c_dict_key if key is None: - key = met_config_name.upper() + key = met_config_name + key = key.upper() - assert c_dict.get(key, '') == expected_output + cbw.add_met_config(name=met_config_name, + data_type='int', + env_var_name=key, + metplus_configs=[mp_config_name]) + + assert cbw.env_var_dict.get(f'METPLUS_{key}', '') == expected_output assert cbw.isOK == is_ok @pytest.mark.parametrize( @@ -631,7 +626,7 @@ def test_set_met_config_int(metplus_config, mp_config_name, met_config_name, '', False), ] ) -def test_set_met_config_float(metplus_config, mp_config_name, met_config_name, +def test_add_met_config_float(metplus_config, mp_config_name, met_config_name, c_dict_key, expected_output, is_ok): cbw = CommandBuilder(metplus_config()) @@ -640,17 +635,17 @@ def test_set_met_config_float(metplus_config, mp_config_name, met_config_name, cbw.config.set('config', 'TEST_FLOAT_3', 4) cbw.config.set('config', 'TEST_FLOAT_4', 'chicken') - c_dict = {} - - cbw.set_met_config_float(c_dict, - mp_config_name, - met_config_name, - c_dict_key=c_dict_key) key = c_dict_key if key is None: - key = met_config_name.upper() + key = met_config_name + key = key.upper() + + cbw.add_met_config(name=met_config_name, + data_type='float', + env_var_name=key, + metplus_configs=[mp_config_name]) - assert c_dict.get(key, '') == expected_output + assert cbw.env_var_dict.get(f'METPLUS_{key}', '') == expected_output assert cbw.isOK == is_ok @pytest.mark.parametrize( @@ -678,7 +673,7 @@ def test_set_met_config_float(metplus_config, mp_config_name, met_config_name, 'test_thresh_6 = NA;', True), ] ) -def test_set_met_config_thresh(metplus_config, mp_config_name, met_config_name, +def test_add_met_config_thresh(metplus_config, mp_config_name, met_config_name, c_dict_key, expected_output, is_ok): cbw = CommandBuilder(metplus_config()) @@ -689,17 +684,18 @@ def test_set_met_config_thresh(metplus_config, mp_config_name, met_config_name, cbw.config.set('config', 'TEST_THRESH_5', '>CDP40&&<=CDP50') cbw.config.set('config', 'TEST_THRESH_6', 'NA') - c_dict = {} - - cbw.set_met_config_thresh(c_dict, - mp_config_name, - met_config_name, - c_dict_key=c_dict_key) key = c_dict_key if key is None: - key = met_config_name.upper() + key = met_config_name + key = key.upper() - assert c_dict.get(key, '') == expected_output + cbw.add_met_config(name=met_config_name, + env_var_name=key, + data_type='thresh', + metplus_configs=[mp_config_name]) + + print(f"KEY: {key}, ENV VARS: {cbw.env_var_dict}") + assert cbw.env_var_dict.get(f'METPLUS_{key}', '') == expected_output assert cbw.isOK == is_ok @pytest.mark.parametrize( @@ -727,7 +723,7 @@ def test_set_met_config_thresh(metplus_config, mp_config_name, met_config_name, True, 'test_list_4 = [value_1, value2];'), ] ) -def test_set_met_config_list(metplus_config, mp_config_name, met_config_name, +def test_add_met_config_list(metplus_config, mp_config_name, met_config_name, c_dict_key, remove_quotes, expected_output): cbw = CommandBuilder(metplus_config()) @@ -736,18 +732,23 @@ def test_set_met_config_list(metplus_config, mp_config_name, met_config_name, cbw.config.set('config', 'TEST_LIST_3', "'value_1', 'value2'") cbw.config.set('config', 'TEST_LIST_4', '"value_1", "value2"') - c_dict = {} + extra_args = {} + if remove_quotes: + extra_args['remove_quotes'] = True - cbw.set_met_config_list(c_dict, - mp_config_name, - met_config_name, - c_dict_key=c_dict_key, - remove_quotes=remove_quotes) key = c_dict_key if key is None: - key = met_config_name.upper() + key = met_config_name + + key = key.upper() - assert c_dict.get(key, '') == expected_output + cbw.add_met_config(name=met_config_name, + data_type='list', + env_var_name=key, + metplus_configs=[mp_config_name], + extra_args=extra_args) + print(f"KEY: {key}, ENV VARS: {cbw.env_var_dict}") + assert cbw.env_var_dict.get(f'METPLUS_{key}', '') == expected_output @pytest.mark.parametrize( 'mp_config_name,allow_empty,expected_output', [ @@ -761,42 +762,28 @@ def test_set_met_config_list(metplus_config, mp_config_name, met_config_name, ('TEST_LIST_2', True, ''), ] ) -def test_set_met_config_list_allow_empty(metplus_config, mp_config_name, +def test_add_met_config_list_allow_empty(metplus_config, mp_config_name, allow_empty, expected_output): cbw = CommandBuilder(metplus_config()) # set some config variables to test cbw.config.set('config', 'TEST_LIST_1', '') - c_dict = {} + extra_args = {} + if allow_empty: + extra_args['allow_empty'] = True met_config_name = mp_config_name.lower() - cbw.set_met_config_list(c_dict, - mp_config_name, - met_config_name, - allow_empty=allow_empty) - - assert c_dict.get(mp_config_name, '') == expected_output + cbw.add_met_config(name=met_config_name, + data_type='list', + metplus_configs=[mp_config_name], + extra_args=extra_args) -@pytest.mark.parametrize( - 'data_type, expected_function', [ - ('int', 'set_met_config_int'), - ('float', 'set_met_config_float'), - ('list', 'set_met_config_list'), - ('string', 'set_met_config_string'), - ('thresh', 'set_met_config_thresh'), - ('bool', 'set_met_config_bool'), - ('bad_name', None), - ] -) -def test_set_met_config_function(metplus_config, data_type, expected_function): - cbw = CommandBuilder(metplus_config()) - function_found = cbw.set_met_config_function(data_type) - function_name = function_found.__name__ if function_found else None - assert(function_name == expected_function) + assert cbw.env_var_dict.get(f'METPLUS_{mp_config_name}', '') == expected_output + #assert c_dict.get(mp_config_name, '') == expected_output -def test_handle_met_config_dict(metplus_config): +def test_add_met_config_dict(metplus_config): dict_name = 'fcst_hr_window' beg = -3 end = 5 @@ -813,12 +800,12 @@ def test_handle_met_config_dict(metplus_config): 'end': 'int', } - cbw.handle_met_config_dict(dict_name, items) + cbw.add_met_config_dict(dict_name, items) print(f"env_var_dict: {cbw.env_var_dict}") actual_value = cbw.env_var_dict.get('METPLUS_FCST_HR_WINDOW_DICT') assert actual_value == expected_value -def test_handle_met_config_window(metplus_config): +def test_add_met_config_window(metplus_config): dict_name = 'fcst_hr_window' beg = -3 end = 5 @@ -830,7 +817,7 @@ def test_handle_met_config_window(metplus_config): cbw = CommandBuilder(config) cbw.app_name = 'tc_gen' - cbw.handle_met_config_window(dict_name) + cbw.add_met_config_window(dict_name) print(f"env_var_dict: {cbw.env_var_dict}") actual_value = cbw.env_var_dict.get('METPLUS_FCST_HR_WINDOW_DICT') assert actual_value == expected_value @@ -848,7 +835,7 @@ def test_add_met_config(metplus_config): expected_value = f'valid_freq = {value};' assert cbw.env_var_dict['METPLUS_VALID_FREQ'] == expected_value -def test_handle_met_config_dict_nested(metplus_config): +def test_add_met_config_dict_nested(metplus_config): dict_name = 'outer' beg = -3 end = 5 @@ -876,6 +863,6 @@ def test_handle_met_config_dict_nested(metplus_config): }), } - cbw.handle_met_config_dict(dict_name, items) + cbw.add_met_config_dict(dict_name, items) print(f"env_var_dict: {cbw.env_var_dict}") assert cbw.env_var_dict.get('METPLUS_OUTER_DICT') == expected_value diff --git a/internal_tests/pytests/config_metplus/test_config_metplus.py b/internal_tests/pytests/config_metplus/test_config_metplus.py index 3fef44ce23..1c55fbc973 100644 --- a/internal_tests/pytests/config_metplus/test_config_metplus.py +++ b/internal_tests/pytests/config_metplus/test_config_metplus.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import pytest +import pprint import os +from datetime import datetime from metplus.util import config_metplus @@ -28,9 +30,965 @@ def test_get_default_config_list(): expected_new = [os.path.join(new_parm_base, item) for item in new_list] expected_both = [os.path.join(both_parm_base, item) for item in both_list] - actual_old = config_metplus.get_default_config_list(old_parm_base) - actual_new = config_metplus.get_default_config_list(new_parm_base) - actual_both = config_metplus.get_default_config_list(both_parm_base) + actual_old = config_metplus._get_default_config_list(old_parm_base) + actual_new = config_metplus._get_default_config_list(new_parm_base) + actual_both = config_metplus._get_default_config_list(both_parm_base) assert actual_old == expected_old assert actual_new == expected_new assert actual_both == expected_both + +@pytest.mark.parametrize( + 'regex,index,id,expected_result', [ + # 0: No ID + (r'^FCST_VAR(\d+)_NAME$', 1, None, + {'1': [None], + '2': [None], + '4': [None]}), + # 1: ID and index 2 + (r'(\w+)_VAR(\d+)_NAME', 2, 1, + {'1': ['FCST'], + '2': ['FCST'], + '4': ['FCST']}), + # 2: index 1, ID 2, multiple identifiers + (r'^FCST_VAR(\d+)_(\w+)$', 1, 2, + {'1': ['NAME', 'LEVELS'], + '2': ['NAME'], + '4': ['NAME']}), + # 3: command that StatAnalysis wrapper uses + (r'MODEL(\d+)$', 1, None, + {'1': [None], + '2': [None],}), + # 4: TCPairs conensus logic + (r'^TC_PAIRS_CONSENSUS(\d+)_(\w+)$', 1, 2, + {'1': ['NAME', 'MEMBERS', 'REQUIRED', 'MIN_REQ'], + '2': ['NAME', 'MEMBERS', 'REQUIRED', 'MIN_REQ']}), + ] +) +def test_find_indices_in_config_section(metplus_config, regex, index, + id, expected_result): + config = metplus_config() + config.set('config', 'FCST_VAR1_NAME', 'name1') + config.set('config', 'FCST_VAR1_LEVELS', 'level1') + config.set('config', 'FCST_VAR2_NAME', 'name2') + config.set('config', 'FCST_VAR4_NAME', 'name4') + config.set('config', 'MODEL1', 'model1') + config.set('config', 'MODEL2', 'model2') + + config.set('config', 'TC_PAIRS_CONSENSUS1_NAME', 'name1') + config.set('config', 'TC_PAIRS_CONSENSUS1_MEMBERS', 'member1') + config.set('config', 'TC_PAIRS_CONSENSUS1_REQUIRED', 'True') + config.set('config', 'TC_PAIRS_CONSENSUS1_MIN_REQ', '1') + config.set('config', 'TC_PAIRS_CONSENSUS2_NAME', 'name2') + config.set('config', 'TC_PAIRS_CONSENSUS2_MEMBERS', 'member2') + config.set('config', 'TC_PAIRS_CONSENSUS2_REQUIRED', 'True') + config.set('config', 'TC_PAIRS_CONSENSUS2_MIN_REQ', '2') + + + indices = config_metplus.find_indices_in_config_section(regex, config, + index_index=index, + id_index=id) + + pp = pprint.PrettyPrinter() + print(f'Indices:') + pp.pprint(indices) + + assert indices == expected_result + +@pytest.mark.parametrize( + 'conf_items, met_tool, expected_result', [ + ({'CUSTOM_LOOP_LIST': "one, two, three"}, '', ['one', 'two', 'three']), + ({'CUSTOM_LOOP_LIST': "one, two, three", + 'GRID_STAT_CUSTOM_LOOP_LIST': "four, five",}, 'grid_stat', ['four', 'five']), + ({'CUSTOM_LOOP_LIST': "one, two, three", + 'GRID_STAT_CUSTOM_LOOP_LIST': "four, five",}, 'point_stat', ['one', 'two', 'three']), + ({'CUSTOM_LOOP_LIST': "one, two, three", + 'ASCII2NC_CUSTOM_LOOP_LIST': "four, five",}, 'ascii2nc', ['four', 'five']), + # fails to read custom loop list for point2grid because there are underscores in name + ({'CUSTOM_LOOP_LIST': "one, two, three", + 'POINT_2_GRID_CUSTOM_LOOP_LIST': "four, five",}, 'point2grid', ['one', 'two', 'three']), + ({'CUSTOM_LOOP_LIST': "one, two, three", + 'POINT2GRID_CUSTOM_LOOP_LIST': "four, five",}, 'point2grid', ['four', 'five']), + ] +) +def test_get_custom_string_list(metplus_config, conf_items, met_tool, expected_result): + config = metplus_config() + for conf_key, conf_value in conf_items.items(): + config.set('config', conf_key, conf_value) + + assert(config_metplus.get_custom_string_list(config, met_tool) == expected_result) + +@pytest.mark.parametrize( + 'config_var_name, expected_indices, set_met_tool', [ + ('FCST_GRID_STAT_VAR1_NAME', ['1'], True), + ('FCST_GRID_STAT_VAR2_INPUT_FIELD_NAME', ['2'], True), + ('FCST_GRID_STAT_VAR3_FIELD_NAME', ['3'], True), + ('BOTH_GRID_STAT_VAR4_NAME', ['4'], True), + ('BOTH_GRID_STAT_VAR5_INPUT_FIELD_NAME', ['5'], True), + ('BOTH_GRID_STAT_VAR6_FIELD_NAME', ['6'], True), + ('FCST_VAR7_NAME', ['7'], False), + ('FCST_VAR8_INPUT_FIELD_NAME', ['8'], False), + ('FCST_VAR9_FIELD_NAME', ['9'], False), + ('BOTH_VAR10_NAME', ['10'], False), + ('BOTH_VAR11_INPUT_FIELD_NAME', ['11'], False), + ('BOTH_VAR12_FIELD_NAME', ['12'], False), + ] +) +def test_find_var_indices_fcst(metplus_config, + config_var_name, + expected_indices, + set_met_tool): + config = metplus_config() + data_types = ['FCST'] + config.set('config', config_var_name, "NAME1") + met_tool = 'grid_stat' if set_met_tool else None + var_name_indices = config_metplus.find_var_name_indices(config, + data_types=data_types, + met_tool=met_tool) + + assert(len(var_name_indices) == len(expected_indices)) + for actual_index in var_name_indices: + assert(actual_index in expected_indices) + +@pytest.mark.parametrize( + 'data_type, met_tool, expected_out', [ + ('FCST', None, ['FCST_', + 'BOTH_',]), + ('OBS', None, ['OBS_', + 'BOTH_',]), + ('FCST', 'grid_stat', ['FCST_GRID_STAT_', + 'BOTH_GRID_STAT_', + 'FCST_', + 'BOTH_', + ]), + ('OBS', 'extract_tiles', ['OBS_EXTRACT_TILES_', + 'BOTH_EXTRACT_TILES_', + 'OBS_', + 'BOTH_', + ]), + ('ENS', None, ['ENS_']), + ('DATA', None, ['DATA_']), + ('DATA', 'tc_gen', ['DATA_TC_GEN_', + 'DATA_']), + + ] +) +def test_get_field_search_prefixes(data_type, met_tool, expected_out): + assert(config_metplus.get_field_search_prefixes(data_type, + met_tool) == expected_out) + +@pytest.mark.parametrize( + 'item_list, extension, is_valid', [ + (['FCST'], 'NAME', False), + (['OBS'], 'NAME', False), + (['FCST', 'OBS'], 'NAME', True), + (['BOTH'], 'NAME', True), + (['FCST', 'OBS', 'BOTH'], 'NAME', False), + (['FCST', 'ENS'], 'NAME', False), + (['OBS', 'ENS'], 'NAME', False), + (['FCST', 'OBS', 'ENS'], 'NAME', True), + (['BOTH', 'ENS'], 'NAME', True), + (['FCST', 'OBS', 'BOTH', 'ENS'], 'NAME', False), + + (['FCST', 'OBS'], 'THRESH', True), + (['BOTH'], 'THRESH', True), + (['FCST', 'OBS', 'BOTH'], 'THRESH', False), + (['FCST', 'OBS', 'ENS'], 'THRESH', True), + (['BOTH', 'ENS'], 'THRESH', True), + (['FCST', 'OBS', 'BOTH', 'ENS'], 'THRESH', False), + + (['FCST'], 'OPTIONS', True), + (['OBS'], 'OPTIONS', True), + (['FCST', 'OBS'], 'OPTIONS', True), + (['BOTH'], 'OPTIONS', True), + (['FCST', 'OBS', 'BOTH'], 'OPTIONS', False), + (['FCST', 'ENS'], 'OPTIONS', True), + (['OBS', 'ENS'], 'OPTIONS', True), + (['FCST', 'OBS', 'ENS'], 'OPTIONS', True), + (['BOTH', 'ENS'], 'OPTIONS', True), + (['FCST', 'OBS', 'BOTH', 'ENS'], 'OPTIONS', False), + + (['FCST', 'OBS', 'BOTH'], 'LEVELS', False), + (['FCST', 'OBS'], 'LEVELS', True), + (['BOTH'], 'LEVELS', True), + (['FCST', 'OBS', 'ENS'], 'LEVELS', True), + (['BOTH', 'ENS'], 'LEVELS', True), + + ] +) +def test_is_var_item_valid(metplus_config, item_list, extension, is_valid): + conf = metplus_config() + assert(config_metplus.is_var_item_valid(item_list, '1', extension, conf)[0] == is_valid) + +@pytest.mark.parametrize( + 'item_list, configs_to_set, is_valid', [ + + (['FCST'], {'FCST_VAR1_LEVELS': 'A06', + 'OBS_VAR1_NAME': 'script_name.py something else'}, True), + (['FCST'], {'FCST_VAR1_LEVELS': 'A06', + 'OBS_VAR1_NAME': 'APCP'}, False), + (['OBS'], {'OBS_VAR1_LEVELS': '"(*,*)"', + 'FCST_VAR1_NAME': 'script_name.py something else'}, True), + (['OBS'], {'OBS_VAR1_LEVELS': '"(*,*)"', + 'FCST_VAR1_NAME': 'APCP'}, False), + + (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06', + 'OBS_VAR1_NAME': 'script_name.py something else'}, True), + (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06', + 'OBS_VAR1_NAME': 'APCP'}, False), + (['OBS', 'ENS'], {'OBS_VAR1_LEVELS': '"(*,*)"', + 'FCST_VAR1_NAME': 'script_name.py something else'}, True), + (['OBS', 'ENS'], {'OBS_VAR1_LEVELS': '"(*,*)"', + 'FCST_VAR1_NAME': 'APCP'}, False), + + (['FCST'], {'FCST_VAR1_LEVELS': 'A06, A12', + 'OBS_VAR1_NAME': 'script_name.py something else'}, False), + (['FCST'], {'FCST_VAR1_LEVELS': 'A06, A12', + 'OBS_VAR1_NAME': 'APCP'}, False), + (['OBS'], {'OBS_VAR1_LEVELS': '"(0,*,*)", "(1,*,*)"', + 'FCST_VAR1_NAME': 'script_name.py something else'}, False), + + (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06, A12', + 'OBS_VAR1_NAME': 'script_name.py something else'}, False), + (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06, A12', + 'OBS_VAR1_NAME': 'APCP'}, False), + (['OBS', 'ENS'], {'OBS_VAR1_LEVELS': '"(0,*,*)", "(1,*,*)"', + 'FCST_VAR1_NAME': 'script_name.py something else'}, False), + + ] +) +def test_is_var_item_valid_levels(metplus_config, item_list, configs_to_set, is_valid): + conf = metplus_config() + for key, value in configs_to_set.items(): + conf.set('config', key, value) + + assert(config_metplus.is_var_item_valid(item_list, '1', 'LEVELS', conf)[0] == is_valid) + +# search prefixes are valid prefixes to append to field info variables +# config_overrides are a dict of config vars and their values +# search_key is the key of the field config item to check +# expected_value is the variable that search_key is set to +@pytest.mark.parametrize( + 'search_prefixes, config_overrides, expected_value', [ + (['BOTH_', 'FCST_'], + {'FCST_VAR1_': 'fcst_var1'}, + 'fcst_var1' + ), + (['BOTH_', 'FCST_'], {}, None), + + (['BOTH_', 'FCST_'], + {'FCST_VAR1_': 'fcst_var1', + 'BOTH_VAR1_': 'both_var1'}, + 'both_var1' + ), + + (['BOTH_GRID_STAT_', 'FCST_GRID_STAT_'], + {'FCST_GRID_STAT_VAR1_': 'fcst_grid_stat_var1'}, + 'fcst_grid_stat_var1' + ), + (['BOTH_GRID_STAT_', 'FCST_GRID_STAT_'], {}, None), + (['BOTH_GRID_STAT_', 'FCST_GRID_STAT_'], + {'FCST_GRID_STAT_VAR1_': 'fcst_grid_stat_var1', + 'BOTH_GRID_STAT_VAR1_': 'both_grid_stat_var1'}, + 'both_grid_stat_var1' + ), + + (['ENS_'], + {'ENS_VAR1_': 'env_var1'}, + 'env_var1' + ), + (['ENS_'], {}, None), + + ] +) +def test_get_field_config_variables(metplus_config, + search_prefixes, + config_overrides, + expected_value): + config = metplus_config() + index = '1' + field_info_types = ['name', 'levels', 'thresh', 'options', 'output_names'] + for field_info_type in field_info_types: + for key, value in config_overrides.items(): + config.set('config', + f'{key}{field_info_type.upper()}', + value) + + field_configs = config_metplus.get_field_config_variables(config, + index, + search_prefixes) + + assert(field_configs.get(field_info_type) == expected_value) + +@pytest.mark.parametrize( + 'config_keys, field_key, expected_value', [ + (['NAME', + ], + 'name', 'NAME' + ), + (['NAME', + 'INPUT_FIELD_NAME', + ], + 'name', 'NAME' + ), + (['INPUT_FIELD_NAME', + ], + 'name', 'INPUT_FIELD_NAME' + ), + ([], 'name', None), + (['LEVELS', + ], + 'levels', 'LEVELS' + ), + (['LEVELS', + 'FIELD_LEVEL', + ], + 'levels', 'LEVELS' + ), + (['FIELD_LEVEL', + ], + 'levels', 'FIELD_LEVEL' + ), + ([], 'levels', None), + (['OUTPUT_NAMES', + ], + 'output_names', 'OUTPUT_NAMES' + ), + (['OUTPUT_NAMES', + 'OUTPUT_FIELD_NAME', + ], + 'output_names', 'OUTPUT_NAMES' + ), + (['OUTPUT_FIELD_NAME', + ], + 'output_names', 'OUTPUT_FIELD_NAME' + ), + ([], 'output_names', None), + ] +) +def test_get_field_config_variables_synonyms(metplus_config, + config_keys, + field_key, + expected_value): + config = metplus_config() + index = '1' + prefix = 'BOTH_REGRID_DATA_PLANE_' + for key in config_keys: + config.set('config', f'{prefix}VAR{index}_{key}', key) + + field_configs = config_metplus.get_field_config_variables(config, + index, + [prefix]) + + assert(field_configs.get(field_key) == expected_value) + +# field info only defined in the FCST_* variables +@pytest.mark.parametrize( + 'data_type, list_created', [ + (None, False), + ('FCST', True), + ('OBS', False), + ] +) +def test_parse_var_list_fcst_only(metplus_config, data_type, list_created): + conf = metplus_config() + conf.set('config', 'FCST_VAR1_NAME', "NAME1") + conf.set('config', 'FCST_VAR1_LEVELS', "LEVELS11, LEVELS12") + conf.set('config', 'FCST_VAR2_NAME', "NAME2") + conf.set('config', 'FCST_VAR2_LEVELS', "LEVELS21, LEVELS22") + + # this should not occur because OBS variables are missing + if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: + assert(False) + + var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) + + # list will be created if requesting just OBS, but it should not be created if + # nothing was requested because FCST values are missing + if list_created: + assert(var_list[0]['fcst_name'] == "NAME1" and \ + var_list[1]['fcst_name'] == "NAME1" and \ + var_list[2]['fcst_name'] == "NAME2" and \ + var_list[3]['fcst_name'] == "NAME2" and \ + var_list[0]['fcst_level'] == "LEVELS11" and \ + var_list[1]['fcst_level'] == "LEVELS12" and \ + var_list[2]['fcst_level'] == "LEVELS21" and \ + var_list[3]['fcst_level'] == "LEVELS22") + else: + assert(not var_list) + +# field info only defined in the OBS_* variables +@pytest.mark.parametrize( + 'data_type, list_created', [ + (None, False), + ('OBS', True), + ('FCST', False), + ] +) +def test_parse_var_list_obs(metplus_config, data_type, list_created): + conf = metplus_config() + conf.set('config', 'OBS_VAR1_NAME', "NAME1") + conf.set('config', 'OBS_VAR1_LEVELS', "LEVELS11, LEVELS12") + conf.set('config', 'OBS_VAR2_NAME', "NAME2") + conf.set('config', 'OBS_VAR2_LEVELS', "LEVELS21, LEVELS22") + + # this should not occur because FCST variables are missing + if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: + assert(False) + + var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) + + # list will be created if requesting just OBS, but it should not be created if + # nothing was requested because FCST values are missing + if list_created: + assert(var_list[0]['obs_name'] == "NAME1" and \ + var_list[1]['obs_name'] == "NAME1" and \ + var_list[2]['obs_name'] == "NAME2" and \ + var_list[3]['obs_name'] == "NAME2" and \ + var_list[0]['obs_level'] == "LEVELS11" and \ + var_list[1]['obs_level'] == "LEVELS12" and \ + var_list[2]['obs_level'] == "LEVELS21" and \ + var_list[3]['obs_level'] == "LEVELS22") + else: + assert(not var_list) + + +# field info only defined in the BOTH_* variables +@pytest.mark.parametrize( + 'data_type, list_created', [ + (None, 'fcst:obs'), + ('FCST', 'fcst'), + ('OBS', 'obs'), + ] +) +def test_parse_var_list_both(metplus_config, data_type, list_created): + conf = metplus_config() + conf.set('config', 'BOTH_VAR1_NAME', "NAME1") + conf.set('config', 'BOTH_VAR1_LEVELS', "LEVELS11, LEVELS12") + conf.set('config', 'BOTH_VAR2_NAME', "NAME2") + conf.set('config', 'BOTH_VAR2_LEVELS', "LEVELS21, LEVELS22") + + # this should not occur because BOTH variables are used + if not config_metplus.validate_configuration_variables(conf, force_check=True)[1]: + assert(False) + + var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) + print(f'var_list:{var_list}') + for list_to_check in list_created.split(':'): + if not var_list[0][f'{list_to_check}_name'] == "NAME1" or \ + not var_list[1][f'{list_to_check}_name'] == "NAME1" or \ + not var_list[2][f'{list_to_check}_name'] == "NAME2" or \ + not var_list[3][f'{list_to_check}_name'] == "NAME2" or \ + not var_list[0][f'{list_to_check}_level'] == "LEVELS11" or \ + not var_list[1][f'{list_to_check}_level'] == "LEVELS12" or \ + not var_list[2][f'{list_to_check}_level'] == "LEVELS21" or \ + not var_list[3][f'{list_to_check}_level'] == "LEVELS22": + assert(False) + +# field info defined in both FCST_* and OBS_* variables +def test_parse_var_list_fcst_and_obs(metplus_config): + conf = metplus_config() + conf.set('config', 'FCST_VAR1_NAME', "FNAME1") + conf.set('config', 'FCST_VAR1_LEVELS', "FLEVELS11, FLEVELS12") + conf.set('config', 'FCST_VAR2_NAME', "FNAME2") + conf.set('config', 'FCST_VAR2_LEVELS', "FLEVELS21, FLEVELS22") + conf.set('config', 'OBS_VAR1_NAME', "ONAME1") + conf.set('config', 'OBS_VAR1_LEVELS', "OLEVELS11, OLEVELS12") + conf.set('config', 'OBS_VAR2_NAME', "ONAME2") + conf.set('config', 'OBS_VAR2_LEVELS', "OLEVELS21, OLEVELS22") + + # this should not occur because FCST and OBS variables are found + if not config_metplus.validate_configuration_variables(conf, force_check=True)[1]: + assert(False) + + var_list = config_metplus.parse_var_list(conf) + + assert(var_list[0]['fcst_name'] == "FNAME1" and \ + var_list[0]['obs_name'] == "ONAME1" and \ + var_list[1]['fcst_name'] == "FNAME1" and \ + var_list[1]['obs_name'] == "ONAME1" and \ + var_list[2]['fcst_name'] == "FNAME2" and \ + var_list[2]['obs_name'] == "ONAME2" and \ + var_list[3]['fcst_name'] == "FNAME2" and \ + var_list[3]['obs_name'] == "ONAME2" and \ + var_list[0]['fcst_level'] == "FLEVELS11" and \ + var_list[0]['obs_level'] == "OLEVELS11" and \ + var_list[1]['fcst_level'] == "FLEVELS12" and \ + var_list[1]['obs_level'] == "OLEVELS12" and \ + var_list[2]['fcst_level'] == "FLEVELS21" and \ + var_list[2]['obs_level'] == "OLEVELS21" and \ + var_list[3]['fcst_level'] == "FLEVELS22" and \ + var_list[3]['obs_level'] == "OLEVELS22") + +# VAR1 defined by FCST, VAR2 defined by OBS +def test_parse_var_list_fcst_and_obs_alternate(metplus_config): + conf = metplus_config() + conf.set('config', 'FCST_VAR1_NAME', "FNAME1") + conf.set('config', 'FCST_VAR1_LEVELS', "FLEVELS11, FLEVELS12") + conf.set('config', 'OBS_VAR2_NAME', "ONAME2") + conf.set('config', 'OBS_VAR2_LEVELS', "OLEVELS21, OLEVELS22") + + # configuration is invalid and parse var list should not give any results + assert(not config_metplus.validate_configuration_variables(conf, force_check=True)[1] and not config_metplus.parse_var_list(conf)) + +# VAR1 defined by OBS, VAR2 by FCST, VAR3 by both FCST AND OBS +@pytest.mark.parametrize( + 'data_type, list_len, name_levels', [ + (None, 0, None), + ('FCST', 4, ('FNAME2:FLEVELS21','FNAME2:FLEVELS22','FNAME3:FLEVELS31','FNAME3:FLEVELS32')), + ('OBS', 4, ('ONAME1:OLEVELS11','ONAME1:OLEVELS12','ONAME3:OLEVELS31','ONAME3:OLEVELS32')), + ] +) +def test_parse_var_list_fcst_and_obs_and_both(metplus_config, data_type, list_len, name_levels): + conf = metplus_config() + conf.set('config', 'OBS_VAR1_NAME', "ONAME1") + conf.set('config', 'OBS_VAR1_LEVELS', "OLEVELS11, OLEVELS12") + conf.set('config', 'FCST_VAR2_NAME', "FNAME2") + conf.set('config', 'FCST_VAR2_LEVELS', "FLEVELS21, FLEVELS22") + conf.set('config', 'FCST_VAR3_NAME', "FNAME3") + conf.set('config', 'FCST_VAR3_LEVELS', "FLEVELS31, FLEVELS32") + conf.set('config', 'OBS_VAR3_NAME', "ONAME3") + conf.set('config', 'OBS_VAR3_LEVELS', "OLEVELS31, OLEVELS32") + + # configuration is invalid and parse var list should not give any results + if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: + assert(False) + + var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) + + if len(var_list) != list_len: + assert(False) + + if data_type is None: + assert(len(var_list) == 0) + + if name_levels is not None: + dt_lower = data_type.lower() + expected = [] + for name_level in name_levels: + name, level = name_level.split(':') + expected.append({f'{dt_lower}_name': name, + f'{dt_lower}_level': level}) + + for expect, reality in zip(expected,var_list): + if expect[f'{dt_lower}_name'] != reality[f'{dt_lower}_name']: + assert(False) + + if expect[f'{dt_lower}_level'] != reality[f'{dt_lower}_level']: + assert(False) + + assert(True) + +# option defined in obs only +@pytest.mark.parametrize( + 'data_type, list_len', [ + (None, 0), + ('FCST', 2), + ('OBS', 0), + ] +) +def test_parse_var_list_fcst_only_options(metplus_config, data_type, list_len): + conf = metplus_config() + conf.set('config', 'FCST_VAR1_NAME', "NAME1") + conf.set('config', 'FCST_VAR1_LEVELS', "LEVELS11, LEVELS12") + conf.set('config', 'FCST_VAR1_THRESH', ">1, >2") + conf.set('config', 'OBS_VAR1_OPTIONS', "OOPTIONS11") + + # this should not occur because OBS variables are missing + if config_metplus.validate_configuration_variables(conf, force_check=True)[1]: + assert(False) + + var_list = config_metplus.parse_var_list(conf, time_info=None, data_type=data_type) + + assert(len(var_list) == list_len) + +@pytest.mark.parametrize( + 'met_tool, indices', [ + (None, {'1': ['FCST']}), + ('GRID_STAT', {'2': ['FCST']}), + ('ENSEMBLE_STAT', {}), + ] +) +def test_find_var_indices_wrapper_specific(metplus_config, met_tool, indices): + conf = metplus_config() + data_type = 'FCST' + conf.set('config', f'{data_type}_VAR1_NAME', "NAME1") + conf.set('config', f'{data_type}_GRID_STAT_VAR2_NAME', "GSNAME2") + + var_name_indices = config_metplus.find_var_name_indices(conf, data_types=[data_type], + met_tool=met_tool) + + assert(var_name_indices == indices) + +# ensure that the field configuration used for +# met_tool_wrapper/EnsembleStat/EnsembleStat.conf +# works as expected +def test_parse_var_list_ensemble(metplus_config): + config = metplus_config() + config.set('config', 'ENS_VAR1_NAME', 'APCP') + config.set('config', 'ENS_VAR1_LEVELS', 'A24') + config.set('config', 'ENS_VAR1_THRESH', '>0.0, >=10.0') + config.set('config', 'ENS_VAR2_NAME', 'REFC') + config.set('config', 'ENS_VAR2_LEVELS', 'L0') + config.set('config', 'ENS_VAR2_THRESH', '>35.0') + config.set('config', 'ENS_VAR2_OPTIONS', 'GRIB1_ptv = 129;') + config.set('config', 'ENS_VAR3_NAME', 'UGRD') + config.set('config', 'ENS_VAR3_LEVELS', 'Z10') + config.set('config', 'ENS_VAR3_THRESH', '>=5.0') + config.set('config', 'ENS_VAR4_NAME', 'VGRD') + config.set('config', 'ENS_VAR4_LEVELS', 'Z10') + config.set('config', 'ENS_VAR4_THRESH', '>=5.0') + config.set('config', 'ENS_VAR5_NAME', 'WIND') + config.set('config', 'ENS_VAR5_LEVELS', 'Z10') + config.set('config', 'ENS_VAR5_THRESH', '>=5.0') + config.set('config', 'FCST_VAR1_NAME', 'APCP') + config.set('config', 'FCST_VAR1_LEVELS', 'A24') + config.set('config', 'FCST_VAR1_THRESH', '>0.01, >=10.0') + config.set('config', 'FCST_VAR1_OPTIONS', ('ens_ssvar_bin_size = 0.1; ' + 'ens_phist_bin_size = 0.05;')) + config.set('config', 'OBS_VAR1_NAME', 'APCP') + config.set('config', 'OBS_VAR1_LEVELS', 'A24') + config.set('config', 'OBS_VAR1_THRESH', '>0.01, >=10.0') + config.set('config', 'OBS_VAR1_OPTIONS', ('ens_ssvar_bin_size = 0.1; ' + 'ens_phist_bin_size = 0.05;')) + time_info = {} + + expected_ens_list = [{'index': '1', + 'ens_name': 'APCP', + 'ens_level': 'A24', + 'ens_thresh': ['>0.0', '>=10.0']}, + {'index': '2', + 'ens_name': 'REFC', + 'ens_level': 'L0', + 'ens_thresh': ['>35.0']}, + {'index': '3', + 'ens_name': 'UGRD', + 'ens_level': 'Z10', + 'ens_thresh': ['>=5.0']}, + {'index': '4', + 'ens_name': 'VGRD', + 'ens_level': 'Z10', + 'ens_thresh': ['>=5.0']}, + {'index': '5', + 'ens_name': 'WIND', + 'ens_level': 'Z10', + 'ens_thresh': ['>=5.0']}, + ] + expected_var_list = [{'index': '1', + 'fcst_name': 'APCP', + 'fcst_level': 'A24', + 'fcst_thresh': ['>0.01', '>=10.0'], + 'fcst_extra': ('ens_ssvar_bin_size = 0.1; ' + 'ens_phist_bin_size = 0.05;'), + 'obs_name': 'APCP', + 'obs_level': 'A24', + 'obs_thresh': ['>0.01', '>=10.0'], + 'obs_extra': ('ens_ssvar_bin_size = 0.1; ' + 'ens_phist_bin_size = 0.05;') + + }, + ] + + ensemble_var_list = config_metplus.parse_var_list(config, time_info, + data_type='ENS') + + # parse optional var list for FCST and/or OBS fields + var_list = config_metplus.parse_var_list(config, time_info, + met_tool='ensemble_stat') + + pp = pprint.PrettyPrinter() + print(f'ENSEMBLE_VAR_LIST:') + pp.pprint(ensemble_var_list) + print(f'VAR_LIST:') + pp.pprint(var_list) + + assert(len(ensemble_var_list) == len(expected_ens_list)) + for actual_ens, expected_ens in zip(ensemble_var_list, expected_ens_list): + for key, value in expected_ens.items(): + assert(actual_ens.get(key) == value) + + assert(len(var_list) == len(expected_var_list)) + for actual_var, expected_var in zip(var_list, expected_var_list): + for key, value in expected_var.items(): + assert(actual_var.get(key) == value) + +def test_parse_var_list_series_by(metplus_config): + config = metplus_config() + config.set('config', 'BOTH_EXTRACT_TILES_VAR1_NAME', 'RH') + config.set('config', 'BOTH_EXTRACT_TILES_VAR1_LEVELS', 'P850, P700') + config.set('config', 'BOTH_EXTRACT_TILES_VAR1_OUTPUT_NAMES', + 'RH_850mb, RH_700mb') + + config.set('config', 'BOTH_SERIES_ANALYSIS_VAR1_NAME', 'RH_850mb') + config.set('config', 'BOTH_SERIES_ANALYSIS_VAR1_LEVELS', 'P850') + config.set('config', 'BOTH_SERIES_ANALYSIS_VAR2_NAME', 'RH_700mb') + config.set('config', 'BOTH_SERIES_ANALYSIS_VAR2_LEVELS', 'P700') + time_info = {} + + expected_et_list = [{'index': '1', + 'fcst_name': 'RH', + 'fcst_level': 'P850', + 'fcst_output_name': 'RH_850mb', + 'obs_name': 'RH', + 'obs_level': 'P850', + 'obs_output_name': 'RH_850mb', + }, + {'index': '1', + 'fcst_name': 'RH', + 'fcst_level': 'P700', + 'fcst_output_name': 'RH_700mb', + 'obs_name': 'RH', + 'obs_level': 'P700', + 'obs_output_name': 'RH_700mb', + }, + ] + expected_sa_list = [{'index': '1', + 'fcst_name': 'RH_850mb', + 'fcst_level': 'P850', + 'obs_name': 'RH_850mb', + 'obs_level': 'P850', + }, + {'index': '2', + 'fcst_name': 'RH_700mb', + 'fcst_level': 'P700', + 'obs_name': 'RH_700mb', + 'obs_level': 'P700', + }, + ] + + actual_et_list = config_metplus.parse_var_list(config, + time_info=time_info, + met_tool='extract_tiles') + + actual_sa_list = config_metplus.parse_var_list(config, + met_tool='series_analysis') + + pp = pprint.PrettyPrinter() + print(f'ExtractTiles var list:') + pp.pprint(actual_et_list) + print(f'SeriesAnalysis var list:') + pp.pprint(actual_sa_list) + + assert(len(actual_et_list) == len(expected_et_list)) + for actual_et, expected_et in zip(actual_et_list, expected_et_list): + for key, value in expected_et.items(): + assert(actual_et.get(key) == value) + + assert(len(actual_sa_list) == len(expected_sa_list)) + for actual_sa, expected_sa in zip(actual_sa_list, expected_sa_list): + for key, value in expected_sa.items(): + assert(actual_sa.get(key) == value) + +def test_parse_var_list_priority_fcst(metplus_config): + priority_list = ['FCST_GRID_STAT_VAR1_NAME', + 'FCST_GRID_STAT_VAR1_INPUT_FIELD_NAME', + 'FCST_GRID_STAT_VAR1_FIELD_NAME', + 'BOTH_GRID_STAT_VAR1_NAME', + 'BOTH_GRID_STAT_VAR1_INPUT_FIELD_NAME', + 'BOTH_GRID_STAT_VAR1_FIELD_NAME', + 'FCST_VAR1_NAME', + 'FCST_VAR1_INPUT_FIELD_NAME', + 'FCST_VAR1_FIELD_NAME', + 'BOTH_VAR1_NAME', + 'BOTH_VAR1_INPUT_FIELD_NAME', + 'BOTH_VAR1_FIELD_NAME', + ] + time_info = {} + + # loop through priority list, process, then pop first value off and + # process again until all items have been popped. + # This will check that list is in priority order + while(priority_list): + config = metplus_config() + for key in priority_list: + config.set('config', key, key.lower()) + + var_list = config_metplus.parse_var_list(config, time_info=time_info, + data_type='FCST', + met_tool='grid_stat') + + assert(len(var_list) == 1) + assert(var_list[0].get('fcst_name') == priority_list[0].lower()) + priority_list.pop(0) + +# test that if wrapper specific field info is specified, it only gets +# values from that list. All generic values should be read if no +# wrapper specific field info variables are specified +def test_parse_var_list_wrapper_specific(metplus_config): + conf = metplus_config() + conf.set('config', 'FCST_VAR1_NAME', "ENAME1") + conf.set('config', 'FCST_VAR1_LEVELS', "ELEVELS11, ELEVELS12") + conf.set('config', 'FCST_VAR2_NAME', "ENAME2") + conf.set('config', 'FCST_VAR2_LEVELS', "ELEVELS21, ELEVELS22") + conf.set('config', 'FCST_GRID_STAT_VAR1_NAME', "GNAME1") + conf.set('config', 'FCST_GRID_STAT_VAR1_LEVELS', "GLEVELS11, GLEVELS12") + + e_var_list = config_metplus.parse_var_list(conf, + time_info=None, + data_type='FCST', + met_tool='ensemble_stat') + + g_var_list = config_metplus.parse_var_list(conf, + time_info=None, + data_type='FCST', + met_tool='grid_stat') + + assert(len(e_var_list) == 4 and len(g_var_list) == 2 and + e_var_list[0]['fcst_name'] == "ENAME1" and + e_var_list[1]['fcst_name'] == "ENAME1" and + e_var_list[2]['fcst_name'] == "ENAME2" and + e_var_list[3]['fcst_name'] == "ENAME2" and + e_var_list[0]['fcst_level'] == "ELEVELS11" and + e_var_list[1]['fcst_level'] == "ELEVELS12" and + e_var_list[2]['fcst_level'] == "ELEVELS21" and + e_var_list[3]['fcst_level'] == "ELEVELS22" and + g_var_list[0]['fcst_name'] == "GNAME1" and + g_var_list[1]['fcst_name'] == "GNAME1" and + g_var_list[0]['fcst_level'] == "GLEVELS11" and + g_var_list[1]['fcst_level'] == "GLEVELS12") + +@pytest.mark.parametrize( + 'config_overrides, expected_results', [ + # 2 levels + ({'FCST_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {fcst_level}', + 'FCST_VAR1_LEVELS': 'P500,P250', + 'OBS_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {obs_level}', + 'OBS_VAR1_LEVELS': 'P500,P250', + }, + ['read_data.py TMP 20200201 P500', + 'read_data.py TMP 20200201 P250', + ]), + ({'BOTH_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {fcst_level}', + 'BOTH_VAR1_LEVELS': 'P500,P250', + }, + ['read_data.py TMP 20200201 P500', + 'read_data.py TMP 20200201 P250', + ]), + # no level but level specified in name + ({'FCST_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {fcst_level}', + 'OBS_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {obs_level}', + }, + ['read_data.py TMP 20200201 ', + ]), + # no level + ({'FCST_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d}', + 'OBS_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d}', + }, + ['read_data.py TMP 20200201', + ]), + # real example + ({'BOTH_VAR1_NAME': ('myscripts/read_nc2xr.py ' + 'mydata/forecast_file.nc4 TMP ' + '{valid?fmt=%Y%m%d_%H%M} {fcst_level}'), + 'BOTH_VAR1_LEVELS': 'P1000,P850,P700,P500,P250,P100', + }, + [('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' + ' P1000'), + ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' + ' P850'), + ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' + ' P700'), + ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' + ' P500'), + ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' + ' P250'), + ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' + ' P100'), + ]), + ] +) +def test_parse_var_list_py_embed_multi_levels(metplus_config, config_overrides, + expected_results): + config = metplus_config() + for key, value in config_overrides.items(): + config.set('config', key, value) + + time_info = {'valid': datetime(2020, 2, 1, 12, 25)} + var_list = config_metplus.parse_var_list(config, + time_info=time_info, + data_type=None) + assert(len(var_list) == len(expected_results)) + + for var_item, expected_result in zip(var_list, expected_results): + assert(var_item['fcst_name'] == expected_result) + + # run again with data type specified + var_list = config_metplus.parse_var_list(config, + time_info=time_info, + data_type='FCST') + assert(len(var_list) == len(expected_results)) + + for var_item, expected_result in zip(var_list, expected_results): + assert(var_item['fcst_name'] == expected_result) + + +@pytest.mark.parametrize( + 'input_list, expected_list', [ + ('Point2Grid', ['Point2Grid']), + # MET documentation syntax (with dashes) + ('Pcp-Combine, Grid-Stat, Ensemble-Stat', ['PCPCombine', + 'GridStat', + 'EnsembleStat']), + ('Point-Stat', ['PointStat']), + ('Mode, MODE Time Domain', ['MODE', + 'MTD']), + # actual tool name (lower case underscore) + ('point_stat, grid_stat, ensemble_stat', ['PointStat', + 'GridStat', + 'EnsembleStat']), + ('mode, mtd', ['MODE', + 'MTD']), + ('ascii2nc, pb2nc, regrid_data_plane', ['ASCII2NC', + 'PB2NC', + 'RegridDataPlane']), + ('pcp_combine, tc_pairs, tc_stat', ['PCPCombine', + 'TCPairs', + 'TCStat']), + ('gen_vx_mask, stat_analysis, series_analysis', ['GenVxMask', + 'StatAnalysis', + 'SeriesAnalysis']), + # old capitalization format + ('PcpCombine, Ascii2Nc, TcStat, TcPairs', ['PCPCombine', + 'ASCII2NC', + 'TCStat', + 'TCPairs']), + # remove MakePlots from list + ('StatAnalysis, MakePlots', ['StatAnalysis']), + ] +) +def test_get_process_list(metplus_config, input_list, expected_list): + conf = metplus_config() + conf.set('config', 'PROCESS_LIST', input_list) + process_list = config_metplus.get_process_list(conf) + output_list = [item[0] for item in process_list] + assert(output_list == expected_list) + +@pytest.mark.parametrize( + 'input_list, expected_list', [ + # no instances + ('Point2Grid', [('Point2Grid', None)]), + # one with instance one without + ('PcpCombine, GridStat(my_instance)', [('PCPCombine', None), + ('GridStat', 'my_instance')]), + # duplicate process, one with instance one without + ('TCStat, ExtractTiles, TCStat(for_series), SeriesAnalysis', ( + [('TCStat',None), + ('ExtractTiles',None), + ('TCStat', 'for_series'), + ('SeriesAnalysis',None),])), + # two processes, both with instances + ('mode(uno), mtd(dos)', [('MODE', 'uno'), + ('MTD', 'dos')]), + # lower-case names, first with instance, second without + ('ascii2nc(some_name), pb2nc', [('ASCII2NC', 'some_name'), + ('PB2NC', None)]), + # duplicate process, both with different instances + ('tc_stat(one), tc_pairs, tc_stat(two)', [('TCStat', 'one'), + ('TCPairs', None), + ('TCStat', 'two')]), + ] +) +def test_get_process_list_instances(metplus_config, input_list, expected_list): + conf = metplus_config() + conf.set('config', 'PROCESS_LIST', input_list) + output_list = config_metplus.get_process_list(conf) + assert(output_list == expected_list) \ No newline at end of file diff --git a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py index be6ee1acb4..1f864709a1 100644 --- a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py +++ b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py @@ -137,7 +137,8 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, ({'ENSEMBLE_STAT_REGRID_TO_GRID': 'FCST', }, - {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}'}), + {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}', + 'REGRID_TO_GRID': 'FCST'}), ({'ENSEMBLE_STAT_REGRID_METHOD': 'NEAREST', }, @@ -163,7 +164,8 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, }, {'METPLUS_REGRID_DICT': ('regrid = {to_grid = FCST;method = NEAREST;' 'width = 1;vld_thresh = 0.5;shape = SQUARE;}' - )}), + ), + 'REGRID_TO_GRID': 'FCST'}), ({'ENSEMBLE_STAT_CLIMO_MEAN_INPUT_TEMPLATE': '/some/path/climo/filename.nc', @@ -575,7 +577,6 @@ def test_ensemble_stat_single_field(metplus_config, config_overrides, f"{config_file} -outdir {out_dir}/2005080800"), ] - all_cmds = wrapper.run_all_times() print(f"ALL COMMANDS: {all_cmds}") assert len(all_cmds) == len(expected_cmds) @@ -585,7 +586,11 @@ def test_ensemble_stat_single_field(metplus_config, config_overrides, assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py b/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py index ed27e0784f..60703aecb2 100644 --- a/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py +++ b/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py @@ -53,35 +53,36 @@ def set_minimum_config_settings(config): @pytest.mark.parametrize( 'config_overrides, env_var_values', [ + # 0 ({'MODEL': 'my_model'}, {'METPLUS_MODEL': 'model = "my_model";'}), - + # 1 ({'GEN_ENS_PROD_DESC': 'my_desc'}, {'METPLUS_DESC': 'desc = "my_desc";'}), - + # 2 ({'DESC': 'my_desc'}, {'METPLUS_DESC': 'desc = "my_desc";'}), - + # 3 ({'GEN_ENS_PROD_REGRID_TO_GRID': 'FCST', }, {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}'}), - + # 4 ({'GEN_ENS_PROD_REGRID_METHOD': 'NEAREST', }, {'METPLUS_REGRID_DICT': 'regrid = {method = NEAREST;}'}), - + # 5 ({'GEN_ENS_PROD_REGRID_WIDTH': '1', }, {'METPLUS_REGRID_DICT': 'regrid = {width = 1;}'}), - + # 6 ({'GEN_ENS_PROD_REGRID_VLD_THRESH': '0.5', }, {'METPLUS_REGRID_DICT': 'regrid = {vld_thresh = 0.5;}'}), - + # 7 ({'GEN_ENS_PROD_REGRID_SHAPE': 'SQUARE', }, {'METPLUS_REGRID_DICT': 'regrid = {shape = SQUARE;}'}), - + # 8 ({'GEN_ENS_PROD_REGRID_TO_GRID': 'FCST', 'GEN_ENS_PROD_REGRID_METHOD': 'NEAREST', 'GEN_ENS_PROD_REGRID_WIDTH': '1', @@ -91,110 +92,65 @@ def set_minimum_config_settings(config): {'METPLUS_REGRID_DICT': ('regrid = {to_grid = FCST;method = NEAREST;' 'width = 1;vld_thresh = 0.5;shape = SQUARE;}' )}), - + # 9 ({'GEN_ENS_PROD_CLIMO_MEAN_INPUT_TEMPLATE': '/some/path/climo/filename.nc', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {file_name = ["/some/path/climo/filename.nc"];}', - 'CLIMO_MEAN_FILE': - '"/some/path/climo/filename.nc"', }), + # 10 ({'GEN_ENS_PROD_CLIMO_STDEV_INPUT_TEMPLATE': '/some/path/climo/stdfile.nc', }, {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {file_name = ["/some/path/climo/stdfile.nc"];}', - 'CLIMO_STDEV_FILE': - '"/some/path/climo/stdfile.nc"', }), - # 12 mask grid and poly (old config var) - ({'GEN_ENS_PROD_MASK_GRID': 'FULL', - 'GEN_ENS_PROD_VERIFICATION_MASK_TEMPLATE': 'one, two', - }, - {'METPLUS_MASK_GRID': - 'grid = ["FULL"];', - 'METPLUS_MASK_POLY': - 'poly = ["one","two"];', - }), - # 13 mask grid and poly (new config var) - ({'GEN_ENS_PROD_MASK_GRID': 'FULL', - 'GEN_ENS_PROD_MASK_POLY': 'one, two', - }, - {'METPLUS_MASK_GRID': - 'grid = ["FULL"];', - 'METPLUS_MASK_POLY': - 'poly = ["one","two"];', - }), - # 14 mask grid value - ({'GEN_ENS_PROD_MASK_GRID': 'FULL', - }, - {'METPLUS_MASK_GRID': - 'grid = ["FULL"];', - }), - # 15 mask grid empty string (should create empty list) - ({'GEN_ENS_PROD_MASK_GRID': '', - }, - {'METPLUS_MASK_GRID': - 'grid = [];', - }), - # 16 mask poly (old config var) - ({'GEN_ENS_PROD_VERIFICATION_MASK_TEMPLATE': 'one, two', - }, - {'METPLUS_MASK_POLY': - 'poly = ["one","two"];', - }), - # 27 mask poly (new config var) - ({'GEN_ENS_PROD_MASK_POLY': 'one, two', - }, - {'METPLUS_MASK_POLY': - 'poly = ["one","two"];', - }), - # ensemble_flag + # 11 ensemble_flag ({'GEN_ENS_PROD_ENSEMBLE_FLAG_LATLON': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {latlon = FALSE;}'}), - + # 12 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_MEAN': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {mean = FALSE;}'}), - + # 13 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_STDEV': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {stdev = FALSE;}'}), - + # 14 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_MINUS': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {minus = FALSE;}'}), - + # 15 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_PLUS': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {plus = FALSE;}'}), - + # 16 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_MIN': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {min = FALSE;}'}), - + # 17 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_MAX': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {max = FALSE;}'}), - + # 18 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_RANGE': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {range = FALSE;}'}), - + # 19 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_VLD_COUNT': 'FALSE', }, { 'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {vld_count = FALSE;}'}), - + # 20 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_FREQUENCY': 'FALSE', }, { 'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {frequency = FALSE;}'}), - + # 21 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_NEP': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {nep = FALSE;}'}), - + # 22 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_NMEP': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {nmep = FALSE;}'}), - + # 23 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_RANK': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {rank = FALSE;}'}), - + # 24 ({'GEN_ENS_PROD_ENSEMBLE_FLAG_WEIGHT': 'FALSE', }, {'METPLUS_ENSEMBLE_FLAG_DICT': 'ensemble_flag = {weight = FALSE;}'}), - + # 25 ({ 'GEN_ENS_PROD_ENSEMBLE_FLAG_LATLON': 'FALSE', 'GEN_ENS_PROD_ENSEMBLE_FLAG_MEAN': 'FALSE', @@ -220,41 +176,40 @@ def set_minimum_config_settings(config): 'frequency = FALSE;nep = FALSE;' 'nmep = FALSE;rank = FALSE;' 'weight = FALSE;}')}), - + # 26 ({'GEN_ENS_PROD_CLIMO_MEAN_FILE_NAME': '/some/climo_mean/file.txt', }, {'METPLUS_CLIMO_MEAN_DICT': ('climo_mean = {file_name = ' - '["/some/climo_mean/file.txt"];}'), - 'CLIMO_MEAN_FILE': '"/some/climo_mean/file.txt"'}), - + '["/some/climo_mean/file.txt"];}'),}), + # 27 ({'GEN_ENS_PROD_CLIMO_MEAN_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), - + # 28 ({'GEN_ENS_PROD_CLIMO_MEAN_REGRID_METHOD': 'NEAREST', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {method = NEAREST;}}'}), - + # 29 ({'GEN_ENS_PROD_CLIMO_MEAN_REGRID_WIDTH': '1', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {width = 1;}}'}), - + # 30 ({'GEN_ENS_PROD_CLIMO_MEAN_REGRID_VLD_THRESH': '0.5', }, { 'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {vld_thresh = 0.5;}}'}), - + # 31 ({'GEN_ENS_PROD_CLIMO_MEAN_REGRID_SHAPE': 'SQUARE', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {shape = SQUARE;}}'}), - + # 32 ({'GEN_ENS_PROD_CLIMO_MEAN_TIME_INTERP_METHOD': 'NEAREST', }, { 'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {time_interp_method = NEAREST;}'}), - + # 33 ({'GEN_ENS_PROD_CLIMO_MEAN_MATCH_MONTH': 'True', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {match_month = TRUE;}'}), - + # 34 ({'GEN_ENS_PROD_CLIMO_MEAN_DAY_INTERVAL': '30', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {day_interval = 30;}'}), - + # 35 ({'GEN_ENS_PROD_CLIMO_MEAN_HOUR_INTERVAL': '12', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {hour_interval = 12;}'}), - + # 36 ({ 'GEN_ENS_PROD_CLIMO_MEAN_FILE_NAME': '/some/climo_mean/file.txt', 'GEN_ENS_PROD_CLIMO_MEAN_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', @@ -274,46 +229,43 @@ def set_minimum_config_settings(config): 'vld_thresh = 0.5;shape = SQUARE;}' 'time_interp_method = NEAREST;' 'match_month = TRUE;day_interval = 30;' - 'hour_interval = 12;}'), - 'CLIMO_MEAN_FILE': '"/some/climo_mean/file.txt"'}), - - # climo stdev + 'hour_interval = 12;}')}), + # 37 climo stdev ({'GEN_ENS_PROD_CLIMO_STDEV_FILE_NAME': '/some/climo_stdev/file.txt', }, {'METPLUS_CLIMO_STDEV_DICT': ('climo_stdev = {file_name = ' - '["/some/climo_stdev/file.txt"];}'), - 'CLIMO_STDEV_FILE': '"/some/climo_stdev/file.txt"'}), - + '["/some/climo_stdev/file.txt"];}')}), + # 38 ({'GEN_ENS_PROD_CLIMO_STDEV_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', }, {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), - + # 39 ({'GEN_ENS_PROD_CLIMO_STDEV_REGRID_METHOD': 'NEAREST', }, { 'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {regrid = {method = NEAREST;}}'}), - + # 40 ({'GEN_ENS_PROD_CLIMO_STDEV_REGRID_WIDTH': '1', }, {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {regrid = {width = 1;}}'}), - + # 41 ({'GEN_ENS_PROD_CLIMO_STDEV_REGRID_VLD_THRESH': '0.5', }, { 'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {regrid = {vld_thresh = 0.5;}}'}), - + # 42 ({'GEN_ENS_PROD_CLIMO_STDEV_REGRID_SHAPE': 'SQUARE', }, { 'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {regrid = {shape = SQUARE;}}'}), - + # 43 ({'GEN_ENS_PROD_CLIMO_STDEV_TIME_INTERP_METHOD': 'NEAREST', }, { 'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {time_interp_method = NEAREST;}'}), - + # 44 ({'GEN_ENS_PROD_CLIMO_STDEV_MATCH_MONTH': 'True', }, {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {match_month = TRUE;}'}), - + # 45 ({'GEN_ENS_PROD_CLIMO_STDEV_DAY_INTERVAL': '30', }, {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {day_interval = 30;}'}), - + # 46 ({'GEN_ENS_PROD_CLIMO_STDEV_HOUR_INTERVAL': '12', }, {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {hour_interval = 12;}'}), - + # 47 ({ 'GEN_ENS_PROD_CLIMO_STDEV_FILE_NAME': '/some/climo_stdev/file.txt', 'GEN_ENS_PROD_CLIMO_STDEV_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', @@ -333,17 +285,17 @@ def set_minimum_config_settings(config): 'vld_thresh = 0.5;shape = SQUARE;}' 'time_interp_method = NEAREST;' 'match_month = TRUE;day_interval = 30;' - 'hour_interval = 12;}'), - 'CLIMO_STDEV_FILE': '"/some/climo_stdev/file.txt"'}), + 'hour_interval = 12;}')}), + # 48 ({'GEN_ENS_PROD_NBRHD_PROB_WIDTH': '5', }, {'METPLUS_NBRHD_PROB_DICT': 'nbrhd_prob = {width = [5];}'}), - + # 49 ({'GEN_ENS_PROD_NBRHD_PROB_SHAPE': 'circle', }, {'METPLUS_NBRHD_PROB_DICT': 'nbrhd_prob = {shape = CIRCLE;}'}), - + # 50 ({'GEN_ENS_PROD_NBRHD_PROB_VLD_THRESH': '0.0', }, {'METPLUS_NBRHD_PROB_DICT': 'nbrhd_prob = {vld_thresh = 0.0;}'}), - + # 51 ({ 'GEN_ENS_PROD_NBRHD_PROB_WIDTH': '5', 'GEN_ENS_PROD_NBRHD_PROB_SHAPE': 'CIRCLE', @@ -355,25 +307,26 @@ def set_minimum_config_settings(config): 'vld_thresh = 0.0;}' ) }), + # 52 ({'GEN_ENS_PROD_NMEP_SMOOTH_VLD_THRESH': '0.0', }, {'METPLUS_NMEP_SMOOTH_DICT': 'nmep_smooth = {vld_thresh = 0.0;}'}), - + # 53 ({'GEN_ENS_PROD_NMEP_SMOOTH_SHAPE': 'circle', }, {'METPLUS_NMEP_SMOOTH_DICT': 'nmep_smooth = {shape = CIRCLE;}'}), - + # 54 ({'GEN_ENS_PROD_NMEP_SMOOTH_GAUSSIAN_DX': '81.27', }, {'METPLUS_NMEP_SMOOTH_DICT': 'nmep_smooth = {gaussian_dx = 81.27;}'}), - + # 55 ({'GEN_ENS_PROD_NMEP_SMOOTH_GAUSSIAN_RADIUS': '120', }, { 'METPLUS_NMEP_SMOOTH_DICT': 'nmep_smooth = {gaussian_radius = 120;}'}), - + # 56 ({'GEN_ENS_PROD_NMEP_SMOOTH_TYPE_METHOD': 'GAUSSIAN', }, {'METPLUS_NMEP_SMOOTH_DICT': 'nmep_smooth = {type = [{method = GAUSSIAN;}];}'}), - + # 57 ({'GEN_ENS_PROD_NMEP_SMOOTH_TYPE_WIDTH': '1', }, {'METPLUS_NMEP_SMOOTH_DICT': 'nmep_smooth = {type = [{width = 1;}];}'}), - + # 58 ({ 'GEN_ENS_PROD_NMEP_SMOOTH_VLD_THRESH': '0.0', 'GEN_ENS_PROD_NMEP_SMOOTH_SHAPE': 'circle', @@ -442,7 +395,11 @@ def test_gen_ens_prod_single_field(metplus_config, config_overrides, assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py b/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py index 3fd1e83f5e..b38bee453a 100644 --- a/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py +++ b/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py @@ -129,7 +129,8 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, ({'GRID_STAT_REGRID_TO_GRID': 'FCST', }, - {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}'}), + {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}', + 'REGRID_TO_GRID': 'FCST'}), ({'GRID_STAT_REGRID_METHOD': 'NEAREST', }, @@ -155,7 +156,8 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, }, {'METPLUS_REGRID_DICT': ('regrid = {to_grid = FCST;method = NEAREST;' 'width = 1;vld_thresh = 0.5;shape = SQUARE;}' - )}), + ), + 'REGRID_TO_GRID': 'FCST'}), ({'GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE': '/some/path/climo/filename.nc', @@ -605,7 +607,11 @@ def test_grid_stat_single_field(metplus_config, config_overrides, assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py b/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py index 47030515d3..d07460055e 100644 --- a/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py +++ b/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py @@ -221,7 +221,11 @@ def test_ioda2nc_wrapper(metplus_config, config_overrides, assert cmd == expected_cmd # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert match is not None diff --git a/internal_tests/pytests/met_config/test_met_config.py b/internal_tests/pytests/met_config/test_met_config.py new file mode 100644 index 0000000000..1e2e5e5f31 --- /dev/null +++ b/internal_tests/pytests/met_config/test_met_config.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +import pytest + +from metplus.util.met_config import * + +@pytest.mark.parametrize( + 'name, data_type, mp_configs, extra_args', [ + ('beg', 'int', 'BEG', None), + ('end', 'int', ['END'], None), + ] +) +def test_met_config_info(name, data_type, mp_configs, extra_args): + item = METConfig(name=name, data_type=data_type) + + item.metplus_configs = mp_configs + item.extra_args = extra_args + + assert(item.name == name) + assert(item.data_type == data_type) + if isinstance(mp_configs, list): + assert(item.metplus_configs == mp_configs) + else: + assert(item.metplus_configs == [mp_configs]) + + if not extra_args: + assert(item.extra_args == {}) + +@pytest.mark.parametrize( + 'data_type, expected_function', [ + ('int', 'set_met_config_int'), + ('float', 'set_met_config_float'), + ('list', 'set_met_config_list'), + ('string', 'set_met_config_string'), + ('thresh', 'set_met_config_thresh'), + ('bool', 'set_met_config_bool'), + ('bad_name', None), + ] +) +def test_set_met_config_function(data_type, expected_function): + try: + function_found = set_met_config_function(data_type) + function_name = function_found.__name__ if function_found else None + assert(function_name == expected_function) + except ValueError: + assert expected_function is None + + +@pytest.mark.parametrize( + 'input, output', [ + ('', 'NONE'), + ('NONE', 'NONE'), + ('FCST', 'FCST'), + ('OBS', 'OBS'), + ('G002', '"G002"'), + ] +) +def test_format_regrid_to_grid(input, output): + assert format_regrid_to_grid(input) == output diff --git a/internal_tests/pytests/met_dictionary_info/test_met_dictionary_info.py b/internal_tests/pytests/met_dictionary_info/test_met_dictionary_info.py deleted file mode 100644 index 4bf2b7b19c..0000000000 --- a/internal_tests/pytests/met_dictionary_info/test_met_dictionary_info.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 - -import pytest - -from metplus.util import METConfigInfo - -@pytest.mark.parametrize( - 'name, data_type, mp_configs, extra_args', [ - ('beg', 'int', 'BEG', None), - ('end', 'int', ['END'], None), - ] -) -def test_met_config_info(name, data_type, mp_configs, extra_args): - item = METConfigInfo(name=name, - data_type=data_type, - ) - item.metplus_configs = mp_configs - item.extra_args = extra_args - - assert(item.name == name) - assert(item.data_type == data_type) - if isinstance(mp_configs, list): - assert(item.metplus_configs == mp_configs) - else: - assert(item.metplus_configs == [mp_configs]) - - if not extra_args: - assert(item.extra_args == {}) - diff --git a/internal_tests/pytests/met_util/test_met_util.py b/internal_tests/pytests/met_util/test_met_util.py index 0154ff68e6..cb3f04af7d 100644 --- a/internal_tests/pytests/met_util/test_met_util.py +++ b/internal_tests/pytests/met_util/test_met_util.py @@ -10,491 +10,7 @@ from metplus.util import met_util as util from metplus.util import time_util - -@pytest.mark.parametrize( - 'regex,index,id,expected_result', [ - # 0: No ID - (r'^FCST_VAR(\d+)_NAME$', 1, None, - {'1': [None], - '2': [None], - '4': [None]}), - # 1: ID and index 2 - (r'(\w+)_VAR(\d+)_NAME', 2, 1, - {'1': ['FCST'], - '2': ['FCST'], - '4': ['FCST']}), - # 2: index 1, ID 2, multiple identifiers - (r'^FCST_VAR(\d+)_(\w+)$', 1, 2, - {'1': ['NAME', 'LEVELS'], - '2': ['NAME'], - '4': ['NAME']}), - # 3: command that StatAnalysis wrapper uses - (r'MODEL(\d+)$', 1, None, - {'1': [None], - '2': [None],}), - # 4: TCPairs conensus logic - (r'^TC_PAIRS_CONSENSUS(\d+)_(\w+)$', 1, 2, - {'1': ['NAME', 'MEMBERS', 'REQUIRED', 'MIN_REQ'], - '2': ['NAME', 'MEMBERS', 'REQUIRED', 'MIN_REQ']}), - ] -) -def test_find_indices_in_config_section(metplus_config, regex, index, - id, expected_result): - config = metplus_config() - config.set('config', 'FCST_VAR1_NAME', 'name1') - config.set('config', 'FCST_VAR1_LEVELS', 'level1') - config.set('config', 'FCST_VAR2_NAME', 'name2') - config.set('config', 'FCST_VAR4_NAME', 'name4') - config.set('config', 'MODEL1', 'model1') - config.set('config', 'MODEL2', 'model2') - - config.set('config', 'TC_PAIRS_CONSENSUS1_NAME', 'name1') - config.set('config', 'TC_PAIRS_CONSENSUS1_MEMBERS', 'member1') - config.set('config', 'TC_PAIRS_CONSENSUS1_REQUIRED', 'True') - config.set('config', 'TC_PAIRS_CONSENSUS1_MIN_REQ', '1') - config.set('config', 'TC_PAIRS_CONSENSUS2_NAME', 'name2') - config.set('config', 'TC_PAIRS_CONSENSUS2_MEMBERS', 'member2') - config.set('config', 'TC_PAIRS_CONSENSUS2_REQUIRED', 'True') - config.set('config', 'TC_PAIRS_CONSENSUS2_MIN_REQ', '2') - - - indices = util.find_indices_in_config_section(regex, config, - index_index=index, - id_index=id) - - pp = pprint.PrettyPrinter() - print(f'Indices:') - pp.pprint(indices) - - assert indices == expected_result - -@pytest.mark.parametrize( - 'data_type, met_tool, expected_out', [ - ('FCST', None, ['FCST_', - 'BOTH_',]), - ('OBS', None, ['OBS_', - 'BOTH_',]), - ('FCST', 'grid_stat', ['FCST_GRID_STAT_', - 'BOTH_GRID_STAT_', - 'FCST_', - 'BOTH_', - ]), - ('OBS', 'extract_tiles', ['OBS_EXTRACT_TILES_', - 'BOTH_EXTRACT_TILES_', - 'OBS_', - 'BOTH_', - ]), - ('ENS', None, ['ENS_']), - ('DATA', None, ['DATA_']), - ('DATA', 'tc_gen', ['DATA_TC_GEN_', - 'DATA_']), - - ] -) -def test_get_field_search_prefixes(data_type, met_tool, expected_out): - assert(util.get_field_search_prefixes(data_type, - met_tool) == expected_out) - -# search prefixes are valid prefixes to append to field info variables -# config_overrides are a dict of config vars and their values -# search_key is the key of the field config item to check -# expected_value is the variable that search_key is set to -@pytest.mark.parametrize( - 'search_prefixes, config_overrides, expected_value', [ - (['BOTH_', 'FCST_'], - {'FCST_VAR1_': 'fcst_var1'}, - 'fcst_var1' - ), - (['BOTH_', 'FCST_'], {}, None), - - (['BOTH_', 'FCST_'], - {'FCST_VAR1_': 'fcst_var1', - 'BOTH_VAR1_': 'both_var1'}, - 'both_var1' - ), - - (['BOTH_GRID_STAT_', 'FCST_GRID_STAT_'], - {'FCST_GRID_STAT_VAR1_': 'fcst_grid_stat_var1'}, - 'fcst_grid_stat_var1' - ), - (['BOTH_GRID_STAT_', 'FCST_GRID_STAT_'], {}, None), - (['BOTH_GRID_STAT_', 'FCST_GRID_STAT_'], - {'FCST_GRID_STAT_VAR1_': 'fcst_grid_stat_var1', - 'BOTH_GRID_STAT_VAR1_': 'both_grid_stat_var1'}, - 'both_grid_stat_var1' - ), - - (['ENS_'], - {'ENS_VAR1_': 'env_var1'}, - 'env_var1' - ), - (['ENS_'], {}, None), - - ] -) -def test_get_field_config_variables(metplus_config, - search_prefixes, - config_overrides, - expected_value): - config = metplus_config() - index = '1' - field_info_types = ['name', 'levels', 'thresh', 'options', 'output_names'] - for field_info_type in field_info_types: - for key, value in config_overrides.items(): - config.set('config', - f'{key}{field_info_type.upper()}', - value) - - field_configs = util.get_field_config_variables(config, - index, - search_prefixes) - - assert(field_configs.get(field_info_type) == expected_value) - -@pytest.mark.parametrize( - 'config_keys, field_key, expected_value', [ - (['NAME', - ], - 'name', 'NAME' - ), - (['NAME', - 'INPUT_FIELD_NAME', - ], - 'name', 'NAME' - ), - (['INPUT_FIELD_NAME', - ], - 'name', 'INPUT_FIELD_NAME' - ), - ([], 'name', None), - (['LEVELS', - ], - 'levels', 'LEVELS' - ), - (['LEVELS', - 'FIELD_LEVEL', - ], - 'levels', 'LEVELS' - ), - (['FIELD_LEVEL', - ], - 'levels', 'FIELD_LEVEL' - ), - ([], 'levels', None), - (['OUTPUT_NAMES', - ], - 'output_names', 'OUTPUT_NAMES' - ), - (['OUTPUT_NAMES', - 'OUTPUT_FIELD_NAME', - ], - 'output_names', 'OUTPUT_NAMES' - ), - (['OUTPUT_FIELD_NAME', - ], - 'output_names', 'OUTPUT_FIELD_NAME' - ), - ([], 'output_names', None), - ] -) -def test_get_field_config_variables_synonyms(metplus_config, - config_keys, - field_key, - expected_value): - config = metplus_config() - index = '1' - prefix = 'BOTH_REGRID_DATA_PLANE_' - for key in config_keys: - config.set('config', f'{prefix}VAR{index}_{key}', key) - - field_configs = util.get_field_config_variables(config, - index, - [prefix]) - - assert(field_configs.get(field_key) == expected_value) - -# ensure that the field configuration used for -# met_tool_wrapper/EnsembleStat/EnsembleStat.conf -# works as expected -def test_parse_var_list_ensemble(metplus_config): - config = metplus_config() - config.set('config', 'ENS_VAR1_NAME', 'APCP') - config.set('config', 'ENS_VAR1_LEVELS', 'A24') - config.set('config', 'ENS_VAR1_THRESH', '>0.0, >=10.0') - config.set('config', 'ENS_VAR2_NAME', 'REFC') - config.set('config', 'ENS_VAR2_LEVELS', 'L0') - config.set('config', 'ENS_VAR2_THRESH', '>35.0') - config.set('config', 'ENS_VAR2_OPTIONS', 'GRIB1_ptv = 129;') - config.set('config', 'ENS_VAR3_NAME', 'UGRD') - config.set('config', 'ENS_VAR3_LEVELS', 'Z10') - config.set('config', 'ENS_VAR3_THRESH', '>=5.0') - config.set('config', 'ENS_VAR4_NAME', 'VGRD') - config.set('config', 'ENS_VAR4_LEVELS', 'Z10') - config.set('config', 'ENS_VAR4_THRESH', '>=5.0') - config.set('config', 'ENS_VAR5_NAME', 'WIND') - config.set('config', 'ENS_VAR5_LEVELS', 'Z10') - config.set('config', 'ENS_VAR5_THRESH', '>=5.0') - config.set('config', 'FCST_VAR1_NAME', 'APCP') - config.set('config', 'FCST_VAR1_LEVELS', 'A24') - config.set('config', 'FCST_VAR1_THRESH', '>0.01, >=10.0') - config.set('config', 'FCST_VAR1_OPTIONS', ('ens_ssvar_bin_size = 0.1; ' - 'ens_phist_bin_size = 0.05;')) - config.set('config', 'OBS_VAR1_NAME', 'APCP') - config.set('config', 'OBS_VAR1_LEVELS', 'A24') - config.set('config', 'OBS_VAR1_THRESH', '>0.01, >=10.0') - config.set('config', 'OBS_VAR1_OPTIONS', ('ens_ssvar_bin_size = 0.1; ' - 'ens_phist_bin_size = 0.05;')) - time_info = {} - - expected_ens_list = [{'index': '1', - 'ens_name': 'APCP', - 'ens_level': 'A24', - 'ens_thresh': ['>0.0', '>=10.0']}, - {'index': '2', - 'ens_name': 'REFC', - 'ens_level': 'L0', - 'ens_thresh': ['>35.0']}, - {'index': '3', - 'ens_name': 'UGRD', - 'ens_level': 'Z10', - 'ens_thresh': ['>=5.0']}, - {'index': '4', - 'ens_name': 'VGRD', - 'ens_level': 'Z10', - 'ens_thresh': ['>=5.0']}, - {'index': '5', - 'ens_name': 'WIND', - 'ens_level': 'Z10', - 'ens_thresh': ['>=5.0']}, - ] - expected_var_list = [{'index': '1', - 'fcst_name': 'APCP', - 'fcst_level': 'A24', - 'fcst_thresh': ['>0.01', '>=10.0'], - 'fcst_extra': ('ens_ssvar_bin_size = 0.1; ' - 'ens_phist_bin_size = 0.05;'), - 'obs_name': 'APCP', - 'obs_level': 'A24', - 'obs_thresh': ['>0.01', '>=10.0'], - 'obs_extra': ('ens_ssvar_bin_size = 0.1; ' - 'ens_phist_bin_size = 0.05;') - - }, - ] - - ensemble_var_list = util.parse_var_list(config, time_info, - data_type='ENS') - - # parse optional var list for FCST and/or OBS fields - var_list = util.parse_var_list(config, time_info, - met_tool='ensemble_stat') - - pp = pprint.PrettyPrinter() - print(f'ENSEMBLE_VAR_LIST:') - pp.pprint(ensemble_var_list) - print(f'VAR_LIST:') - pp.pprint(var_list) - - assert(len(ensemble_var_list) == len(expected_ens_list)) - for actual_ens, expected_ens in zip(ensemble_var_list, expected_ens_list): - for key, value in expected_ens.items(): - assert(actual_ens.get(key) == value) - - assert(len(var_list) == len(expected_var_list)) - for actual_var, expected_var in zip(var_list, expected_var_list): - for key, value in expected_var.items(): - assert(actual_var.get(key) == value) - -def test_parse_var_list_series_by(metplus_config): - config = metplus_config() - config.set('config', 'BOTH_EXTRACT_TILES_VAR1_NAME', 'RH') - config.set('config', 'BOTH_EXTRACT_TILES_VAR1_LEVELS', 'P850, P700') - config.set('config', 'BOTH_EXTRACT_TILES_VAR1_OUTPUT_NAMES', - 'RH_850mb, RH_700mb') - - config.set('config', 'BOTH_SERIES_ANALYSIS_VAR1_NAME', 'RH_850mb') - config.set('config', 'BOTH_SERIES_ANALYSIS_VAR1_LEVELS', 'P850') - config.set('config', 'BOTH_SERIES_ANALYSIS_VAR2_NAME', 'RH_700mb') - config.set('config', 'BOTH_SERIES_ANALYSIS_VAR2_LEVELS', 'P700') - time_info = {} - - expected_et_list = [{'index': '1', - 'fcst_name': 'RH', - 'fcst_level': 'P850', - 'fcst_output_name': 'RH_850mb', - 'obs_name': 'RH', - 'obs_level': 'P850', - 'obs_output_name': 'RH_850mb', - }, - {'index': '1', - 'fcst_name': 'RH', - 'fcst_level': 'P700', - 'fcst_output_name': 'RH_700mb', - 'obs_name': 'RH', - 'obs_level': 'P700', - 'obs_output_name': 'RH_700mb', - }, - ] - expected_sa_list = [{'index': '1', - 'fcst_name': 'RH_850mb', - 'fcst_level': 'P850', - 'obs_name': 'RH_850mb', - 'obs_level': 'P850', - }, - {'index': '2', - 'fcst_name': 'RH_700mb', - 'fcst_level': 'P700', - 'obs_name': 'RH_700mb', - 'obs_level': 'P700', - }, - ] - - actual_et_list = util.parse_var_list(config, - time_info=time_info, - met_tool='extract_tiles') - - actual_sa_list = util.parse_var_list(config, - met_tool='series_analysis') - - pp = pprint.PrettyPrinter() - print(f'ExtractTiles var list:') - pp.pprint(actual_et_list) - print(f'SeriesAnalysis var list:') - pp.pprint(actual_sa_list) - - assert(len(actual_et_list) == len(expected_et_list)) - for actual_et, expected_et in zip(actual_et_list, expected_et_list): - for key, value in expected_et.items(): - assert(actual_et.get(key) == value) - - assert(len(actual_sa_list) == len(expected_sa_list)) - for actual_sa, expected_sa in zip(actual_sa_list, expected_sa_list): - for key, value in expected_sa.items(): - assert(actual_sa.get(key) == value) - -@pytest.mark.parametrize( - 'input_dict, expected_list', [ - ({'init': datetime.datetime(2019, 2, 1, 6), - 'lead': 7200, }, - [ - {'index': '1', - 'fcst_name': 'FNAME_2019', - 'fcst_level': 'Z06', - 'obs_name': 'ONAME_2019', - 'obs_level': 'L06', - }, - {'index': '1', - 'fcst_name': 'FNAME_2019', - 'fcst_level': 'Z08', - 'obs_name': 'ONAME_2019', - 'obs_level': 'L08', - }, - ]), - ({'init': datetime.datetime(2021, 4, 13, 9), - 'lead': 10800, }, - [ - {'index': '1', - 'fcst_name': 'FNAME_2021', - 'fcst_level': 'Z09', - 'obs_name': 'ONAME_2021', - 'obs_level': 'L09', - }, - {'index': '1', - 'fcst_name': 'FNAME_2021', - 'fcst_level': 'Z12', - 'obs_name': 'ONAME_2021', - 'obs_level': 'L12', - }, - ]), - ] -) -def test_sub_var_list(metplus_config, input_dict, expected_list): - config = metplus_config() - config.set('config', 'FCST_VAR1_NAME', 'FNAME_{init?fmt=%Y}') - config.set('config', 'FCST_VAR1_LEVELS', 'Z{init?fmt=%H}, Z{valid?fmt=%H}') - config.set('config', 'OBS_VAR1_NAME', 'ONAME_{init?fmt=%Y}') - config.set('config', 'OBS_VAR1_LEVELS', 'L{init?fmt=%H}, L{valid?fmt=%H}') - - time_info = time_util.ti_calculate(input_dict) - - actual_temp = util.parse_var_list(config) - - pp = pprint.PrettyPrinter() - print(f'Actual var list (before sub):') - pp.pprint(actual_temp) - - actual_list = util.sub_var_list(actual_temp, time_info) - print(f'Actual var list (after sub):') - pp.pprint(actual_list) - - assert(len(actual_list) == len(expected_list)) - for actual, expected in zip(actual_list, expected_list): - for key, value in expected.items(): - assert(actual.get(key) == value) - -@pytest.mark.parametrize( - 'config_var_name, expected_indices, set_met_tool', [ - ('FCST_GRID_STAT_VAR1_NAME', ['1'], True), - ('FCST_GRID_STAT_VAR2_INPUT_FIELD_NAME', ['2'], True), - ('FCST_GRID_STAT_VAR3_FIELD_NAME', ['3'], True), - ('BOTH_GRID_STAT_VAR4_NAME', ['4'], True), - ('BOTH_GRID_STAT_VAR5_INPUT_FIELD_NAME', ['5'], True), - ('BOTH_GRID_STAT_VAR6_FIELD_NAME', ['6'], True), - ('FCST_VAR7_NAME', ['7'], False), - ('FCST_VAR8_INPUT_FIELD_NAME', ['8'], False), - ('FCST_VAR9_FIELD_NAME', ['9'], False), - ('BOTH_VAR10_NAME', ['10'], False), - ('BOTH_VAR11_INPUT_FIELD_NAME', ['11'], False), - ('BOTH_VAR12_FIELD_NAME', ['12'], False), - ] -) -def test_find_var_indices_fcst(metplus_config, - config_var_name, - expected_indices, - set_met_tool): - config = metplus_config() - data_types = ['FCST'] - config.set('config', config_var_name, "NAME1") - met_tool = 'grid_stat' if set_met_tool else None - var_name_indices = util.find_var_name_indices(config, - data_types=data_types, - met_tool=met_tool) - - assert(len(var_name_indices) == len(expected_indices)) - for actual_index in var_name_indices: - assert(actual_index in expected_indices) - -def test_parse_var_list_priority_fcst(metplus_config): - priority_list = ['FCST_GRID_STAT_VAR1_NAME', - 'FCST_GRID_STAT_VAR1_INPUT_FIELD_NAME', - 'FCST_GRID_STAT_VAR1_FIELD_NAME', - 'BOTH_GRID_STAT_VAR1_NAME', - 'BOTH_GRID_STAT_VAR1_INPUT_FIELD_NAME', - 'BOTH_GRID_STAT_VAR1_FIELD_NAME', - 'FCST_VAR1_NAME', - 'FCST_VAR1_INPUT_FIELD_NAME', - 'FCST_VAR1_FIELD_NAME', - 'BOTH_VAR1_NAME', - 'BOTH_VAR1_INPUT_FIELD_NAME', - 'BOTH_VAR1_FIELD_NAME', - ] - time_info = {} - - # loop through priority list, process, then pop first value off and - # process again until all items have been popped. - # This will check that list is in priority order - while(priority_list): - config = metplus_config() - for key in priority_list: - config.set('config', key, key.lower()) - - var_list = util.parse_var_list(config, time_info=time_info, - data_type='FCST', - met_tool='grid_stat') - - assert(len(var_list) == 1) - assert(var_list[0].get('fcst_name') == priority_list[0].lower()) - priority_list.pop(0) +from metplus.util.config_metplus import parse_var_list @pytest.mark.parametrize( 'before, after', [ @@ -662,244 +178,6 @@ def test_getlist_empty(): test_list = util.getlist(l) assert(test_list == []) -# field info only defined in the FCST_* variables -@pytest.mark.parametrize( - 'data_type, list_created', [ - (None, False), - ('FCST', True), - ('OBS', False), - ] -) -def test_parse_var_list_fcst_only(metplus_config, data_type, list_created): - conf = metplus_config() - conf.set('config', 'FCST_VAR1_NAME', "NAME1") - conf.set('config', 'FCST_VAR1_LEVELS', "LEVELS11, LEVELS12") - conf.set('config', 'FCST_VAR2_NAME', "NAME2") - conf.set('config', 'FCST_VAR2_LEVELS', "LEVELS21, LEVELS22") - - # this should not occur because OBS variables are missing - if util.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) - - var_list = util.parse_var_list(conf, time_info=None, data_type=data_type) - - # list will be created if requesting just OBS, but it should not be created if - # nothing was requested because FCST values are missing - if list_created: - assert(var_list[0]['fcst_name'] == "NAME1" and \ - var_list[1]['fcst_name'] == "NAME1" and \ - var_list[2]['fcst_name'] == "NAME2" and \ - var_list[3]['fcst_name'] == "NAME2" and \ - var_list[0]['fcst_level'] == "LEVELS11" and \ - var_list[1]['fcst_level'] == "LEVELS12" and \ - var_list[2]['fcst_level'] == "LEVELS21" and \ - var_list[3]['fcst_level'] == "LEVELS22") - else: - assert(not var_list) - -# field info only defined in the OBS_* variables -@pytest.mark.parametrize( - 'data_type, list_created', [ - (None, False), - ('OBS', True), - ('FCST', False), - ] -) -def test_parse_var_list_obs(metplus_config, data_type, list_created): - conf = metplus_config() - conf.set('config', 'OBS_VAR1_NAME', "NAME1") - conf.set('config', 'OBS_VAR1_LEVELS', "LEVELS11, LEVELS12") - conf.set('config', 'OBS_VAR2_NAME', "NAME2") - conf.set('config', 'OBS_VAR2_LEVELS', "LEVELS21, LEVELS22") - - # this should not occur because FCST variables are missing - if util.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) - - var_list = util.parse_var_list(conf, time_info=None, data_type=data_type) - - # list will be created if requesting just OBS, but it should not be created if - # nothing was requested because FCST values are missing - if list_created: - assert(var_list[0]['obs_name'] == "NAME1" and \ - var_list[1]['obs_name'] == "NAME1" and \ - var_list[2]['obs_name'] == "NAME2" and \ - var_list[3]['obs_name'] == "NAME2" and \ - var_list[0]['obs_level'] == "LEVELS11" and \ - var_list[1]['obs_level'] == "LEVELS12" and \ - var_list[2]['obs_level'] == "LEVELS21" and \ - var_list[3]['obs_level'] == "LEVELS22") - else: - assert(not var_list) - - -# field info only defined in the BOTH_* variables -@pytest.mark.parametrize( - 'data_type, list_created', [ - (None, 'fcst:obs'), - ('FCST', 'fcst'), - ('OBS', 'obs'), - ] -) -def test_parse_var_list_both(metplus_config, data_type, list_created): - conf = metplus_config() - conf.set('config', 'BOTH_VAR1_NAME', "NAME1") - conf.set('config', 'BOTH_VAR1_LEVELS', "LEVELS11, LEVELS12") - conf.set('config', 'BOTH_VAR2_NAME', "NAME2") - conf.set('config', 'BOTH_VAR2_LEVELS', "LEVELS21, LEVELS22") - - # this should not occur because BOTH variables are used - if not util.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) - - var_list = util.parse_var_list(conf, time_info=None, data_type=data_type) - print(f'var_list:{var_list}') - for list_to_check in list_created.split(':'): - if not var_list[0][f'{list_to_check}_name'] == "NAME1" or \ - not var_list[1][f'{list_to_check}_name'] == "NAME1" or \ - not var_list[2][f'{list_to_check}_name'] == "NAME2" or \ - not var_list[3][f'{list_to_check}_name'] == "NAME2" or \ - not var_list[0][f'{list_to_check}_level'] == "LEVELS11" or \ - not var_list[1][f'{list_to_check}_level'] == "LEVELS12" or \ - not var_list[2][f'{list_to_check}_level'] == "LEVELS21" or \ - not var_list[3][f'{list_to_check}_level'] == "LEVELS22": - assert(False) - -# field info defined in both FCST_* and OBS_* variables -def test_parse_var_list_fcst_and_obs(metplus_config): - conf = metplus_config() - conf.set('config', 'FCST_VAR1_NAME', "FNAME1") - conf.set('config', 'FCST_VAR1_LEVELS', "FLEVELS11, FLEVELS12") - conf.set('config', 'FCST_VAR2_NAME', "FNAME2") - conf.set('config', 'FCST_VAR2_LEVELS', "FLEVELS21, FLEVELS22") - conf.set('config', 'OBS_VAR1_NAME', "ONAME1") - conf.set('config', 'OBS_VAR1_LEVELS', "OLEVELS11, OLEVELS12") - conf.set('config', 'OBS_VAR2_NAME', "ONAME2") - conf.set('config', 'OBS_VAR2_LEVELS', "OLEVELS21, OLEVELS22") - - # this should not occur because FCST and OBS variables are found - if not util.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) - - var_list = util.parse_var_list(conf) - - assert(var_list[0]['fcst_name'] == "FNAME1" and \ - var_list[0]['obs_name'] == "ONAME1" and \ - var_list[1]['fcst_name'] == "FNAME1" and \ - var_list[1]['obs_name'] == "ONAME1" and \ - var_list[2]['fcst_name'] == "FNAME2" and \ - var_list[2]['obs_name'] == "ONAME2" and \ - var_list[3]['fcst_name'] == "FNAME2" and \ - var_list[3]['obs_name'] == "ONAME2" and \ - var_list[0]['fcst_level'] == "FLEVELS11" and \ - var_list[0]['obs_level'] == "OLEVELS11" and \ - var_list[1]['fcst_level'] == "FLEVELS12" and \ - var_list[1]['obs_level'] == "OLEVELS12" and \ - var_list[2]['fcst_level'] == "FLEVELS21" and \ - var_list[2]['obs_level'] == "OLEVELS21" and \ - var_list[3]['fcst_level'] == "FLEVELS22" and \ - var_list[3]['obs_level'] == "OLEVELS22") - -# VAR1 defined by FCST, VAR2 defined by OBS -def test_parse_var_list_fcst_and_obs_alternate(metplus_config): - conf = metplus_config() - conf.set('config', 'FCST_VAR1_NAME', "FNAME1") - conf.set('config', 'FCST_VAR1_LEVELS', "FLEVELS11, FLEVELS12") - conf.set('config', 'OBS_VAR2_NAME', "ONAME2") - conf.set('config', 'OBS_VAR2_LEVELS', "OLEVELS21, OLEVELS22") - - # configuration is invalid and parse var list should not give any results - assert(not util.validate_configuration_variables(conf, force_check=True)[1] and not util.parse_var_list(conf)) - -# VAR1 defined by OBS, VAR2 by FCST, VAR3 by both FCST AND OBS -@pytest.mark.parametrize( - 'data_type, list_len, name_levels', [ - (None, 0, None), - ('FCST', 4, ('FNAME2:FLEVELS21','FNAME2:FLEVELS22','FNAME3:FLEVELS31','FNAME3:FLEVELS32')), - ('OBS', 4, ('ONAME1:OLEVELS11','ONAME1:OLEVELS12','ONAME3:OLEVELS31','ONAME3:OLEVELS32')), - ] -) -def test_parse_var_list_fcst_and_obs_and_both(metplus_config, data_type, list_len, name_levels): - conf = metplus_config() - conf.set('config', 'OBS_VAR1_NAME', "ONAME1") - conf.set('config', 'OBS_VAR1_LEVELS', "OLEVELS11, OLEVELS12") - conf.set('config', 'FCST_VAR2_NAME', "FNAME2") - conf.set('config', 'FCST_VAR2_LEVELS', "FLEVELS21, FLEVELS22") - conf.set('config', 'FCST_VAR3_NAME', "FNAME3") - conf.set('config', 'FCST_VAR3_LEVELS', "FLEVELS31, FLEVELS32") - conf.set('config', 'OBS_VAR3_NAME', "ONAME3") - conf.set('config', 'OBS_VAR3_LEVELS', "OLEVELS31, OLEVELS32") - - # configuration is invalid and parse var list should not give any results - if util.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) - - var_list = util.parse_var_list(conf, time_info=None, data_type=data_type) - - if len(var_list) != list_len: - assert(False) - - if data_type is None: - assert(len(var_list) == 0) - - if name_levels is not None: - dt_lower = data_type.lower() - expected = [] - for name_level in name_levels: - name, level = name_level.split(':') - expected.append({f'{dt_lower}_name': name, - f'{dt_lower}_level': level}) - - for expect, reality in zip(expected,var_list): - if expect[f'{dt_lower}_name'] != reality[f'{dt_lower}_name']: - assert(False) - - if expect[f'{dt_lower}_level'] != reality[f'{dt_lower}_level']: - assert(False) - - assert(True) - -# option defined in obs only -@pytest.mark.parametrize( - 'data_type, list_len', [ - (None, 0), - ('FCST', 2), - ('OBS', 0), - ] -) -def test_parse_var_list_fcst_only_options(metplus_config, data_type, list_len): - conf = metplus_config() - conf.set('config', 'FCST_VAR1_NAME', "NAME1") - conf.set('config', 'FCST_VAR1_LEVELS', "LEVELS11, LEVELS12") - conf.set('config', 'FCST_VAR1_THRESH', ">1, >2") - conf.set('config', 'OBS_VAR1_OPTIONS', "OOPTIONS11") - - # this should not occur because OBS variables are missing - if util.validate_configuration_variables(conf, force_check=True)[1]: - assert(False) - - var_list = util.parse_var_list(conf, time_info=None, data_type=data_type) - - assert(len(var_list) == list_len) - -@pytest.mark.parametrize( - 'met_tool, indices', [ - (None, {'1': ['FCST']}), - ('GRID_STAT', {'2': ['FCST']}), - ('ENSEMBLE_STAT', {}), - ] -) -def test_find_var_indices_wrapper_specific(metplus_config, met_tool, indices): - conf = metplus_config() - data_type = 'FCST' - conf.set('config', f'{data_type}_VAR1_NAME', "NAME1") - conf.set('config', f'{data_type}_GRID_STAT_VAR2_NAME', "GSNAME2") - - var_name_indices = util.find_var_name_indices(conf, data_types=[data_type], - met_tool=met_tool) - - assert(var_name_indices == indices) - def test_get_lead_sequence_lead(metplus_config): input_dict = {'valid': datetime.datetime(2019, 2, 1, 13)} conf = metplus_config() @@ -1089,201 +367,6 @@ def test_get_lead_sequence_init_min_10(metplus_config): lead_seq = [12, 24] assert(test_seq == [relativedelta(hours=lead) for lead in lead_seq]) -@pytest.mark.parametrize( - 'item_list, extension, is_valid', [ - (['FCST'], 'NAME', False), - (['OBS'], 'NAME', False), - (['FCST', 'OBS'], 'NAME', True), - (['BOTH'], 'NAME', True), - (['FCST', 'OBS', 'BOTH'], 'NAME', False), - (['FCST', 'ENS'], 'NAME', False), - (['OBS', 'ENS'], 'NAME', False), - (['FCST', 'OBS', 'ENS'], 'NAME', True), - (['BOTH', 'ENS'], 'NAME', True), - (['FCST', 'OBS', 'BOTH', 'ENS'], 'NAME', False), - - (['FCST', 'OBS'], 'THRESH', True), - (['BOTH'], 'THRESH', True), - (['FCST', 'OBS', 'BOTH'], 'THRESH', False), - (['FCST', 'OBS', 'ENS'], 'THRESH', True), - (['BOTH', 'ENS'], 'THRESH', True), - (['FCST', 'OBS', 'BOTH', 'ENS'], 'THRESH', False), - - (['FCST'], 'OPTIONS', True), - (['OBS'], 'OPTIONS', True), - (['FCST', 'OBS'], 'OPTIONS', True), - (['BOTH'], 'OPTIONS', True), - (['FCST', 'OBS', 'BOTH'], 'OPTIONS', False), - (['FCST', 'ENS'], 'OPTIONS', True), - (['OBS', 'ENS'], 'OPTIONS', True), - (['FCST', 'OBS', 'ENS'], 'OPTIONS', True), - (['BOTH', 'ENS'], 'OPTIONS', True), - (['FCST', 'OBS', 'BOTH', 'ENS'], 'OPTIONS', False), - - (['FCST', 'OBS', 'BOTH'], 'LEVELS', False), - (['FCST', 'OBS'], 'LEVELS', True), - (['BOTH'], 'LEVELS', True), - (['FCST', 'OBS', 'ENS'], 'LEVELS', True), - (['BOTH', 'ENS'], 'LEVELS', True), - - ] -) -def test_is_var_item_valid(metplus_config, item_list, extension, is_valid): - conf = metplus_config() - assert(util.is_var_item_valid(item_list, '1', extension, conf)[0] == is_valid) - -@pytest.mark.parametrize( - 'item_list, configs_to_set, is_valid', [ - - (['FCST'], {'FCST_VAR1_LEVELS': 'A06', - 'OBS_VAR1_NAME': 'script_name.py something else'}, True), - (['FCST'], {'FCST_VAR1_LEVELS': 'A06', - 'OBS_VAR1_NAME': 'APCP'}, False), - (['OBS'], {'OBS_VAR1_LEVELS': '"(*,*)"', - 'FCST_VAR1_NAME': 'script_name.py something else'}, True), - (['OBS'], {'OBS_VAR1_LEVELS': '"(*,*)"', - 'FCST_VAR1_NAME': 'APCP'}, False), - - (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06', - 'OBS_VAR1_NAME': 'script_name.py something else'}, True), - (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06', - 'OBS_VAR1_NAME': 'APCP'}, False), - (['OBS', 'ENS'], {'OBS_VAR1_LEVELS': '"(*,*)"', - 'FCST_VAR1_NAME': 'script_name.py something else'}, True), - (['OBS', 'ENS'], {'OBS_VAR1_LEVELS': '"(*,*)"', - 'FCST_VAR1_NAME': 'APCP'}, False), - - (['FCST'], {'FCST_VAR1_LEVELS': 'A06, A12', - 'OBS_VAR1_NAME': 'script_name.py something else'}, False), - (['FCST'], {'FCST_VAR1_LEVELS': 'A06, A12', - 'OBS_VAR1_NAME': 'APCP'}, False), - (['OBS'], {'OBS_VAR1_LEVELS': '"(0,*,*)", "(1,*,*)"', - 'FCST_VAR1_NAME': 'script_name.py something else'}, False), - - (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06, A12', - 'OBS_VAR1_NAME': 'script_name.py something else'}, False), - (['FCST', 'ENS'], {'FCST_VAR1_LEVELS': 'A06, A12', - 'OBS_VAR1_NAME': 'APCP'}, False), - (['OBS', 'ENS'], {'OBS_VAR1_LEVELS': '"(0,*,*)", "(1,*,*)"', - 'FCST_VAR1_NAME': 'script_name.py something else'}, False), - - ] -) -def test_is_var_item_valid_levels(metplus_config, item_list, configs_to_set, is_valid): - conf = metplus_config() - for key, value in configs_to_set.items(): - conf.set('config', key, value) - - assert(util.is_var_item_valid(item_list, '1', 'LEVELS', conf)[0] == is_valid) - -# test that if wrapper specific field info is specified, it only gets -# values from that list. All generic values should be read if no -# wrapper specific field info variables are specified -def test_parse_var_list_wrapper_specific(metplus_config): - conf = metplus_config() - conf.set('config', 'FCST_VAR1_NAME', "ENAME1") - conf.set('config', 'FCST_VAR1_LEVELS', "ELEVELS11, ELEVELS12") - conf.set('config', 'FCST_VAR2_NAME', "ENAME2") - conf.set('config', 'FCST_VAR2_LEVELS', "ELEVELS21, ELEVELS22") - conf.set('config', 'FCST_GRID_STAT_VAR1_NAME', "GNAME1") - conf.set('config', 'FCST_GRID_STAT_VAR1_LEVELS', "GLEVELS11, GLEVELS12") - - e_var_list = util.parse_var_list(conf, - time_info=None, - data_type='FCST', - met_tool='ensemble_stat') - - g_var_list = util.parse_var_list(conf, - time_info=None, - data_type='FCST', - met_tool='grid_stat') - - assert(len(e_var_list) == 4 and len(g_var_list) == 2 and - e_var_list[0]['fcst_name'] == "ENAME1" and - e_var_list[1]['fcst_name'] == "ENAME1" and - e_var_list[2]['fcst_name'] == "ENAME2" and - e_var_list[3]['fcst_name'] == "ENAME2" and - e_var_list[0]['fcst_level'] == "ELEVELS11" and - e_var_list[1]['fcst_level'] == "ELEVELS12" and - e_var_list[2]['fcst_level'] == "ELEVELS21" and - e_var_list[3]['fcst_level'] == "ELEVELS22" and - g_var_list[0]['fcst_name'] == "GNAME1" and - g_var_list[1]['fcst_name'] == "GNAME1" and - g_var_list[0]['fcst_level'] == "GLEVELS11" and - g_var_list[1]['fcst_level'] == "GLEVELS12") - -@pytest.mark.parametrize( - 'input_list, expected_list', [ - ('Point2Grid', ['Point2Grid']), - # MET documentation syntax (with dashes) - ('Pcp-Combine, Grid-Stat, Ensemble-Stat', ['PCPCombine', - 'GridStat', - 'EnsembleStat']), - ('Point-Stat', ['PointStat']), - ('Mode, MODE Time Domain', ['MODE', - 'MTD']), - # actual tool name (lower case underscore) - ('point_stat, grid_stat, ensemble_stat', ['PointStat', - 'GridStat', - 'EnsembleStat']), - ('mode, mtd', ['MODE', - 'MTD']), - ('ascii2nc, pb2nc, regrid_data_plane', ['ASCII2NC', - 'PB2NC', - 'RegridDataPlane']), - ('pcp_combine, tc_pairs, tc_stat', ['PCPCombine', - 'TCPairs', - 'TCStat']), - ('gen_vx_mask, stat_analysis, series_analysis', ['GenVxMask', - 'StatAnalysis', - 'SeriesAnalysis']), - # old capitalization format - ('PcpCombine, Ascii2Nc, TcStat, TcPairs', ['PCPCombine', - 'ASCII2NC', - 'TCStat', - 'TCPairs']), - # remove MakePlots from list - ('StatAnalysis, MakePlots', ['StatAnalysis']), - ] -) -def test_get_process_list(metplus_config, input_list, expected_list): - conf = metplus_config() - conf.set('config', 'PROCESS_LIST', input_list) - process_list = util.get_process_list(conf) - output_list = [item[0] for item in process_list] - assert(output_list == expected_list) - -@pytest.mark.parametrize( - 'input_list, expected_list', [ - # no instances - ('Point2Grid', [('Point2Grid', None)]), - # one with instance one without - ('PcpCombine, GridStat(my_instance)', [('PCPCombine', None), - ('GridStat', 'my_instance')]), - # duplicate process, one with instance one without - ('TCStat, ExtractTiles, TCStat(for_series), SeriesAnalysis', ( - [('TCStat',None), - ('ExtractTiles',None), - ('TCStat', 'for_series'), - ('SeriesAnalysis',None),])), - # two processes, both with instances - ('mode(uno), mtd(dos)', [('MODE', 'uno'), - ('MTD', 'dos')]), - # lower-case names, first with instance, second without - ('ascii2nc(some_name), pb2nc', [('ASCII2NC', 'some_name'), - ('PB2NC', None)]), - # duplicate process, both with different instances - ('tc_stat(one), tc_pairs, tc_stat(two)', [('TCStat', 'one'), - ('TCPairs', None), - ('TCStat', 'two')]), - ] -) -def test_get_process_list_instances(metplus_config, input_list, expected_list): - conf = metplus_config() - conf.set('config', 'PROCESS_LIST', input_list) - output_list = util.get_process_list(conf) - assert(output_list == expected_list) - @pytest.mark.parametrize( 'time_from_conf, fmt, is_datetime', [ ('', '%Y', False), @@ -1363,23 +446,6 @@ def test_fix_list(list_str, expected_fixed_list): def test_camel_to_underscore(camel, underscore): assert(util.camel_to_underscore(camel) == underscore) -@pytest.mark.parametrize( - 'filepath, template, expected_result', [ - (os.getcwd(), 'file.{valid?fmt=%Y%m%d%H}.ext', None), - ('file.2019020104.ext', 'file.{valid?fmt=%Y%m%d%H}.ext', datetime.datetime(2019, 2, 1, 4)), - ('filename.2019020104.ext', 'file.{valid?fmt=%Y%m%d%H}.ext', None), - ('file.2019020104.ext.gz', 'file.{valid?fmt=%Y%m%d%H}.ext', datetime.datetime(2019, 2, 1, 4)), - ('filename.2019020104.ext.gz', 'file.{valid?fmt=%Y%m%d%H}.ext', None), - ] -) -def test_get_time_from_file(filepath, template, expected_result): - result = util.get_time_from_file(filepath, template) - - if result is None: - assert(expected_result is None) - else: - assert(result['valid'] == expected_result) - @pytest.mark.parametrize( 'value, expected_result', [ (3.3, 3.5), @@ -1404,29 +470,6 @@ def test_round_0p5(value, expected_result): def test_comparison_to_letter_format(expression, expected_result): assert(util.comparison_to_letter_format(expression) == expected_result) -@pytest.mark.parametrize( - 'conf_items, met_tool, expected_result', [ - ({'CUSTOM_LOOP_LIST': "one, two, three"}, '', ['one', 'two', 'three']), - ({'CUSTOM_LOOP_LIST': "one, two, three", - 'GRID_STAT_CUSTOM_LOOP_LIST': "four, five",}, 'grid_stat', ['four', 'five']), - ({'CUSTOM_LOOP_LIST': "one, two, three", - 'GRID_STAT_CUSTOM_LOOP_LIST': "four, five",}, 'point_stat', ['one', 'two', 'three']), - ({'CUSTOM_LOOP_LIST': "one, two, three", - 'ASCII2NC_CUSTOM_LOOP_LIST': "four, five",}, 'ascii2nc', ['four', 'five']), - # fails to read custom loop list for point2grid because there are underscores in name - ({'CUSTOM_LOOP_LIST': "one, two, three", - 'POINT_2_GRID_CUSTOM_LOOP_LIST': "four, five",}, 'point2grid', ['one', 'two', 'three']), - ({'CUSTOM_LOOP_LIST': "one, two, three", - 'POINT2GRID_CUSTOM_LOOP_LIST': "four, five",}, 'point2grid', ['four', 'five']), - ] -) -def test_get_custom_string_list(metplus_config, conf_items, met_tool, expected_result): - config = metplus_config() - for conf_key, conf_value in conf_items.items(): - config.set('config', conf_key, conf_value) - - assert(util.get_custom_string_list(config, met_tool) == expected_result) - @pytest.mark.parametrize( 'skip_times_conf, expected_dict', [ ('"%d:30,31"', {'%d': ['30','31']}), @@ -1574,7 +617,7 @@ def test_get_storm_ids(metplus_config, filename, expected_result): 'stat_data', filename) - assert(util.get_storm_ids(filepath) == expected_result) + assert(util.get_storms(filepath, id_only=True) == expected_result) @pytest.mark.parametrize( 'filename, expected_result', [ @@ -1669,80 +712,6 @@ def test_format_var_items_options_semicolon(config_value, result = var_items.get('extra') assert(result == expected_result) -@pytest.mark.parametrize( - 'config_overrides, expected_results', [ - # 2 levels - ({'FCST_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {fcst_level}', - 'FCST_VAR1_LEVELS': 'P500,P250', - 'OBS_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {obs_level}', - 'OBS_VAR1_LEVELS': 'P500,P250', - }, - ['read_data.py TMP 20200201 P500', - 'read_data.py TMP 20200201 P250', - ]), - ({'BOTH_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {fcst_level}', - 'BOTH_VAR1_LEVELS': 'P500,P250', - }, - ['read_data.py TMP 20200201 P500', - 'read_data.py TMP 20200201 P250', - ]), - # no level but level specified in name - ({'FCST_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {fcst_level}', - 'OBS_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d} {obs_level}', - }, - ['read_data.py TMP 20200201 ', - ]), - # no level - ({'FCST_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d}', - 'OBS_VAR1_NAME': 'read_data.py TMP {valid?fmt=%Y%m%d}', - }, - ['read_data.py TMP 20200201', - ]), - # real example - ({'BOTH_VAR1_NAME': ('myscripts/read_nc2xr.py ' - 'mydata/forecast_file.nc4 TMP ' - '{valid?fmt=%Y%m%d_%H%M} {fcst_level}'), - 'BOTH_VAR1_LEVELS': 'P1000,P850,P700,P500,P250,P100', - }, - [('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' - ' P1000'), - ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' - ' P850'), - ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' - ' P700'), - ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' - ' P500'), - ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' - ' P250'), - ('myscripts/read_nc2xr.py mydata/forecast_file.nc4 TMP 20200201_1225' - ' P100'), - ]), - ] -) -def test_parse_var_list_py_embed_multi_levels(metplus_config, config_overrides, - expected_results): - config = metplus_config() - for key, value in config_overrides.items(): - config.set('config', key, value) - - time_info = {'valid': datetime.datetime(2020, 2, 1, 12, 25)} - var_list = util.parse_var_list(config, - time_info=time_info, - data_type=None) - assert(len(var_list) == len(expected_results)) - - for var_item, expected_result in zip(var_list, expected_results): - assert(var_item['fcst_name'] == expected_result) - - # run again with data type specified - var_list = util.parse_var_list(config, - time_info=time_info, - data_type='FCST') - assert(len(var_list) == len(expected_results)) - - for var_item, expected_result in zip(var_list, expected_results): - assert(var_item['fcst_name'] == expected_result) - @pytest.mark.parametrize( 'level, expected_result', [ ('level', 'level'), @@ -1753,3 +722,63 @@ def test_parse_var_list_py_embed_multi_levels(metplus_config, config_overrides, ) def test_format_level(level, expected_result): assert(util.format_level(level) == expected_result) + +@pytest.mark.parametrize( + 'input_dict, expected_list', [ + ({'init': datetime.datetime(2019, 2, 1, 6), + 'lead': 7200, }, + [ + {'index': '1', + 'fcst_name': 'FNAME_2019', + 'fcst_level': 'Z06', + 'obs_name': 'ONAME_2019', + 'obs_level': 'L06', + }, + {'index': '1', + 'fcst_name': 'FNAME_2019', + 'fcst_level': 'Z08', + 'obs_name': 'ONAME_2019', + 'obs_level': 'L08', + }, + ]), + ({'init': datetime.datetime(2021, 4, 13, 9), + 'lead': 10800, }, + [ + {'index': '1', + 'fcst_name': 'FNAME_2021', + 'fcst_level': 'Z09', + 'obs_name': 'ONAME_2021', + 'obs_level': 'L09', + }, + {'index': '1', + 'fcst_name': 'FNAME_2021', + 'fcst_level': 'Z12', + 'obs_name': 'ONAME_2021', + 'obs_level': 'L12', + }, + ]), + ] +) +def test_sub_var_list(metplus_config, input_dict, expected_list): + config = metplus_config() + config.set('config', 'FCST_VAR1_NAME', 'FNAME_{init?fmt=%Y}') + config.set('config', 'FCST_VAR1_LEVELS', 'Z{init?fmt=%H}, Z{valid?fmt=%H}') + config.set('config', 'OBS_VAR1_NAME', 'ONAME_{init?fmt=%Y}') + config.set('config', 'OBS_VAR1_LEVELS', 'L{init?fmt=%H}, L{valid?fmt=%H}') + + time_info = time_util.ti_calculate(input_dict) + + actual_temp = parse_var_list(config) + + pp = pprint.PrettyPrinter() + print(f'Actual var list (before sub):') + pp.pprint(actual_temp) + + actual_list = util.sub_var_list(actual_temp, time_info) + print(f'Actual var list (after sub):') + pp.pprint(actual_list) + + assert(len(actual_list) == len(expected_list)) + for actual, expected in zip(actual_list, expected_list): + for key, value in expected.items(): + assert(actual.get(key) == value) diff --git a/internal_tests/pytests/mode/test_mode_wrapper.py b/internal_tests/pytests/mode/test_mode_wrapper.py index 69a17f9146..8c4144fd68 100644 --- a/internal_tests/pytests/mode/test_mode_wrapper.py +++ b/internal_tests/pytests/mode/test_mode_wrapper.py @@ -74,7 +74,8 @@ def set_minimum_config_settings(config): ({'MODE_REGRID_TO_GRID': 'FCST', }, - {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}'}), + {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}', + 'REGRID_TO_GRID': 'FCST'}), ({'MODE_REGRID_METHOD': 'NEAREST', }, @@ -100,7 +101,8 @@ def set_minimum_config_settings(config): }, {'METPLUS_REGRID_DICT': ('regrid = {to_grid = FCST;method = NEAREST;' 'width = 1;vld_thresh = 0.5;shape = SQUARE;}' - )}), + ), + 'REGRID_TO_GRID': 'FCST'}), ({'MODE_QUILT': 'True'}, {'METPLUS_QUILT': 'quilt = TRUE;'}), @@ -362,7 +364,11 @@ def test_mode_single_field(metplus_config, config_overrides, assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in expected_output + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/internal_tests/pytests/mtd/test_mtd_wrapper.py b/internal_tests/pytests/mtd/test_mtd_wrapper.py index 27cf14b611..aff12a1bb7 100644 --- a/internal_tests/pytests/mtd/test_mtd_wrapper.py +++ b/internal_tests/pytests/mtd/test_mtd_wrapper.py @@ -1,39 +1,12 @@ #!/usr/bin/env python3 import os -import sys -import re -import logging import datetime -from collections import namedtuple import pytest -import produtil - from metplus.wrappers.mtd_wrapper import MTDWrapper -from metplus.util import met_util as util - -# --------------------TEST CONFIGURATION and FIXTURE SUPPORT ------------- -# -# The test configuration and fixture support the additional configuration -# files used in METplus -# !!!!!!!!!!!!!!! -# !!!IMPORTANT!!! -# !!!!!!!!!!!!!!! -# The following two methods should be included in ALL pytest tests for METplus. -# -# -#def pytest_addoption(parser): -# parser.addoption("-c", action="store", help=" -c ") - - -# @pytest.fixture -#def cmdopt(request): -# return request.config.getoption("-c") -# -----------------FIXTURES THAT CAN BE USED BY ALL TESTS---------------- -#@pytest.fixture def mtd_wrapper(metplus_config, lead_seq=None): """! Returns a default MTDWrapper with /path/to entries in the metplus_system.conf and metplus_runtime.conf configuration diff --git a/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py b/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py index f9c3ab8cc0..a1c202c335 100644 --- a/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py +++ b/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py @@ -283,7 +283,7 @@ def test_find_input_files(metplus_config, offsets, offset_to_find): ] ) def test_pb2nc_all_fields(metplus_config, config_overrides, - env_var_values): + env_var_values): input_dir = '/some/input/dir' config = metplus_config() @@ -341,7 +341,11 @@ def test_pb2nc_all_fields(metplus_config, config_overrides, assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py index e8c2302f35..f706e702d1 100755 --- a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py +++ b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py @@ -61,12 +61,10 @@ def test_met_dictionary_in_var_options(metplus_config): ({'DESC': 'my_desc'}, {'METPLUS_DESC': 'desc = "my_desc";'}), - ({'OBTYPE': 'my_obtype'}, - {'METPLUS_OBTYPE': 'obtype = "my_obtype";'}), - ({'POINT_STAT_REGRID_TO_GRID': 'FCST', }, - {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}'}), + {'METPLUS_REGRID_DICT': 'regrid = {to_grid = FCST;}', + 'REGRID_TO_GRID': 'FCST'}), ({'POINT_STAT_REGRID_METHOD': 'NEAREST', }, @@ -92,7 +90,8 @@ def test_met_dictionary_in_var_options(metplus_config): }, {'METPLUS_REGRID_DICT': ('regrid = {to_grid = FCST;method = NEAREST;' 'width = 1;vld_thresh = 0.5;shape = SQUARE;}' - )}), + ), + 'REGRID_TO_GRID': 'FCST'}), # mask grid and poly (old config var) ({'POINT_STAT_MASK_GRID': 'FULL', @@ -139,15 +138,6 @@ def test_met_dictionary_in_var_options(metplus_config): 'sid = ["one", "two"];', }), - ({'POINT_STAT_NEIGHBORHOOD_COV_THRESH': '>=0.5'}, - {'METPLUS_NBRHD_COV_THRESH': 'cov_thresh = [>=0.5];'}), - - ({'POINT_STAT_NEIGHBORHOOD_WIDTH': '1,2'}, - {'METPLUS_NBRHD_WIDTH': 'width = [1, 2];'}), - - ({'POINT_STAT_NEIGHBORHOOD_SHAPE': 'CIRCLE'}, - {'METPLUS_NBRHD_SHAPE': 'shape = CIRCLE;'}), - ({'POINT_STAT_OUTPUT_PREFIX': 'my_output_prefix'}, {'METPLUS_OUTPUT_PREFIX': 'output_prefix = "my_output_prefix";'}), @@ -159,6 +149,8 @@ def test_met_dictionary_in_var_options(metplus_config): }, {'METPLUS_OBS_WINDOW_DICT': 'obs_window = {beg = -2700;end = 2700;}', + 'OBS_WINDOW_BEGIN': '-2700', + 'OBS_WINDOW_END': '2700' }), ({'POINT_STAT_CLIMO_CDF_CDF_BINS': '1', }, @@ -507,7 +499,11 @@ def test_point_stat_all_fields(metplus_config, config_overrides, assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py b/internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py index 9486c629b6..825a48d67a 100644 --- a/internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py +++ b/internal_tests/pytests/tc_gen/test_tc_gen_wrapper.py @@ -346,7 +346,11 @@ def test_tc_gen(metplus_config, config_overrides, env_var_values): assert(cmd == expected_cmd) # check that environment variables were set properly - for env_var_key in wrapper.WRAPPER_ENV_VAR_KEYS: + # including deprecated env vars (not in wrapper env var keys) + env_var_keys = (wrapper.WRAPPER_ENV_VAR_KEYS + + [name for name in env_var_values + if name not in wrapper.WRAPPER_ENV_VAR_KEYS]) + for env_var_key in env_var_keys: match = next((item for item in env_vars if item.startswith(env_var_key)), None) assert(match is not None) diff --git a/metplus/util/__init__.py b/metplus/util/__init__.py index 570ee45208..b124dcb5b8 100644 --- a/metplus/util/__init__.py +++ b/metplus/util/__init__.py @@ -1,7 +1,8 @@ +from .constants import * from .metplus_check import * from .doc_util import * from .config_metplus import * from .time_util import * from .met_util import * from .string_template_substitution import * -from .met_dictionary_info import * +from .met_config import * diff --git a/metplus/util/config_metplus.py b/metplus/util/config_metplus.py index 3a6df221d3..892990994e 100644 --- a/metplus/util/config_metplus.py +++ b/metplus/util/config_metplus.py @@ -13,26 +13,23 @@ import re import sys import logging -import collections import datetime import shutil -from os.path import dirname, realpath -import inspect from configparser import ConfigParser, NoOptionError from pathlib import Path -import copy from produtil.config import ProdConfig -import produtil.fileop from . import met_util as util +from .string_template_substitution import get_tags, do_string_sub +from .met_util import getlist, is_python_script, format_var_items +from .doc_util import get_wrapper_name """!Creates the initial METplus directory structure, loads information into each job. This module is used to create the initial METplus conf file in the first METplus job via the metplus.config_metplus.launch(). -The metplus.config_metplus.load() then reloads that configuration. The launch() function does more than just create the conf file though. It creates several initial files and directories @@ -43,15 +40,16 @@ """ '''!@var __all__ -All symbols exported by "from metplus.util.config.config_metplus import *" +All symbols exported by "from metplus.util.config_metplus import *" ''' -__all__ = ['load', - 'launch', - 'parse_launch_args', - 'setup', - 'METplusConfig', - 'METplusLogFormatter', - ] +__all__ = [ + 'setup', + 'get_custom_string_list', + 'find_indices_in_config_section', + 'parse_var_list', + 'get_process_list', + 'validate_configuration_variables', +] '''!@var METPLUS_BASE The METplus installation directory @@ -81,7 +79,33 @@ 'metplus_logging.conf' ] -def get_default_config_list(parm_base=None): +def setup(args, logger=None, base_confs=None): + """!The METplus setup function. + @param args list of configuration files or configuration + variable overrides. Reads all configuration inputs and returns + a configuration object. + """ + if base_confs is None: + base_confs = _get_default_config_list() + + # Setup Task logger, Until a Conf object is created, Task logger is + # only logging to tty, not a file. + if logger is None: + logger = logging.getLogger('metplus') + + logger.info('Starting METplus configuration setup.') + + override_list = _parse_launch_args(args, logger) + + # add default config files to override list + override_list = base_confs + override_list + config = launch(override_list) + + logger.debug('Completed METplus configuration setup.') + + return config + +def _get_default_config_list(parm_base=None): """! Get list of default METplus config files. Look through BASE_CONFS list and check if each file exists under the parm base. Add each to a list if they do exist. @@ -105,38 +129,12 @@ def get_default_config_list(parm_base=None): default_config_list.append(conf_path) if not default_config_list: - print(f"ERROR: No default config files found in {conf_dir}") + print(f"FATAL: No default config files found in {conf_dir}") sys.exit(1) return default_config_list -def setup(args, logger=None, base_confs=None): - """!The METplus setup function. - @param args list of configuration files or configuration - variable overrides. Reads all configuration inputs and returns - a configuration object. - """ - if base_confs is None: - base_confs = get_default_config_list() - - # Setup Task logger, Until a Conf object is created, Task logger is - # only logging to tty, not a file. - if logger is None: - logger = logging.getLogger('metplus') - - logger.info('Starting METplus configuration setup.') - - override_list = parse_launch_args(args, logger) - - # add default config files to override list - override_list = base_confs + override_list - config = launch(override_list) - - logger.debug('Completed METplus configuration setup.') - - return config - -def parse_launch_args(args, logger): +def _parse_launch_args(args, logger): """! Parsed arguments to scripts that launch the METplus wrappers. Options: @@ -207,8 +205,7 @@ def launch(config_list): read files. Explicit configuration variables are read after all config files are processed. - @param file_list list of configuration files to read - @param moreopt explicit configuration variable overrides + @param config_list list of configuration files to process """ config = METplusConfig() logger = config.log() @@ -235,7 +232,7 @@ def launch(config_list): config_format_list.append(f'{section}.{key}={value}') # move all config variables from old sections into the [config] section - config.move_all_to_config_section() + config._move_all_to_config_section() # save list of user configuration files in a variable config.set('config', 'CONFIG_INPUT', ','.join(config_format_list)) @@ -265,19 +262,6 @@ def launch(config_list): return config -def load(filename): - """!Loads the METplusConfig created by the launch() function. - - Creates an METplusConfig object for a METplus workflow that was - previously initialized by metplus.config_metplus.launch. - The only argument is the name of the config file produced by - the launch command. - - @param filename The metplus*.conf file created by launch()""" - config = METplusConfig() - config.read(filename) - return config - def _set_logvars(config, logger=None): """!Sets and adds the LOG_METPLUS and LOG_TIMESTAMP to the config object. If LOG_METPLUS was already defined by the @@ -292,44 +276,22 @@ def _set_logvars(config, logger=None): if logger is None: logger = config.log() - # LOG_TIMESTAMP_TEMPLATE is not required in the conf file, - # so lets first test for that. - log_timestamp_template = config.getstr('config', 'LOG_TIMESTAMP_TEMPLATE', '') - if log_timestamp_template: - # Note: strftime appears to handle if log_timestamp_template - # is a string ie. 'blah' and not a valid set of % directives %Y%m%d, - # it does return the string 'blah', instead of crashing. - # However, I'm still going to test for a valid % directive and - # set a default. It probably is ok to remove the if not block pattern - # test, and not set a default, especially if causing some unintended - # consequences or the pattern is not capturing a valid directive. - # The reality is, the user is expected to have entered a correct - # directive in the conf file. - # This pattern is meant to test for a repeating set of - # case insensitive %(AnyAlphabeticCharacter), ie. %Y%m ... - # The basic pattern is (%+[a-z])+ , %+ allows for 1 or more - # % characters, ie. %%Y, %% is a valid directive. - # (?i) case insensitive, \A begin string \Z end of string - if not re.match(r'(?i)\A(?:(%+[a-z])+)\Z', log_timestamp_template): - logger.warning('Your LOG_TIMESTAMP_TEMPLATE is not ' - 'a valid strftime directive: %s' % repr(log_timestamp_template)) - logger.info('Using the following default: %Y%m%d%H') - log_timestamp_template = '%Y%m%d%H' - date_t = datetime.datetime.now() - if config.getbool('config', 'LOG_TIMESTAMP_USE_DATATIME', False): - if util.is_loop_by_init(config): - date_t = datetime.datetime.strptime(config.getstr('config', - 'INIT_BEG'), - config.getstr('config', - 'INIT_TIME_FMT')) - else: - date_t = datetime.datetime.strptime(config.getstr('config', - 'VALID_BEG'), - config.getstr('config', - 'VALID_TIME_FMT')) - log_filenametimestamp = date_t.strftime(log_timestamp_template) + log_timestamp_template = config.getstr('config', 'LOG_TIMESTAMP_TEMPLATE', + '') + if config.getbool('config', 'LOG_TIMESTAMP_USE_DATATIME', False): + if util.is_loop_by_init(config): + loop_by = 'INIT' + else: + loop_by = 'VALID' + + date_t = datetime.datetime.strptime( + config.getstr('config', f'{loop_by}_BEG'), + config.getstr('config', f'{loop_by}_TIME_FMT') + ) else: - log_filenametimestamp = '' + date_t = datetime.datetime.now() + + log_filenametimestamp = date_t.strftime(log_timestamp_template) log_dir = config.getdir('LOG_DIR') @@ -343,11 +305,14 @@ def _set_logvars(config, logger=None): if config.has_option('config', 'LOG_METPLUS'): user_defined_log_file = True # strinterp will set metpluslog to '' if LOG_METPLUS = is unset. - metpluslog = config.strinterp('config', '{LOG_METPLUS}', - LOG_TIMESTAMP_TEMPLATE=log_filenametimestamp) - - # test if there is any path information, if there is, assUme it is as intended, - # if there is not, than add log_dir. + metpluslog = config.strinterp( + 'config', + '{LOG_METPLUS}', + LOG_TIMESTAMP_TEMPLATE=log_filenametimestamp + ) + + # test if there is any path information, if there is, + # assume it is as intended, if there is not, than add log_dir. if metpluslog: if os.path.basename(metpluslog) == metpluslog: metpluslog = os.path.join(log_dir, metpluslog) @@ -360,12 +325,15 @@ def _set_logvars(config, logger=None): # it out, in case the group wanted a stand alone metplus log filename # template variable. - # If metpluslog_filename includes a path, python joins it intelligently. + # If metpluslog_filename includes a path, python joins it intelligently # Set the metplus log filename. - # strinterp will set metpluslog_filename to '' if LOG_FILENAME_TEMPLATE = + # strinterp will set metpluslog_filename to '' if template is empty if config.has_option('config', 'LOG_FILENAME_TEMPLATE'): - metpluslog_filename = config.strinterp('config', '{LOG_FILENAME_TEMPLATE}', - LOG_TIMESTAMP_TEMPLATE=log_filenametimestamp) + metpluslog_filename = config.strinterp( + 'config', + '{LOG_FILENAME_TEMPLATE}', + LOG_TIMESTAMP_TEMPLATE=log_filenametimestamp + ) else: metpluslog_filename = '' if metpluslog_filename: @@ -374,15 +342,15 @@ def _set_logvars(config, logger=None): metpluslog = '' # Adding LOG_TIMESTAMP to the final configuration file. - logger.info('Adding: config.LOG_TIMESTAMP=%s' % repr(log_filenametimestamp)) + logger.info('Adding LOG_TIMESTAMP=%s' % repr(log_filenametimestamp)) config.set('config', 'LOG_TIMESTAMP', log_filenametimestamp) # Setting LOG_METPLUS in the configuration object # At this point LOG_METPLUS will have a value or '' the empty string. if user_defined_log_file: - logger.info('Replace [config] LOG_METPLUS with %s' % repr(metpluslog)) + logger.info('Replace LOG_METPLUS with %s' % repr(metpluslog)) else: - logger.info('Adding: config.LOG_METPLUS=%s' % repr(metpluslog)) + logger.info('Adding LOG_METPLUS=%s' % repr(metpluslog)) # expand LOG_METPLUS to ensure it is available config.set('config', 'LOG_METPLUS', metpluslog) @@ -501,25 +469,28 @@ def replace_config_from_section(config, section, required=True): return new_config class METplusConfig(ProdConfig): - """!A replacement for the produtil.config.ProdConfig used throughout - the METplus system. You should never need to instantiate one of - these --- the launch() and load() functions do that for you. This - class is the underlying implementation of most of the - functionality described in launch() and load()""" - - # items that are found in these sections will be moved into the [config] section - OLD_SECTIONS = ['dir', - 'exe', - 'filename_templates', - 'regex_pattern', - ] + """! Configuration class to store configuration values read from + METplus config files. + """ + + # items that are found in these sections + # will be moved into the [config] section + OLD_SECTIONS = ( + 'dir', + 'exe', + 'filename_templates', + 'regex_pattern', + ) def __init__(self, conf=None): """!Creates a new METplusConfig - @param conf The configuration file.""" + @param conf The configuration file + """ # set interpolation to None so you can supply filename template # that contain % to config.set - conf = ConfigParser(strict=False, inline_comment_prefixes=(';',), interpolation=None) if (conf is None) else conf + conf = ConfigParser(strict=False, + inline_comment_prefixes=(';',), + interpolation=None) if (conf is None) else conf super().__init__(conf) self._cycle = None self._logger = logging.getLogger('metplus') @@ -530,11 +501,11 @@ def __init__(self, conf=None): # get the OS environment and store it self.env = os.environ.copy() - # add user_env_vars section to hold environment variables defined by the user + # add section to hold environment variables defined by the user self.add_section('user_env_vars') def log(self, sublog=None): - """!Overrides method in ProdConfig + """! Overrides method in ProdConfig If the sublog argument is provided, then the logger will be under that subdomain of the "metplus" logging domain. Otherwise, this METplusConfig's logger @@ -546,7 +517,7 @@ def log(self, sublog=None): return logging.getLogger('metplus.'+sublog) return self._logger - def move_all_to_config_section(self): + def _move_all_to_config_section(self): """! Move all configuration variables that are found in the previously supported sections into the config section. """ @@ -588,6 +559,7 @@ def move_runtime_configs(self): ] more_run_confs = [item for item in self.keys(from_section) if item.startswith('LOG') or item.endswith('BASE')] + # create destination section if it does not exist if not self.has_section(to_section): self._conf.add_section(to_section) @@ -611,42 +583,19 @@ def remove_current_vars(self): if self.has_option('config', current_var): self._conf.remove_option('config', current_var) - def find_section(self, sec, opt): - """! Search through list of previously supported config sections - to find variable requested. This allows the removal of these - sections to consider all of the variables members of the - [config] section. - Args: - @param sec section requested - look in this section first - @param opt configuration variable to find - @returns section heading name or None if not found - """ - # first check the section requested - if self.has_option(sec, opt): - return sec - - # loop through previously supported sections to find variable opt - # return section name if found - for section in self.OLD_SECTIONS: - if self.has_option(section, opt): - return section - - # return None if variable is not found - return None - # override get methods to perform additional error checking def getraw(self, sec, opt, default='', count=0): """ parse parameter and replace any existing parameters referenced with the value (looking in same section, then config, dir, and os environment) returns raw string, preserving {valid?fmt=%Y} blocks - Args: - @param sec: Section in the conf file to look for variable - @param opt: Variable to interpret - @param default: Default value to use if config is not set - @param count: Counter used to stop recursion to prevent infinite - Returns: - Raw string or empty string if function calls itself too many times + + @param sec: Section in the conf file to look for variable + @param opt: Variable to interpret + @param default: Default value to use if config is not set + @param count: Counter used to stop recursion to prevent infinite + @returns Raw string or empty string if function calls itself too + many times """ if count >= 10: self.logger.error("Could not resolve getraw - check for circular " @@ -689,13 +638,14 @@ def getraw(self, sec, opt, default='', count=0): return in_template.replace('//', '/') def check_default(self, sec, name, default): - """!helper function for get methods, report error and raise NoOptionError if - default is not set. If default is set, set the config variable to the - default value so that the value is stored in the final conf - Args: - @param sec section of config - @param name name of config variable - @param default value to use - if set to None, error and raise exception + """! helper function for get methods, report error and raise + NoOptionError if default is not set. + If default is set, set the config variable to the + default value so that the value is stored in the final conf + + @param sec section of config + @param name name of config variable + @param default value to use - if set to None, error/raise exception """ if default is None: raise @@ -720,8 +670,9 @@ def check_default(self, sec, name, default): self.set(sec, name, default) def getexe(self, exe_name): - """!Wraps produtil exe with checks to see if option is set and if - exe actually exists. Returns None if not found instead of exiting""" + """! Wraps produtil exe with checks to see if option is set and if + exe actually exists. Returns None if not found instead of exiting + """ try: exe_path = super().getstr('config', exe_name) except NoOptionError as e: @@ -734,7 +685,7 @@ def getexe(self, exe_name): full_exe_path = shutil.which(exe_path) if full_exe_path is None: - msg = 'Executable {} does not exist at {}'.format(exe_name, exe_path) + msg = f'Executable {exe_name} does not exist at {exe_path}' if self.logger: self.logger.error(msg) else: @@ -745,8 +696,11 @@ def getexe(self, exe_name): self.set('config', exe_name, full_exe_path) return full_exe_path - def getdir(self, dir_name, default=None, morevars=None,taskvars=None, must_exist=False): - """!Wraps produtil getdir and reports an error if it is set to /path/to""" + def getdir(self, dir_name, default=None, morevars=None,taskvars=None, + must_exist=False): + """! Wraps produtil getdir and reports an error if + it is set to /path/to + """ try: dir_path = super().getstr('config', dir_name, default=None, morevars=morevars, taskvars=taskvars) @@ -755,7 +709,8 @@ def getdir(self, dir_name, default=None, morevars=None,taskvars=None, must_exist dir_path = default if '/path/to' in dir_path: - raise ValueError("[config] " + dir_name + " cannot be set to or contain '/path/to'") + raise ValueError(f"{dir_name} cannot be set to " + "or contain '/path/to'") if '\n' in dir_path: raise ValueError(f"Invalid value for [config] {dir_name} " @@ -769,24 +724,28 @@ def getdir(self, dir_name, default=None, morevars=None,taskvars=None, must_exist return dir_path.replace('//', '/') def getdir_nocheck(self, dir_name, default=None): - return super().getstr('config', dir_name, default=default).replace('//', '/') + return super().getstr('config', dir_name, + default=default).replace('//', '/') def getstr_nocheck(self, sec, name, default=None): - # if requested section is in the list of sections that are no longer used - # look in the [config] section for the variable + # if requested section is in the list of sections that are + # no longer used look in the [config] section for the variable if sec in self.OLD_SECTIONS: sec = 'config' return super().getstr(sec, name, default=default).replace('//', '/') - def getstr(self, sec, name, default=None, badtypeok=False, morevars=None, taskvars=None): - """!Wraps produtil getstr. Config variable is checked with a default value of None - because if the config is not set and a default is specified, it will just return - that value. We want to log that a default was used and set it in the config so - it will show up in the final conf that is generated at the end of execution. - If no default was specified in the call, the NoOptionError is raised again. - Replace double forward slash with single to prevent error that occurs if that - is found inside a MET config file (because it considers // the start of a comment + def getstr(self, sec, name, default=None, badtypeok=False, morevars=None, + taskvars=None): + """! Wraps produtil getstr. Config variable is checked with a default + value of None because if the config is not set and a default is + specified, it will just return that value. + We want to log that a default was used and set it in the config so + it will show up in the final conf that is generated at the end of + execution. If no default was specified in the call, + the NoOptionError is raised again. Replace double forward slash + with single to prevent error that occurs if that is found inside + a MET config file because it considers // the start of a comment """ if sec in self.OLD_SECTIONS: sec = 'config' @@ -800,20 +759,25 @@ def getstr(self, sec, name, default=None, badtypeok=False, morevars=None, taskva self.check_default(sec, name, default) return default.replace('//', '/') - def getbool(self, sec, name, default=None, badtypeok=False, morevars=None, taskvars=None): - """!Wraps produtil getbool. Config variable is checked with a default value of None - because if the config is not set and a default is specified, it will just return - that value. We want to log that a default was used and set it in the config so - it will show up in the final conf that is generated at the end of execution. - If no default was specified in the call, the NoOptionError is raised again. - @returns None if value is not a boolean (or yes/no), value if set, default if not set + def getbool(self, sec, name, default=None, badtypeok=False, morevars=None, + taskvars=None): + """! Wraps produtil getbool. Config variable is checked with a + default value of None because if the config is not set and a + default is specified, it will just return that value. + We want to log that a default was used and set it in the config so + it will show up in the final conf that is generated at the end of + execution. If no default was specified in the call, + the NoOptionError is raised again. + @returns None if value is not a boolean (or yes/no), value if set, + default if not set """ if sec in self.OLD_SECTIONS: sec = 'config' try: return super().getbool(sec, name, default=None, - badtypeok=badtypeok, morevars=morevars, taskvars=taskvars) + badtypeok=badtypeok, morevars=morevars, + taskvars=taskvars) except NoOptionError: # config item was not set self.check_default(sec, name, default) @@ -838,17 +802,21 @@ def getbool(self, sec, name, default=None, badtypeok=False, morevars=None, taskv self.logger.error(f"[{sec}] {name} must be an boolean.") return None - def getint(self, sec, name, default=None, badtypeok=False, morevars=None, taskvars=None): + def getint(self, sec, name, default=None, badtypeok=False, morevars=None, + taskvars=None): """!Wraps produtil getint to gracefully report if variable is not set and no default value is specified - @returns Value if set, default of missing value if not set, None if value is an incorrect type""" + @returns Value if set, default of missing value if not set, + None if value is an incorrect type""" if sec in self.OLD_SECTIONS: sec = 'config' try: - # call ProdConfig function with no default set so we can log and set the default + # call ProdConfig function with no default set so + # we can log and set the default return super().getint(sec, name, default=None, - badtypeok=badtypeok, morevars=morevars, taskvars=taskvars) + badtypeok=badtypeok, morevars=morevars, + taskvars=taskvars) # if config variable is not set except NoOptionError: @@ -860,7 +828,7 @@ def getint(self, sec, name, default=None, badtypeok=False, morevars=None, taskva # if invalid value except ValueError: - # check if it was an empty string and return MISSING_DATA_VALUE if so + # check if it was an empty string and return MISSING_DATA_VALUE if super().getstr(sec, name) == '': return util.MISSING_DATA_VALUE @@ -868,17 +836,21 @@ def getint(self, sec, name, default=None, badtypeok=False, morevars=None, taskva self.logger.error(f"[{sec}] {name} must be an integer.") return None - def getfloat(self, sec, name, default=None, badtypeok=False, morevars=None, taskvars=None): + def getfloat(self, sec, name, default=None, badtypeok=False, morevars=None, + taskvars=None): """!Wraps produtil getint to gracefully report if variable is not set and no default value is specified - @returns Value if set, default of missing value if not set, None if value is an incorrect type""" + @returns Value if set, default of missing value if not set, + None if value is an incorrect type""" if sec in self.OLD_SECTIONS: sec = 'config' try: - # call ProdConfig function with no default set so we can log and set the default + # call ProdConfig function with no default set so + # we can log and set the default return super().getfloat(sec, name, default=None, - badtypeok=badtypeok, morevars=morevars, taskvars=taskvars) + badtypeok=badtypeok, morevars=morevars, + taskvars=taskvars) # if config variable is not set except NoOptionError: @@ -890,7 +862,7 @@ def getfloat(self, sec, name, default=None, badtypeok=False, morevars=None, task # if invalid value except ValueError: - # check if it was an empty string and return MISSING_DATA_VALUE if so + # check if it was an empty string and return MISSING_DATA_VALUE if super().getstr(sec, name) == '': return util.MISSING_DATA_VALUE @@ -898,7 +870,8 @@ def getfloat(self, sec, name, default=None, badtypeok=False, morevars=None, task self.logger.error(f"[{sec}] {name} must be a float.") return None - def getseconds(self, sec, name, default=None, badtypeok=False, morevars=None, taskvars=None): + def getseconds(self, sec, name, default=None, badtypeok=False, + morevars=None, taskvars=None): """!Converts time values ending in H, M, or S to seconds""" if sec in self.OLD_SECTIONS: sec = 'config' @@ -907,7 +880,8 @@ def getseconds(self, sec, name, default=None, badtypeok=False, morevars=None, ta # convert value to seconds # Valid options match format 3600, 3600S, 60M, or 1H value = super().getstr(sec, name, default=None, - badtypeok=badtypeok, morevars=morevars, taskvars=taskvars) + badtypeok=badtypeok, morevars=morevars, + taskvars=taskvars) regex_and_multiplier = {r'(-*)(\d+)S': 1, r'(-*)(\d+)M': 60, r'(-*)(\d+)H': 3600, @@ -921,8 +895,8 @@ def getseconds(self, sec, name, default=None, badtypeok=False, morevars=None, ta return int(match.group(2)) * mult # if value is not in an expected format, error and exit - msg = '[{}] {} does not match expected format. '.format(sec, name) +\ - 'Valid options match 3600, 3600S, 60M, or 1H' + msg = (f'[{sec}] {name} does not match expected format. ' + 'Valid options match 3600, 3600S, 60M, or 1H') if self.logger: self.logger.error(msg) else: @@ -935,14 +909,38 @@ def getseconds(self, sec, name, default=None, badtypeok=False, morevars=None, ta self.check_default(sec, name, default) return default + def get_mp_config_name(self, mp_config): + """! Get first name of METplus config variable that is set. + + @param mp_config list of METplus config keys to check. Can also be a + single item + @returns Name of first METplus config name in list that is set in the + METplusConfig object. None if none keys in the list are set. + """ + if not isinstance(mp_config, list): + mp_configs = [mp_config] + else: + mp_configs = mp_config + + for mp_config_name in mp_configs: + if self.has_option('config', mp_config_name): + return mp_config_name + + return None + + class METplusLogFormatter(logging.Formatter): def __init__(self, config): self.default_fmt = config.getraw('config', 'LOG_LINE_FORMAT') - self.info_fmt = config.getraw('config', 'LOG_INFO_LINE_FORMAT', self.default_fmt) - self.debug_fmt = config.getraw('config', 'LOG_DEBUG_LINE_FORMAT', self.default_fmt) - self.error_fmt = config.getraw('config', 'LOG_ERR_LINE_FORMAT', self.default_fmt) + self.info_fmt = config.getraw('config', 'LOG_INFO_LINE_FORMAT', + self.default_fmt) + self.debug_fmt = config.getraw('config', 'LOG_DEBUG_LINE_FORMAT', + self.default_fmt) + self.error_fmt = config.getraw('config', 'LOG_ERR_LINE_FORMAT', + self.default_fmt) super().__init__(fmt=self.default_fmt, - datefmt=config.getraw('config', 'LOG_LINE_DATE_FORMAT'), + datefmt=config.getraw('config', + 'LOG_LINE_DATE_FORMAT'), style='%') def format(self, record): @@ -959,3 +957,1016 @@ def format(self, record): self._style._fmt = self.default_fmt return output + +def validate_configuration_variables(config, force_check=False): + + all_sed_cmds = [] + # check for deprecated config items and warn user to remove/replace them + deprecated_isOK, sed_cmds = check_for_deprecated_config(config) + all_sed_cmds.extend(sed_cmds) + + # check for deprecated env vars in MET config files and warn user to remove/replace them + deprecatedMET_isOK, sed_cmds = check_for_deprecated_met_config(config) + all_sed_cmds.extend(sed_cmds) + + # validate configuration variables + field_isOK, sed_cmds = validate_field_info_configs(config, force_check) + all_sed_cmds.extend(sed_cmds) + + # check that OUTPUT_BASE is not set to the exact same value as INPUT_BASE + inoutbase_isOK = True + input_real_path = os.path.realpath(config.getdir_nocheck('INPUT_BASE', '')) + output_real_path = os.path.realpath(config.getdir('OUTPUT_BASE')) + if input_real_path == output_real_path: + config.logger.error(f"INPUT_BASE AND OUTPUT_BASE are set to the exact same path: {input_real_path}") + config.logger.error("Please change one of these paths to avoid risk of losing input data") + inoutbase_isOK = False + + check_user_environment(config) + + return deprecated_isOK, field_isOK, inoutbase_isOK, deprecatedMET_isOK, all_sed_cmds + +def check_for_deprecated_config(config): + """!Checks user configuration files and reports errors or warnings if any deprecated variable + is found. If an alternate variable name can be suggested, add it to the 'alt' section + If the alternate cannot be literally substituted for the old name, set copy to False + Args: + @config : METplusConfig object to evaluate + Returns: + A tuple containing a boolean if the configuration is suitable to run or not and + if it is not correct, the 2nd item is a list of sed commands that can be run to help + fix the incorrect configuration variables + """ + + # key is the name of the depreacted variable that is no longer allowed in any config files + # value is a dictionary containing information about what to do with the deprecated config + # 'sec' is the section of the config file where the replacement resides, i.e. config, dir, + # filename_templates + # 'alt' is the alternative name for the deprecated config. this can be a single variable name or + # text to describe multiple variables or how to handle it. Set to None to tell the user to + # just remove the variable + # 'copy' is an optional item (defaults to True). set this to False if one cannot simply replace + # the deprecated config variable name with the value in 'alt' + # 'req' is an optional item (defaults to True). this to False to report a warning for the + # deprecated config and allow execution to continue. this is generally no longer used + # because we are requiring users to update the config files. if used, the developer must + # modify the code to handle both variables accordingly + deprecated_dict = { + 'LOOP_BY_INIT' : {'sec' : 'config', 'alt' : 'LOOP_BY', 'copy': False}, + 'LOOP_METHOD' : {'sec' : 'config', 'alt' : 'LOOP_ORDER'}, + 'PREPBUFR_DIR_REGEX' : {'sec' : 'regex_pattern', 'alt' : None}, + 'PREPBUFR_FILE_REGEX' : {'sec' : 'regex_pattern', 'alt' : None}, + 'OBS_INPUT_DIR_REGEX' : {'sec' : 'regex_pattern', 'alt' : 'OBS_POINT_STAT_INPUT_DIR', 'copy': False}, + 'FCST_INPUT_DIR_REGEX' : {'sec' : 'regex_pattern', 'alt' : 'FCST_POINT_STAT_INPUT_DIR', 'copy': False}, + 'FCST_INPUT_FILE_REGEX' : + {'sec' : 'regex_pattern', 'alt' : 'FCST_POINT_STAT_INPUT_TEMPLATE', 'copy': False}, + 'OBS_INPUT_FILE_REGEX' : {'sec' : 'regex_pattern', 'alt' : 'OBS_POINT_STAT_INPUT_TEMPLATE', 'copy': False}, + 'PREPBUFR_DATA_DIR' : {'sec' : 'dir', 'alt' : 'PB2NC_INPUT_DIR'}, + 'PREPBUFR_MODEL_DIR_NAME' : {'sec' : 'dir', 'alt' : 'PB2NC_INPUT_DIR', 'copy': False}, + 'OBS_INPUT_FILE_TMPL' : + {'sec' : 'filename_templates', 'alt' : 'OBS_POINT_STAT_INPUT_TEMPLATE'}, + 'FCST_INPUT_FILE_TMPL' : + {'sec' : 'filename_templates', 'alt' : 'FCST_POINT_STAT_INPUT_TEMPLATE'}, + 'NC_FILE_TMPL' : {'sec' : 'filename_templates', 'alt' : 'PB2NC_OUTPUT_TEMPLATE'}, + 'FCST_INPUT_DIR' : {'sec' : 'dir', 'alt' : 'FCST_POINT_STAT_INPUT_DIR'}, + 'OBS_INPUT_DIR' : {'sec' : 'dir', 'alt' : 'OBS_POINT_STAT_INPUT_DIR'}, + 'REGRID_TO_GRID' : {'sec' : 'config', 'alt' : 'POINT_STAT_REGRID_TO_GRID'}, + 'FCST_HR_START' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, + 'FCST_HR_END' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, + 'FCST_HR_INTERVAL' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, + 'START_DATE' : {'sec' : 'config', 'alt' : 'INIT_BEG or VALID_BEG', 'copy': False}, + 'END_DATE' : {'sec' : 'config', 'alt' : 'INIT_END or VALID_END', 'copy': False}, + 'INTERVAL_TIME' : {'sec' : 'config', 'alt' : 'INIT_INCREMENT or VALID_INCREMENT', 'copy': False}, + 'BEG_TIME' : {'sec' : 'config', 'alt' : 'INIT_BEG or VALID_BEG', 'copy': False}, + 'END_TIME' : {'sec' : 'config', 'alt' : 'INIT_END or VALID_END', 'copy': False}, + 'START_HOUR' : {'sec' : 'config', 'alt' : 'INIT_BEG or VALID_BEG', 'copy': False}, + 'END_HOUR' : {'sec' : 'config', 'alt' : 'INIT_END or VALID_END', 'copy': False}, + 'OBS_BUFR_VAR_LIST' : {'sec' : 'config', 'alt' : 'PB2NC_OBS_BUFR_VAR_LIST'}, + 'TIME_SUMMARY_FLAG' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_FLAG'}, + 'TIME_SUMMARY_BEG' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_BEG'}, + 'TIME_SUMMARY_END' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_END'}, + 'TIME_SUMMARY_VAR_NAMES' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_VAR_NAMES'}, + 'TIME_SUMMARY_TYPE' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_TYPE'}, + 'OVERWRITE_NC_OUTPUT' : {'sec' : 'config', 'alt' : 'PB2NC_SKIP_IF_OUTPUT_EXISTS', 'copy': False}, + 'VERTICAL_LOCATION' : {'sec' : 'config', 'alt' : 'PB2NC_VERTICAL_LOCATION'}, + 'VERIFICATION_GRID' : {'sec' : 'config', 'alt' : 'REGRID_DATA_PLANE_VERIF_GRID'}, + 'WINDOW_RANGE_BEG' : {'sec' : 'config', 'alt' : 'OBS_WINDOW_BEGIN'}, + 'WINDOW_RANGE_END' : {'sec' : 'config', 'alt' : 'OBS_WINDOW_END'}, + 'OBS_EXACT_VALID_TIME' : + {'sec' : 'config', 'alt' : 'OBS_WINDOW_BEGIN and OBS_WINDOW_END', 'copy': False}, + 'FCST_EXACT_VALID_TIME' : + {'sec' : 'config', 'alt' : 'FCST_WINDOW_BEGIN and FCST_WINDOW_END', 'copy': False}, + 'PCP_COMBINE_METHOD' : + {'sec' : 'config', 'alt' : 'FCST_PCP_COMBINE_METHOD and/or OBS_PCP_COMBINE_METHOD', 'copy': False}, + 'FHR_BEG' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, + 'FHR_END' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, + 'FHR_INC' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, + 'FHR_GROUP_BEG' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_[N]', 'copy': False}, + 'FHR_GROUP_END' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_[N]', 'copy': False}, + 'FHR_GROUP_LABELS' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_[N]_LABEL', 'copy': False}, + 'CYCLONE_OUT_DIR' : {'sec' : 'dir', 'alt' : 'CYCLONE_OUTPUT_DIR'}, + 'ENSEMBLE_STAT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'ENSEMBLE_STAT_OUTPUT_DIR'}, + 'EXTRACT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'EXTRACT_TILES_OUTPUT_DIR'}, + 'GRID_STAT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'GRID_STAT_OUTPUT_DIR'}, + 'MODE_OUT_DIR' : {'sec' : 'dir', 'alt' : 'MODE_OUTPUT_DIR'}, + 'MTD_OUT_DIR' : {'sec' : 'dir', 'alt' : 'MTD_OUTPUT_DIR'}, + 'SERIES_INIT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_OUTPUT_DIR'}, + 'SERIES_LEAD_OUT_DIR' : {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_OUTPUT_DIR'}, + 'SERIES_INIT_FILTERED_OUT_DIR' : + {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, + 'SERIES_LEAD_FILTERED_OUT_DIR' : + {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, + 'STAT_ANALYSIS_OUT_DIR' : + {'sec' : 'dir', 'alt' : 'STAT_ANALYSIS_OUTPUT_DIR'}, + 'TCMPR_PLOT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'TCMPR_PLOT_OUTPUT_DIR'}, + 'FCST_MIN_FORECAST' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_MIN'}, + 'FCST_MAX_FORECAST' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_MAX'}, + 'OBS_MIN_FORECAST' : {'sec' : 'config', 'alt' : 'OBS_PCP_COMBINE_MIN_LEAD'}, + 'OBS_MAX_FORECAST' : {'sec' : 'config', 'alt' : 'OBS_PCP_COMBINE_MAX_LEAD'}, + 'FCST_INIT_INTERVAL' : {'sec' : 'config', 'alt' : None}, + 'OBS_INIT_INTERVAL' : {'sec' : 'config', 'alt' : None}, + 'FCST_DATA_INTERVAL' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_DATA_INTERVAL'}, + 'OBS_DATA_INTERVAL' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_DATA_INTERVAL'}, + 'FCST_IS_DAILY_FILE' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_IS_DAILY_FILE'}, + 'OBS_IS_DAILY_FILE' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_IS_DAILY_FILE'}, + 'FCST_TIMES_PER_FILE' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_TIMES_PER_FILE'}, + 'OBS_TIMES_PER_FILE' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_TIMES_PER_FILE'}, + 'FCST_LEVEL' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_INPUT_ACCUMS', 'copy': False}, + 'OBS_LEVEL' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_INPUT_ACCUMS', 'copy': False}, + 'MODE_FCST_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'FCST_MODE_CONV_RADIUS'}, + 'MODE_FCST_CONV_THRESH' : {'sec' : 'config', 'alt' : 'FCST_MODE_CONV_THRESH'}, + 'MODE_FCST_MERGE_FLAG' : {'sec' : 'config', 'alt' : 'FCST_MODE_MERGE_FLAG'}, + 'MODE_FCST_MERGE_THRESH' : {'sec' : 'config', 'alt' : 'FCST_MODE_MERGE_THRESH'}, + 'MODE_OBS_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'OBS_MODE_CONV_RADIUS'}, + 'MODE_OBS_CONV_THRESH' : {'sec' : 'config', 'alt' : 'OBS_MODE_CONV_THRESH'}, + 'MODE_OBS_MERGE_FLAG' : {'sec' : 'config', 'alt' : 'OBS_MODE_MERGE_FLAG'}, + 'MODE_OBS_MERGE_THRESH' : {'sec' : 'config', 'alt' : 'OBS_MODE_MERGE_THRESH'}, + 'MTD_FCST_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'FCST_MTD_CONV_RADIUS'}, + 'MTD_FCST_CONV_THRESH' : {'sec' : 'config', 'alt' : 'FCST_MTD_CONV_THRESH'}, + 'MTD_OBS_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'OBS_MTD_CONV_RADIUS'}, + 'MTD_OBS_CONV_THRESH' : {'sec' : 'config', 'alt' : 'OBS_MTD_CONV_THRESH'}, + 'RM_EXE' : {'sec' : 'exe', 'alt' : 'RM'}, + 'CUT_EXE' : {'sec' : 'exe', 'alt' : 'CUT'}, + 'TR_EXE' : {'sec' : 'exe', 'alt' : 'TR'}, + 'NCAP2_EXE' : {'sec' : 'exe', 'alt' : 'NCAP2'}, + 'CONVERT_EXE' : {'sec' : 'exe', 'alt' : 'CONVERT'}, + 'NCDUMP_EXE' : {'sec' : 'exe', 'alt' : 'NCDUMP'}, + 'EGREP_EXE' : {'sec' : 'exe', 'alt' : 'EGREP'}, + 'ADECK_TRACK_DATA_DIR' : {'sec' : 'dir', 'alt' : 'TC_PAIRS_ADECK_INPUT_DIR'}, + 'BDECK_TRACK_DATA_DIR' : {'sec' : 'dir', 'alt' : 'TC_PAIRS_BDECK_INPUT_DIR'}, + 'MISSING_VAL_TO_REPLACE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_MISSING_VAL_TO_REPLACE'}, + 'MISSING_VAL' : {'sec' : 'config', 'alt' : 'TC_PAIRS_MISSING_VAL'}, + 'TRACK_DATA_SUBDIR_MOD' : {'sec' : 'dir', 'alt' : None}, + 'ADECK_FILE_PREFIX' : {'sec' : 'config', 'alt' : 'TC_PAIRS_ADECK_TEMPLATE', 'copy': False}, + 'BDECK_FILE_PREFIX' : {'sec' : 'config', 'alt' : 'TC_PAIRS_BDECK_TEMPLATE', 'copy': False}, + 'TOP_LEVEL_DIRS' : {'sec' : 'config', 'alt' : 'TC_PAIRS_READ_ALL_FILES'}, + 'TC_PAIRS_DIR' : {'sec' : 'dir', 'alt' : 'TC_PAIRS_OUTPUT_DIR'}, + 'CYCLONE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_CYCLONE'}, + 'STORM_ID' : {'sec' : 'config', 'alt' : 'TC_PAIRS_STORM_ID'}, + 'BASIN' : {'sec' : 'config', 'alt' : 'TC_PAIRS_BASIN'}, + 'STORM_NAME' : {'sec' : 'config', 'alt' : 'TC_PAIRS_STORM_NAME'}, + 'DLAND_FILE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_DLAND_FILE'}, + 'TRACK_TYPE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_REFORMAT_DECK'}, + 'FORECAST_TMPL' : {'sec' : 'filename_templates', 'alt' : 'TC_PAIRS_ADECK_TEMPLATE'}, + 'REFERENCE_TMPL' : {'sec' : 'filename_templates', 'alt' : 'TC_PAIRS_BDECK_TEMPLATE'}, + 'TRACK_DATA_MOD_FORCE_OVERWRITE' : + {'sec' : 'config', 'alt' : 'TC_PAIRS_SKIP_IF_REFORMAT_EXISTS', 'copy': False}, + 'TC_PAIRS_FORCE_OVERWRITE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_SKIP_IF_OUTPUT_EXISTS', 'copy': False}, + 'GRID_STAT_CONFIG' : {'sec' : 'config', 'alt' : 'GRID_STAT_CONFIG_FILE'}, + 'MODE_CONFIG' : {'sec' : 'config', 'alt': 'MODE_CONFIG_FILE'}, + 'FCST_PCP_COMBINE_INPUT_LEVEL': {'sec': 'config', 'alt' : 'FCST_PCP_COMBINE_INPUT_ACCUMS'}, + 'OBS_PCP_COMBINE_INPUT_LEVEL': {'sec': 'config', 'alt' : 'OBS_PCP_COMBINE_INPUT_ACCUMS'}, + 'TIME_METHOD': {'sec': 'config', 'alt': 'LOOP_BY', 'copy': False}, + 'MODEL_DATA_DIR': {'sec': 'dir', 'alt': 'EXTRACT_TILES_GRID_INPUT_DIR'}, + 'STAT_LIST': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_STAT_LIST'}, + 'NLAT': {'sec': 'config', 'alt': 'EXTRACT_TILES_NLAT'}, + 'NLON': {'sec': 'config', 'alt': 'EXTRACT_TILES_NLON'}, + 'DLAT': {'sec': 'config', 'alt': 'EXTRACT_TILES_DLAT'}, + 'DLON': {'sec': 'config', 'alt': 'EXTRACT_TILES_DLON'}, + 'LON_ADJ': {'sec': 'config', 'alt': 'EXTRACT_TILES_LON_ADJ'}, + 'LAT_ADJ': {'sec': 'config', 'alt': 'EXTRACT_TILES_LAT_ADJ'}, + 'OVERWRITE_TRACK': {'sec': 'config', 'alt': 'EXTRACT_TILES_OVERWRITE_TRACK'}, + 'BACKGROUND_MAP': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_BACKGROUND_MAP'}, + 'GFS_FCST_FILE_TMPL': {'sec': 'filename_templates', 'alt': 'FCST_EXTRACT_TILES_INPUT_TEMPLATE'}, + 'GFS_ANLY_FILE_TMPL': {'sec': 'filename_templates', 'alt': 'OBS_EXTRACT_TILES_INPUT_TEMPLATE'}, + 'SERIES_BY_LEAD_FILTERED_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, + 'SERIES_BY_INIT_FILTERED_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, + 'SERIES_BY_LEAD_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_OUTPUT_DIR'}, + 'SERIES_BY_INIT_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_OUTPUT_DIR'}, + 'SERIES_BY_LEAD_GROUP_FCSTS': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_GROUP_FCSTS'}, + 'SERIES_ANALYSIS_BY_LEAD_CONFIG_FILE': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_CONFIG_FILE'}, + 'SERIES_ANALYSIS_BY_INIT_CONFIG_FILE': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_CONFIG_FILE'}, + 'ENSEMBLE_STAT_MET_OBS_ERROR_TABLE': {'sec': 'config', 'alt': 'ENSEMBLE_STAT_MET_OBS_ERR_TABLE'}, + 'VAR_LIST': {'sec': 'config', 'alt': 'BOTH_VAR_NAME BOTH_VAR_LEVELS or SERIES_ANALYSIS_VAR_LIST', 'copy': False}, + 'SERIES_ANALYSIS_VAR_LIST': {'sec': 'config', 'alt': 'BOTH_VAR_NAME BOTH_VAR_LEVELS', 'copy': False}, + 'EXTRACT_TILES_VAR_LIST': {'sec': 'config', 'alt': ''}, + 'STAT_ANALYSIS_LOOKIN_DIR': {'sec': 'dir', 'alt': 'MODEL1_STAT_ANALYSIS_LOOKIN_DIR'}, + 'VALID_HOUR_METHOD': {'sec': 'config', 'alt': None}, + 'VALID_HOUR_BEG': {'sec': 'config', 'alt': None}, + 'VALID_HOUR_END': {'sec': 'config', 'alt': None}, + 'VALID_HOUR_INCREMENT': {'sec': 'config', 'alt': None}, + 'INIT_HOUR_METHOD': {'sec': 'config', 'alt': None}, + 'INIT_HOUR_BEG': {'sec': 'config', 'alt': None}, + 'INIT_HOUR_END': {'sec': 'config', 'alt': None}, + 'INIT_HOUR_INCREMENT': {'sec': 'config', 'alt': None}, + 'STAT_ANALYSIS_CONFIG': {'sec': 'config', 'alt': 'STAT_ANALYSIS_CONFIG_FILE'}, + 'JOB_NAME': {'sec': 'config', 'alt': 'STAT_ANALYSIS_JOB_NAME'}, + 'JOB_ARGS': {'sec': 'config', 'alt': 'STAT_ANALYSIS_JOB_ARGS'}, + 'FCST_LEAD': {'sec': 'config', 'alt': 'FCST_LEAD_LIST'}, + 'FCST_VAR_NAME': {'sec': 'config', 'alt': 'FCST_VAR_LIST'}, + 'FCST_VAR_LEVEL': {'sec': 'config', 'alt': 'FCST_VAR_LEVEL_LIST'}, + 'OBS_VAR_NAME': {'sec': 'config', 'alt': 'OBS_VAR_LIST'}, + 'OBS_VAR_LEVEL': {'sec': 'config', 'alt': 'OBS_VAR_LEVEL_LIST'}, + 'REGION': {'sec': 'config', 'alt': 'VX_MASK_LIST'}, + 'INTERP': {'sec': 'config', 'alt': 'INTERP_LIST'}, + 'INTERP_PTS': {'sec': 'config', 'alt': 'INTERP_PTS_LIST'}, + 'CONV_THRESH': {'sec': 'config', 'alt': 'CONV_THRESH_LIST'}, + 'FCST_THRESH': {'sec': 'config', 'alt': 'FCST_THRESH_LIST'}, + 'LINE_TYPE': {'sec': 'config', 'alt': 'LINE_TYPE_LIST'}, + 'STAT_ANALYSIS_DUMP_ROW_TMPL': {'sec': 'filename_templates', 'alt': 'STAT_ANALYSIS_DUMP_ROW_TEMPLATE'}, + 'STAT_ANALYSIS_OUT_STAT_TMPL': {'sec': 'filename_templates', 'alt': 'STAT_ANALYSIS_OUT_STAT_TEMPLATE'}, + 'PLOTTING_SCRIPTS_DIR': {'sec': 'dir', 'alt': 'MAKE_PLOTS_SCRIPTS_DIR'}, + 'STAT_FILES_INPUT_DIR': {'sec': 'dir', 'alt': 'MAKE_PLOTS_INPUT_DIR'}, + 'PLOTTING_OUTPUT_DIR': {'sec': 'dir', 'alt': 'MAKE_PLOTS_OUTPUT_DIR'}, + 'VERIF_CASE': {'sec': 'config', 'alt': 'MAKE_PLOTS_VERIF_CASE'}, + 'VERIF_TYPE': {'sec': 'config', 'alt': 'MAKE_PLOTS_VERIF_TYPE'}, + 'PLOT_TIME': {'sec': 'config', 'alt': 'DATE_TIME'}, + 'MODEL_NAME': {'sec': 'config', 'alt': 'MODEL'}, + 'MODEL_OBS_NAME': {'sec': 'config', 'alt': 'MODEL_OBTYPE'}, + 'MODEL_STAT_DIR': {'sec': 'dir', 'alt': 'MODEL_STAT_ANALYSIS_LOOKIN_DIR'}, + 'MODEL_NAME_ON_PLOT': {'sec': 'config', 'alt': 'MODEL_REFERENCE_NAME'}, + 'REGION_LIST': {'sec': 'config', 'alt': 'VX_MASK_LIST'}, + 'PLOT_STATS_LIST': {'sec': 'config', 'alt': 'MAKE_PLOT_STATS_LIST'}, + 'CI_METHOD': {'sec': 'config', 'alt': 'MAKE_PLOTS_CI_METHOD'}, + 'VERIF_GRID': {'sec': 'config', 'alt': 'MAKE_PLOTS_VERIF_GRID'}, + 'EVENT_EQUALIZATION': {'sec': 'config', 'alt': 'MAKE_PLOTS_EVENT_EQUALIZATION'}, + 'MTD_CONFIG': {'sec': 'config', 'alt': 'MTD_CONFIG_FILE'}, + 'CLIMO_GRID_STAT_INPUT_DIR': {'sec': 'dir', 'alt': 'GRID_STAT_CLIMO_MEAN_INPUT_DIR'}, + 'CLIMO_GRID_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', 'alt': 'GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE'}, + 'CLIMO_POINT_STAT_INPUT_DIR': {'sec': 'dir', 'alt': 'POINT_STAT_CLIMO_MEAN_INPUT_DIR'}, + 'CLIMO_POINT_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', 'alt': 'POINT_STAT_CLIMO_MEAN_INPUT_TEMPLATE'}, + 'GEMPAKTOCF_CLASSPATH': {'sec': 'exe', 'alt': 'GEMPAKTOCF_JAR', 'copy': False}, + 'CUSTOM_INGEST__OUTPUT_DIR': {'sec': 'dir', 'alt': 'PY_EMBED_INGEST__OUTPUT_DIR'}, + 'CUSTOM_INGEST__OUTPUT_TEMPLATE': {'sec': 'filename_templates', 'alt': 'PY_EMBED_INGEST__OUTPUT_TEMPLATE'}, + 'CUSTOM_INGEST__OUTPUT_GRID': {'sec': 'config', 'alt': 'PY_EMBED_INGEST__OUTPUT_GRID'}, + 'CUSTOM_INGEST__SCRIPT': {'sec': 'config', 'alt': 'PY_EMBED_INGEST__SCRIPT'}, + 'CUSTOM_INGEST__TYPE': {'sec': 'config', 'alt': 'PY_EMBED_INGEST__TYPE'}, + 'TC_STAT_RUN_VIA': {'sec': 'config', 'alt': 'TC_STAT_CONFIG_FILE', + 'copy': False}, + 'TC_STAT_CMD_LINE_JOB': {'sec': 'config', 'alt': 'TC_STAT_JOB_ARGS'}, + 'TC_STAT_JOBS_LIST': {'sec': 'config', 'alt': 'TC_STAT_JOB_ARGS'}, + 'EXTRACT_TILES_OVERWRITE_TRACK': {'sec': 'config', + 'alt': 'EXTRACT_TILES_SKIP_IF_OUTPUT_EXISTS', + 'copy': False}, + 'EXTRACT_TILES_PAIRS_INPUT_DIR': {'sec': 'dir', + 'alt': 'EXTRACT_TILES_STAT_INPUT_DIR', + 'copy': False}, + 'EXTRACT_TILES_FILTERED_OUTPUT_TEMPLATE': {'sec': 'filename_template', + 'alt': 'EXTRACT_TILES_STAT_INPUT_TEMPLATE',}, + 'EXTRACT_TILES_GRID_INPUT_DIR': {'sec': 'dir', + 'alt': 'FCST_EXTRACT_TILES_INPUT_DIR' + 'and ' + 'OBS_EXTRACT_TILES_INPUT_DIR', + 'copy': False}, + 'SERIES_ANALYSIS_FILTER_OPTS': {'sec': 'config', + 'alt': 'TC_STAT_JOB_ARGS', + 'copy': False}, + 'SERIES_ANALYSIS_INPUT_DIR': {'sec': 'dir', + 'alt': 'FCST_SERIES_ANALYSIS_INPUT_DIR ' + 'and ' + 'OBS_SERIES_ANALYSIS_INPUT_DIR'}, + 'FCST_SERIES_ANALYSIS_TILE_INPUT_TEMPLATE': {'sec': 'filename_templates', + 'alt': 'FCST_SERIES_ANALYSIS_INPUT_TEMPLATE '}, + 'OBS_SERIES_ANALYSIS_TILE_INPUT_TEMPLATE': {'sec': 'filename_templates', + 'alt': 'OBS_SERIES_ANALYSIS_INPUT_TEMPLATE '}, + 'EXTRACT_TILES_STAT_INPUT_DIR': {'sec': 'dir', + 'alt': 'EXTRACT_TILES_TC_STAT_INPUT_DIR',}, + 'EXTRACT_TILES_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', + 'alt': 'EXTRACT_TILES_TC_STAT_INPUT_TEMPLATE',}, + 'SERIES_ANALYSIS_STAT_INPUT_DIR': {'sec': 'dir', + 'alt': 'SERIES_ANALYSIS_TC_STAT_INPUT_DIR', }, + 'SERIES_ANALYSIS_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', + 'alt': 'SERIES_ANALYSIS_TC_STAT_INPUT_TEMPLATE', }, + } + + # template '' : {'sec' : '', 'alt' : '', 'copy': True}, + + logger = config.logger + + # create list of errors and warnings to report for deprecated configs + e_list = [] + w_list = [] + all_sed_cmds = [] + + for old, depr_info in deprecated_dict.items(): + if isinstance(depr_info, dict): + + # check if is found in the old item, use regex to find variables if found + if '' in old: + old_regex = old.replace('', r'(\d+)') + indices = find_indices_in_config_section(old_regex, + config, + index_index=1).keys() + for index in indices: + old_with_index = old.replace('', index) + if depr_info['alt']: + alt_with_index = depr_info['alt'].replace('', index) + else: + alt_with_index = '' + + handle_deprecated(old_with_index, alt_with_index, depr_info, + config, all_sed_cmds, w_list, e_list) + else: + handle_deprecated(old, depr_info['alt'], depr_info, + config, all_sed_cmds, w_list, e_list) + + + # check all templates and error if any deprecated tags are used + # value of dict is replacement tag, set to None if no replacement exists + # deprecated tags: region (replace with basin) + deprecated_tags = {'region' : 'basin'} + template_vars = config.keys('config') + template_vars = [tvar for tvar in template_vars if tvar.endswith('_TEMPLATE')] + for temp_var in template_vars: + template = config.getraw('filename_templates', temp_var) + tags = get_tags(template) + + for depr_tag, replace_tag in deprecated_tags.items(): + if depr_tag in tags: + e_msg = 'Deprecated tag {{{}}} found in {}.'.format(depr_tag, + temp_var) + if replace_tag is not None: + e_msg += ' Replace with {{{}}}'.format(replace_tag) + + e_list.append(e_msg) + + # if any warning exist, report them + if w_list: + for warning_msg in w_list: + logger.warning(warning_msg) + + # if any errors exist, report them and exit + if e_list: + logger.error('DEPRECATED CONFIG ITEMS WERE FOUND. ' +\ + 'PLEASE REMOVE/REPLACE THEM FROM CONFIG FILES') + for error_msg in e_list: + logger.error(error_msg) + return False, all_sed_cmds + + return True, [] + +def check_for_deprecated_met_config(config): + sed_cmds = [] + all_good = True + + # set CURRENT_* METplus variables in case they are referenced in a + # METplus config variable and not already set + for fcst_or_obs in ['FCST', 'OBS']: + for name_or_level in ['NAME', 'LEVEL']: + current_var = f'CURRENT_{fcst_or_obs}_{name_or_level}' + if not config.has_option('config', current_var): + config.set('config', current_var, '') + + # check if *_CONFIG_FILE if set in the METplus config file and check for + # deprecated environment variables in those files + met_config_keys = [key for key in config.keys('config') + if key.endswith('CONFIG_FILE')] + + for met_config_key in met_config_keys: + met_tool = met_config_key.replace('_CONFIG_FILE', '') + + # get custom loop list to check if multiple config files are used based on the custom string + custom_list = get_custom_string_list(config, met_tool) + + for custom_string in custom_list: + met_config = config.getraw('config', met_config_key) + if not met_config: + continue + + met_config_file = do_string_sub(met_config, custom=custom_string) + + if not check_for_deprecated_met_config_file(config, met_config_file, sed_cmds, met_tool): + all_good = False + + return all_good, sed_cmds + +def check_for_deprecated_met_config_file(config, met_config, sed_cmds, met_tool): + + all_good = True + if not os.path.exists(met_config): + config.logger.error(f"Config file does not exist: {met_config}") + return False + + deprecated_met_list = ['MET_VALID_HHMM', 'GRID_VX', 'CONFIG_DIR'] + deprecated_output_prefix_list = ['FCST_VAR', 'OBS_VAR'] + config.logger.debug(f"Checking for deprecated environment variables in: {met_config}") + + with open(met_config, 'r') as file_handle: + lines = file_handle.read().splitlines() + + for line in lines: + for deprecated_item in deprecated_met_list: + if '${' + deprecated_item + '}' in line: + all_good = False + config.logger.error("Please remove deprecated environment variable " + f"${{{deprecated_item}}} found in MET config file: " + f"{met_config}") + + if deprecated_item == 'MET_VALID_HHMM' and 'file_name' in line: + config.logger.error(f"Set {met_tool}_CLIMO_MEAN_INPUT_[DIR/TEMPLATE] in a " + "METplus config file to set CLIMO_MEAN_FILE in a MET config") + new_line = " file_name = [ ${CLIMO_MEAN_FILE} ];" + + # escape [ and ] because they are special characters in sed commands + old_line = line.rstrip().replace('[', r'\[').replace(']', r'\]') + + sed_cmds.append(f"sed -i 's|^{old_line}|{new_line}|g' {met_config}") + add_line = f"{met_tool}_CLIMO_MEAN_INPUT_TEMPLATE" + sed_cmds.append(f"#Add {add_line}") + break + + if 'to_grid' in line: + config.logger.error("MET to_grid variable should reference " + "${REGRID_TO_GRID} environment variable") + new_line = " to_grid = ${REGRID_TO_GRID};" + + # escape [ and ] because they are special characters in sed commands + old_line = line.rstrip().replace('[', r'\[').replace(']', r'\]') + + sed_cmds.append(f"sed -i 's|^{old_line}|{new_line}|g' {met_config}") + config.logger.info(f"Be sure to set {met_tool}_REGRID_TO_GRID to the correct value.") + add_line = f"{met_tool}_REGRID_TO_GRID" + sed_cmds.append(f"#Add {add_line}") + break + + + for deprecated_item in deprecated_output_prefix_list: + # if deprecated item found in output prefix or to_grid line, replace line to use + # env var OUTPUT_PREFIX or REGRID_TO_GRID + if '${' + deprecated_item + '}' in line and 'output_prefix' in line: + config.logger.error("output_prefix variable should reference " + "${OUTPUT_PREFIX} environment variable") + new_line = "output_prefix = \"${OUTPUT_PREFIX}\";" + + # escape [ and ] because they are special characters in sed commands + old_line = line.rstrip().replace('[', r'\[').replace(']', r'\]') + + sed_cmds.append(f"sed -i 's|^{old_line}|{new_line}|g' {met_config}") + config.logger.info(f"You will need to add {met_tool}_OUTPUT_PREFIX to the METplus config file" + f" that sets {met_tool}_CONFIG_FILE. Set it to:") + output_prefix = _replace_output_prefix(line) + add_line = f"{met_tool}_OUTPUT_PREFIX = {output_prefix}" + config.logger.info(add_line) + sed_cmds.append(f"#Add {add_line}") + all_good = False + break + + return all_good + +def validate_field_info_configs(config, force_check=False): + """!Verify that config variables with _VAR_ in them are valid. Returns True if all are valid. + Returns False if any items are invalid""" + + variable_extensions = ['NAME', 'LEVELS', 'THRESH', 'OPTIONS'] + all_good = True, [] + + if skip_field_info_validation(config) and not force_check: + return True, [] + + # keep track of all sed commands to replace config variable names + all_sed_cmds = [] + + for ext in variable_extensions: + # find all _VAR_ keys in the conf files + data_types_and_indices = find_indices_in_config_section(r"(\w+)_VAR(\d+)_"+ext, + config, + index_index=2, + id_index=1) + + # if BOTH_VAR_ is used, set FCST and OBS to the same value + # if FCST or OBS is used, the other must be present as well + # if BOTH and either FCST or OBS are set, report an error + # get other data type + for index, data_type_list in data_types_and_indices.items(): + + is_valid, err_msgs, sed_cmds = is_var_item_valid(data_type_list, index, ext, config) + if not is_valid: + for err_msg in err_msgs: + config.logger.error(err_msg) + all_sed_cmds.extend(sed_cmds) + all_good = False + + # make sure FCST and OBS have the same number of levels if coming from separate variables + elif ext == 'LEVELS' and all(item in ['FCST', 'OBS'] for item in data_type_list): + fcst_levels = getlist(config.getraw('config', f"FCST_VAR{index}_LEVELS", '')) + + # add empty string if no levels are found because python embedding items do not need + # to include a level, but the other item may have a level and the numbers need to match + if not fcst_levels: + fcst_levels.append('') + + obs_levels = getlist(config.getraw('config', f"OBS_VAR{index}_LEVELS", '')) + if not obs_levels: + obs_levels.append('') + + if len(fcst_levels) != len(obs_levels): + config.logger.error(f"FCST_VAR{index}_LEVELS and OBS_VAR{index}_LEVELS do not have " + "the same number of elements") + all_good = False + + return all_good, all_sed_cmds + +def check_user_environment(config): + """!Check if any environment variables set in [user_env_vars] are already set in + the user's environment. Warn them that it will be overwritten from the conf if it is""" + if not config.has_section('user_env_vars'): + return + + for env_var in config.keys('user_env_vars'): + if env_var in os.environ: + msg = '{} is already set in the environment. '.format(env_var) +\ + 'Overwriting from conf file' + config.logger.warning(msg) + +def find_indices_in_config_section(regex, config, sec='config', + index_index=1, id_index=None): + """! Use regular expression to get all config variables that match and + are set in the user's configuration. This is used to handle config + variables that have multiple indices, i.e. FCST_VAR1_NAME, FCST_VAR2_NAME, + etc. + + @param regex regular expression to use to find variables + @param config METplusConfig object to search + @param sec (optional) config file section to search. Defaults to config + @param index_index 1 based number that is the regex match index for the + index number (default is 1) + @param id_index 1 based number that is the regex match index for the + identifier. Defaults to None which does not extract an indentifier + + number and the first match is used as an identifier + @returns dictionary where keys are the index number and the value is a + list of identifiers (if noID=True) or a list containing None + """ + # regex expression must have 2 () items and the 2nd item must be the index + all_conf = config.keys(sec) + indices = {} + regex = re.compile(regex) + for conf in all_conf: + result = regex.match(conf) + if result is not None: + index = result.group(index_index) + if id_index: + identifier = result.group(id_index) + else: + identifier = None + + if index not in indices: + indices[index] = [identifier] + else: + indices[index].append(identifier) + + return indices + +def handle_deprecated(old, alt, depr_info, config, all_sed_cmds, w_list, e_list): + sec = depr_info['sec'] + config_files = config.getstr('config', 'CONFIG_INPUT', '').split(',') + # if deprecated config item is found + if config.has_option(sec, old): + # if it is not required to remove, add to warning list + if 'req' in depr_info.keys() and depr_info['req'] is False: + msg = '[{}] {} is deprecated and will be '.format(sec, old) + \ + 'removed in a future version of METplus' + if alt: + msg += ". Please replace with {}".format(alt) + w_list.append(msg) + # if it is required to remove, add to error list + else: + if not alt: + e_list.append("[{}] {} should be removed".format(sec, old)) + else: + e_list.append("[{}] {} should be replaced with {}".format(sec, old, alt)) + + if 'copy' not in depr_info.keys() or depr_info['copy']: + for config_file in config_files: + all_sed_cmds.append(f"sed -i 's|^{old}|{alt}|g' {config_file}") + all_sed_cmds.append(f"sed -i 's|{{{old}}}|{{{alt}}}|g' {config_file}") + +def get_custom_string_list(config, met_tool): + var_name = 'CUSTOM_LOOP_LIST' + custom_loop_list = config.getstr_nocheck('config', + f'{met_tool.upper()}_{var_name}', + config.getstr_nocheck('config', + var_name, + '')) + custom_loop_list = getlist(custom_loop_list) + if not custom_loop_list: + custom_loop_list.append('') + + return custom_loop_list + +def _replace_output_prefix(line): + op_replacements = {'${MODEL}': '{MODEL}', + '${FCST_VAR}': '{CURRENT_FCST_NAME}', + '${OBTYPE}': '{OBTYPE}', + '${OBS_VAR}': '{CURRENT_OBS_NAME}', + '${LEVEL}': '{CURRENT_FCST_LEVEL}', + '${FCST_TIME}': '{lead?fmt=%3H}', + } + prefix = line.split('=')[1].strip().rstrip(';').strip('"') + for key, value, in op_replacements.items(): + prefix = prefix.replace(key, value) + + return prefix + +def parse_var_list(config, time_info=None, data_type=None, met_tool=None, + levels_as_list=False): + """ read conf items and populate list of dictionaries containing + information about each variable to be compared + + @param config: METplusConfig object + @param time_info: time object for string sub, optional + @param data_type: data type to find. Can be FCST, OBS, or ENS. + If not set, get FCST/OBS/BOTH + @param met_tool: optional name of MET tool to look for wrapper + specific var items + @param levels_as_list If true, store levels and output names as + a list instead of creating a field info dict for each name/level + @returns list of dictionaries with variable information + """ + + # validate configs again in case wrapper is not running from run_metplus + # this does not need to be done if parsing a specific data type, + # i.e. ENS or FCST + if data_type is None: + if not validate_field_info_configs(config)[0]: + return [] + elif data_type == 'BOTH': + config.logger.error("Cannot request BOTH explicitly in parse_var_list") + return [] + + # var_list is a list containing an list of dictionaries + var_list = [] + + # if specific data type is requested, only get that type + if data_type: + data_types = [data_type] + # otherwise get both FCST and OBS + else: + data_types = ['FCST', 'OBS'] + + # get indices of VAR items for data type and/or met tool + indices = [] + if met_tool: + indices = find_var_name_indices(config, data_types, met_tool).keys() + if not indices: + indices = find_var_name_indices(config, data_types).keys() + + # get config name prefixes for each data type to find + dt_search_prefixes = {} + for current_type in data_types: + # get list of variable prefixes to search + prefixes = get_field_search_prefixes(current_type, met_tool) + dt_search_prefixes[current_type] = prefixes + + # loop over all possible variables and add them to list + for index in indices: + field_info_list = [] + for current_type in data_types: + # get dictionary of existing config variables to use + search_prefixes = dt_search_prefixes[current_type] + field_configs = get_field_config_variables(config, + index, + search_prefixes) + + field_info = format_var_items(field_configs, time_info) + if not isinstance(field_info, dict): + config.logger.error(f'Could not process {current_type}_' + f'VAR{index} variables: {field_info}') + continue + + field_info['data_type'] = current_type.lower() + field_info_list.append(field_info) + + # check that all fields types were found + if not field_info_list or len(data_types) != len(field_info_list): + continue + + # check if number of levels for each field type matches + n_levels = len(field_info_list[0]['levels']) + if len(data_types) > 1: + if (n_levels != len(field_info_list[1]['levels'])): + continue + + # if requested, put all field levels in a single item + if levels_as_list: + var_dict = {} + for field_info in field_info_list: + current_type = field_info.get('data_type') + var_dict[f"{current_type}_name"] = field_info.get('name') + var_dict[f"{current_type}_level"] = field_info.get('levels') + var_dict[f"{current_type}_thresh"] = field_info.get('thresh') + var_dict[f"{current_type}_extra"] = field_info.get('extra') + var_dict[f"{current_type}_output_name"] = field_info.get('output_names') + + var_dict['index'] = index + var_list.append(var_dict) + continue + + # loop over levels and add all values to output dictionary + for level_index in range(n_levels): + var_dict = {} + + # get level values to use for string substitution in name + # used for python embedding calls that read the level value + sub_info = {} + for field_info in field_info_list: + dt_level = f"{field_info.get('data_type')}_level" + sub_info[dt_level] = field_info.get('levels')[level_index] + + for field_info in field_info_list: + current_type = field_info.get('data_type') + name = field_info.get('name') + level = field_info.get('levels')[level_index] + thresh = field_info.get('thresh') + extra = field_info.get('extra') + output_name = field_info.get('output_names')[level_index] + + # substitute level in name if filename template is specified + subbed_name = do_string_sub(name, + skip_missing_tags=True, + **sub_info) + + var_dict[f"{current_type}_name"] = subbed_name + var_dict[f"{current_type}_level"] = level + var_dict[f"{current_type}_thresh"] = thresh + var_dict[f"{current_type}_extra"] = extra + var_dict[f"{current_type}_output_name"] = output_name + + var_dict['index'] = index + var_list.append(var_dict) + + # extra debugging information used for developer debugging only + ''' + for v in var_list: + config.logger.debug(f"VAR{v['index']}:") + if 'fcst_name' in v.keys(): + config.logger.debug(" fcst_name:"+v['fcst_name']) + config.logger.debug(" fcst_level:"+v['fcst_level']) + if 'fcst_thresh' in v.keys(): + config.logger.debug(" fcst_thresh:"+str(v['fcst_thresh'])) + if 'fcst_extra' in v.keys(): + config.logger.debug(" fcst_extra:"+v['fcst_extra']) + if 'fcst_output_name' in v.keys(): + config.logger.debug(" fcst_output_name:"+v['fcst_output_name']) + if 'obs_name' in v.keys(): + config.logger.debug(" obs_name:"+v['obs_name']) + config.logger.debug(" obs_level:"+v['obs_level']) + if 'obs_thresh' in v.keys(): + config.logger.debug(" obs_thresh:"+str(v['obs_thresh'])) + if 'obs_extra' in v.keys(): + config.logger.debug(" obs_extra:"+v['obs_extra']) + if 'obs_output_name' in v.keys(): + config.logger.debug(" obs_output_name:"+v['obs_output_name']) + if 'ens_name' in v.keys(): + config.logger.debug(" ens_name:"+v['ens_name']) + config.logger.debug(" ens_level:"+v['ens_level']) + if 'ens_thresh' in v.keys(): + config.logger.debug(" ens_thresh:"+str(v['ens_thresh'])) + if 'ens_extra' in v.keys(): + config.logger.debug(" ens_extra:"+v['ens_extra']) + if 'ens_output_name' in v.keys(): + config.logger.debug(" ens_output_name:"+v['ens_output_name']) + ''' + return sorted(var_list, key=lambda x: x['index']) + +def find_var_name_indices(config, data_types, met_tool=None): + data_type_regex = f"{'|'.join(data_types)}" + + # if data_types includes FCST or OBS, also search for BOTH + if any([item for item in ['FCST', 'OBS'] if item in data_types]): + data_type_regex += '|BOTH' + + regex_string = f"({data_type_regex})" + + # if MET tool is specified, get tool specific items + if met_tool: + regex_string += f"_{met_tool.upper()}" + + regex_string += r"_VAR(\d+)_(NAME|INPUT_FIELD_NAME|FIELD_NAME)" + + # find all _VAR_NAME keys in the conf files + return find_indices_in_config_section(regex_string, + config, + index_index=2, + id_index=1) + +def skip_field_info_validation(config): + """!Check config to see if having corresponding FCST/OBS variables is necessary. If process list only + contains reformatter wrappers, don't validate field info. Also, if MTD is in the process list and + it is configured to only process either FCST or OBS, validation is unnecessary.""" + + reformatters = ['PCPCombine', 'RegridDataPlane'] + process_list = [item[0] for item in get_process_list(config)] + + # if running MTD in single mode, you don't need matching FCST/OBS + if 'MTD' in process_list and config.getbool('config', 'MTD_SINGLE_RUN'): + return True + + # if running any app other than the reformatters, you need matching FCST/OBS, so don't skip + if [item for item in process_list if item not in reformatters]: + return False + + return True + +def get_process_list(config): + """!Read process list, Extract instance string if specified inside + parenthesis. Remove dashes/underscores and change to lower case, + then map the name to the correct wrapper name + + @param config METplusConfig object to read PROCESS_LIST value + @returns list of tuple containing process name and instance identifier + (None if no instance was set) + """ + # get list of processes + process_list = getlist(config.getstr('config', 'PROCESS_LIST')) + + out_process_list = [] + # for each item remove dashes, underscores, and cast to lower-case + for process in process_list: + # if instance is specified, extract the text inside parenthesis + match = re.match(r'(.*)\((.*)\)', process) + if match: + instance = match.group(2) + process_name = match.group(1) + else: + instance = None + process_name = process + + wrapper_name = get_wrapper_name(process_name) + if wrapper_name is None: + config.logger.warning(f"PROCESS_LIST item {process_name} " + "may be invalid.") + wrapper_name = process_name + + # if MakePlots is in process list, remove it because + # it will be called directly from StatAnalysis + if wrapper_name == 'MakePlots': + continue + + out_process_list.append((wrapper_name, instance)) + + return out_process_list + +def get_field_search_prefixes(data_type, met_tool=None): + """! Get list of prefixes to search for field variables. + + @param data_type type of field to search for, i.e. FCST, OBS, ENS, etc. + Check for BOTH_ variables first only if data type is FCST or OBS + @param met_tool name of tool to search for variable or None if looking + for generic field info + @returns list of prefixes to search, i.e. [BOTH_, FCST_] or + [ENS_] or [BOTH_GRID_STAT_, OBS_GRID_STAT_] + """ + search_prefixes = [] + var_strings = [] + + # if met tool name is set, prioritize + # wrapper-specific configs before generic configs + if met_tool: + var_strings.append(f'{met_tool.upper()}_') + + var_strings.append('') + + for var_string in var_strings: + search_prefixes.append(f"{data_type}_{var_string}") + + # if looking for FCST or OBS, also check for BOTH prefix + if data_type in ['FCST', 'OBS']: + search_prefixes.append(f"BOTH_{var_string}") + + return search_prefixes + +def is_var_item_valid(item_list, index, ext, config): + """!Given a list of data types (FCST, OBS, ENS, or BOTH) check if the + combination is valid. + If BOTH is found, FCST and OBS should not be found. + If FCST or OBS is found, the other must also be found. + @param item_list list of data types that were found for a given index + @param index number following _VAR in the variable name + @param ext extension to check, i.e. NAME, LEVELS, THRESH, or OPTIONS + @param config METplusConfig instance + @returns tuple containing boolean if var item is valid, list of error + messages and list of sed commands to help the user update their old + configuration files + """ + + full_ext = f"_VAR{index}_{ext}" + msg = [] + sed_cmds = [] + if 'BOTH' in item_list and ('FCST' in item_list or 'OBS' in item_list): + + msg.append(f"Cannot set FCST{full_ext} or OBS{full_ext} if BOTH{full_ext} is set.") + elif ext == 'THRESH': + # allow thresholds unless BOTH and (FCST or OBS) are set + pass + + elif 'FCST' in item_list and 'OBS' not in item_list: + # if FCST level has 1 item and OBS name is a python embedding script, + # don't report error + level_list = getlist(config.getraw('config', + f'FCST_VAR{index}_LEVELS', + '')) + other_name = config.getraw('config', f'OBS_VAR{index}_NAME', '') + skip_error_for_py_embed = ext == 'LEVELS' and is_python_script(other_name) and len(level_list) == 1 + # do not report error for OPTIONS since it isn't required to be the same length + if ext not in ['OPTIONS'] and not skip_error_for_py_embed: + msg.append(f"If FCST{full_ext} is set, you must either set OBS{full_ext} or " + f"change FCST{full_ext} to BOTH{full_ext}") + + config_files = config.getstr('config', 'CONFIG_INPUT', '').split(',') + for config_file in config_files: + sed_cmds.append(f"sed -i 's|^FCST{full_ext}|BOTH{full_ext}|g' {config_file}") + sed_cmds.append(f"sed -i 's|{{FCST{full_ext}}}|{{BOTH{full_ext}}}|g' {config_file}") + + elif 'OBS' in item_list and 'FCST' not in item_list: + # if OBS level has 1 item and FCST name is a python embedding script, + # don't report error + level_list = getlist(config.getraw('config', + f'OBS_VAR{index}_LEVELS', + '')) + other_name = config.getraw('config', f'FCST_VAR{index}_NAME', '') + skip_error_for_py_embed = ext == 'LEVELS' and is_python_script(other_name) and len(level_list) == 1 + + if ext not in ['OPTIONS'] and not skip_error_for_py_embed: + msg.append(f"If OBS{full_ext} is set, you must either set FCST{full_ext} or " + f"change OBS{full_ext} to BOTH{full_ext}") + + config_files = config.getstr('config', 'CONFIG_INPUT', '').split(',') + for config_file in config_files: + sed_cmds.append(f"sed -i 's|^OBS{full_ext}|BOTH{full_ext}|g' {config_file}") + sed_cmds.append(f"sed -i 's|{{OBS{full_ext}}}|{{BOTH{full_ext}}}|g' {config_file}") + + return not bool(msg), msg, sed_cmds + +def get_field_config_variables(config, index, search_prefixes): + """! Search for variables that are set in the config that correspond to + the fields requested. Some field info items have + synonyms that can be used if the typical name is not set. This is used + in RegridDataPlane wrapper. + + @param config METplusConfig object to search + @param index of field (VAR) to find + @param search_prefixes list of valid prefixes to search for variables + in the config, i.e. FCST_VAR1_ or OBS_GRID_STAT_VAR2_ + @returns dictionary containing a config variable name to be used for + each field info value. If a valid config variable was not set for a + field info value, the value for that key will be set to None. + """ + # list of field info variables to find from config + # used as keys for dictionaries + field_info_items = ['name', + 'levels', + 'thresh', + 'options', + 'output_names', + ] + + field_configs = {} + search_suffixes = {} + + # initialize field configs dictionary values to None + # initialize dictionary of valid suffixes to search for with + # the capitalized version of field info name + for field_info_item in field_info_items: + field_configs[field_info_item] = None + search_suffixes[field_info_item] = [field_info_item.upper()] + + # add alternate suffixes for config variable names to attempt + search_suffixes['name'].append('INPUT_FIELD_NAME') + search_suffixes['name'].append('FIELD_NAME') + search_suffixes['levels'].append('INPUT_LEVEL') + search_suffixes['levels'].append('FIELD_LEVEL') + search_suffixes['output_names'].append('OUTPUT_FIELD_NAME') + search_suffixes['output_names'].append('FIELD_NAME') + + # look through field config keys and obtain highest priority + # variable name for each field config + for search_var, suffixes in search_suffixes.items(): + for prefix in search_prefixes: + + found = False + for suffix in suffixes: + var_name = f"{prefix}VAR{index}_{suffix}" + # if variable is found in config, + # get the value and break out of suffix loop + if config.has_option('config', var_name): + field_configs[search_var] = config.getraw('config', + var_name) + found = True + break + + # if config variable was found, break out of prefix loop + if found: + break + + return field_configs diff --git a/metplus/util/constants.py b/metplus/util/constants.py new file mode 100644 index 0000000000..02cb3e22e5 --- /dev/null +++ b/metplus/util/constants.py @@ -0,0 +1,2 @@ +COMPRESSION_EXTENSIONS = ['.gz', '.bz2', '.zip'] + diff --git a/ci/util/diff_util.py b/metplus/util/diff_util.py similarity index 100% rename from ci/util/diff_util.py rename to metplus/util/diff_util.py diff --git a/metplus/util/met_config.py b/metplus/util/met_config.py new file mode 100644 index 0000000000..2e7c54ad00 --- /dev/null +++ b/metplus/util/met_config.py @@ -0,0 +1,710 @@ +""" +Program Name: met_config.py +Contact(s): George McCabe +""" + +import os + +from .met_util import getlist, get_threshold_via_regex, MISSING_DATA_VALUE +from .met_util import remove_quotes as util_remove_quotes +from .config_metplus import find_indices_in_config_section + +class METConfig: + """! Stores information for a member of a MET config variables that + can be used to set the value, the data type of the item, + optional name of environment variable to set (without METPLUS_ prefix) + if it differs from the name, + and any additional requirements such as remove quotes or make uppercase. + output_dict argument is ignored and only added to allow the argument + to the function that creates an instance of this object. + """ + def __init__(self, name, data_type, + env_var_name=None, + metplus_configs=None, + extra_args=None, + children=None, + output_dict=None): + self.name = name + self.data_type = data_type + self.metplus_configs = metplus_configs + self.extra_args = extra_args + self.env_var_name = env_var_name if env_var_name else name + self.children = children + + def __repr__(self): + return (f'{self.__class__.__name__}({self.name}, {self.data_type}, ' + f'{self.env_var_name}, ' + f'{self.metplus_configs}, ' + f'{self.extra_args}' + f', {self.children}' + ')') + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + if not isinstance(name, str): + raise TypeError("Name must be a string") + self._name = name + + @property + def data_type(self): + return self._data_type + + @data_type.setter + def data_type(self, data_type): + self._data_type = data_type + + @property + def env_var_name(self): + return self._env_var_name + + @env_var_name.setter + def env_var_name(self, env_var_name): + if not isinstance(env_var_name, str): + raise TypeError("Name must be a string") + self._env_var_name = env_var_name + + @property + def metplus_configs(self): + return self._metplus_configs + + @metplus_configs.setter + def metplus_configs(self, metplus_configs): + # convert to a list if input is a single value + config_names = metplus_configs + if config_names and not isinstance(config_names, list): + config_names = [config_names] + + self._metplus_configs = config_names + + @property + def extra_args(self): + return self._extra_args + + @extra_args.setter + def extra_args(self, extra_args): + args = extra_args if extra_args else {} + if not isinstance(args, dict): + raise TypeError("Expected a dictionary") + + self._extra_args = args + + @property + def children(self): + return self._children + + @children.setter + def children(self, children): + if not children and 'dict' in self.data_type: + raise TypeError("Must have children if data_type is dict.") + + if children: + if 'dict' not in self.data_type: + raise TypeError("data_type must be dict to have " + f"children. data_type is {self.data_type}") + + self._children = children + +def get_wrapped_met_config_file(config, app_name, default_config_file=None): + """! Get the MET config file path for the wrapper from the + METplusConfig object. If unset, use the default value if provided. + + @param default_config_file (optional) filename of wrapped MET config + file found in parm/met_config to use if config file is not set + @returns path to wrapped config file or None if no default is provided + """ + config_name = f'{app_name.upper()}_CONFIG_FILE' + config_file = config.getraw('config', config_name, '') + if config_file: + return config_file + + if not default_config_file: + return None + + default_config_path = os.path.join(config.getdir('PARM_BASE'), + 'met_config', + default_config_file) + config.logger.debug(f"{config_name} is not set. " + f"Using {default_config_path}") + return default_config_path + +def add_met_config_dict(config, app_name, output_dict, dict_name, items): + """! Read config variables for MET config dictionary and set + env_var_dict with formatted values + + @params dict_name name of MET dictionary variable + @params items dictionary where the key is name of variable inside MET + dictionary and the value is info about the item (see parse_item_info + function for more information) + """ + dict_items = [] + + # config prefix i.e GRID_STAT_CLIMO_MEAN_ + metplus_prefix = f'{app_name}_{dict_name}_'.upper() + for name, item_info in items.items(): + data_type, extra, kids, nicknames = _parse_item_info(item_info) + + # config name i.e. GRID_STAT_CLIMO_MEAN_FILE_NAME + metplus_name = f'{metplus_prefix}{name.upper()}' + + # change (n) to _N i.e. distance_map.beta_value(n) + metplus_name = metplus_name.replace('(N)', '_N') + metplus_configs = [] + + if 'dict' not in data_type: + children = None + # handle legacy OBS_WINDOW variables that put OBS_ before app name + # i.e. OBS_GRID_STAT_WINDOW_[BEGIN/END] + if dict_name == 'obs_window': + suffix = 'BEGIN' if name == 'beg' else name.upper() + + metplus_configs.append( + f"OBS_{app_name}_WINDOW_{suffix}".upper() + ) + # also add OBS_WINDOW_[BEGIN/END] + metplus_configs.append(f"OBS_WINDOW_{suffix}") + + # if variable ends with _BEG, read _BEGIN first + if metplus_name.endswith('BEG'): + metplus_configs.append(f'{metplus_name}IN') + + metplus_configs.append(metplus_name) + if nicknames: + for nickname in nicknames: + metplus_configs.append( + f'{app_name}_{nickname}'.upper() + ) + + # if dictionary, read get children from MET config + else: + children = [] + for kid_name, kid_info in kids.items(): + kid_upper = kid_name.upper() + kid_type, kid_extra, _, _ = _parse_item_info(kid_info) + + metplus_configs.append(f'{metplus_name}_{kid_upper}') + metplus_configs.append(f'{metplus_prefix}{kid_upper}') + + kid_args = _parse_extra_args(kid_extra) + child_item = METConfig( + name=kid_name, + data_type=kid_type, + metplus_configs=metplus_configs.copy(), + extra_args=kid_args, + ) + children.append(child_item) + + # reset metplus config list for next kid + metplus_configs.clear() + + # set metplus_configs + metplus_configs = None + + extra_args = _parse_extra_args(extra) + dict_item = ( + METConfig( + name=name, + data_type=data_type, + metplus_configs=metplus_configs, + extra_args=extra_args, + children=children, + ) + ) + dict_items.append(dict_item) + + final_met_config = METConfig( + name=dict_name, + data_type='dict', + children=dict_items, + ) + + return add_met_config_item(config, + final_met_config, + output_dict) + +def add_met_config_item(config, item, output_dict, depth=0): + """! Reads info from METConfig object, gets value from + METplusConfig, and formats it based on the specifications. Sets + value in output dictionary with key starting with METPLUS_. + + @param item METConfig object to read and determine what to get + @param output_dict dictionary to save formatted output + @param depth counter to check if item being processed is nested within + another variable or not. If depth is 0, it is a top level variable. + This is used internally by this function and shouldn't be supplied + outside of calls within this function. + """ + env_var_name = item.env_var_name.upper() + if not env_var_name.startswith('METPLUS_'): + env_var_name = f'METPLUS_{env_var_name}' + + # handle dictionary or dictionary list item + if 'dict' in item.data_type: + tmp_dict = {} + for child in item.children: + if not add_met_config_item(config, child, tmp_dict, + depth=depth+1): + return False + + dict_string = format_met_config(item.data_type, + tmp_dict, + item.name, + keys=None) + + # if handling dict MET config that is not nested inside another + if not depth and item.data_type == 'dict': + env_var_name = f'{env_var_name}_DICT' + + output_dict[env_var_name] = dict_string + return True + + # handle non-dictionary item + set_met_config = set_met_config_function(item.data_type) + if not set_met_config: + return False + + return set_met_config(config, + output_dict, + item.metplus_configs, + item.name, + c_dict_key=env_var_name, + **item.extra_args) + +def add_met_config_dict_list(config, app_name, output_dict, dict_name, + dict_items): + search_string = f'{app_name}_{dict_name}'.upper() + regex = r'^' + search_string + r'(\d+)_(\w+)$' + indices = find_indices_in_config_section(regex, config, + index_index=1, + id_index=2) + + all_met_config_items = {} + is_ok = True + for index, items in indices.items(): + # read all variables for each index + met_config_items = {} + + # check if any variable found doesn't match valid variables + not_in_dict = [item for item in items + if item.lower() not in dict_items] + if any(not_in_dict): + for item in not_in_dict: + config.logger.error("Invalid variable: " + f"{search_string}{index}_{item}") + is_ok = False + continue + + for name, item_info in dict_items.items(): + data_type, extra, kids, nicknames = _parse_item_info(item_info) + metplus_configs = [f'{search_string}{index}_{name.upper()}'] + extra_args = _parse_extra_args(extra) + item = METConfig(name=name, + data_type=data_type, + metplus_configs=metplus_configs, + extra_args=extra_args, + ) + + if not add_met_config_item(config, item, met_config_items): + is_ok = False + continue + + dict_string = format_met_config('dict', + met_config_items, + name='') + all_met_config_items[index] = dict_string + + # format list of dictionaries + output_string = format_met_config('list', + all_met_config_items, + dict_name) + output_dict[f'METPLUS_{dict_name.upper()}_LIST'] = output_string + return is_ok + +def format_met_config(data_type, c_dict, name, keys=None): + """! Return formatted variable named with any if they + are set to a value. If none of the items are set, return empty string + + @param data_type type of value to format + @param c_dict config dictionary to read values from + @param name name of dictionary to create + @param keys list of c_dict keys to use if they are set. If unset (None) + then read all keys from c_dict + @returns MET config formatted dictionary/list + if any items are set, or empty string if not + """ + values = [] + if keys is None: + keys = c_dict.keys() + + for key in keys: + value = c_dict.get(key) + if value: + values.append(str(value)) + + # if none of the keys are set to a value in dict, return empty string + if not values: + return '' + + output = ''.join(values) + # add curly braces if dictionary + if 'dict' in data_type: + output = f"{{{output}}}" + + # add square braces if list + if 'list' in data_type: + output = f"[{output}];" + + # if name is not empty, add variable name and equals sign + if name: + output = f'{name} = {output}' + return output + +def set_met_config_function(item_type): + """! Return function to use based on item type + + @param item_type type of MET config variable to obtain + Valid values: list, string, int, float, thresh, bool + @returns function to use or None if invalid type provided + """ + if item_type == 'int': + return set_met_config_int + elif item_type == 'string': + return set_met_config_string + elif item_type == 'list': + return set_met_config_list + elif item_type == 'float': + return set_met_config_float + elif item_type == 'thresh': + return set_met_config_thresh + elif item_type == 'bool': + return set_met_config_bool + else: + raise ValueError(f"Invalid argument for item type: {item_type}") + +def _get_config_or_default(mp_config_name, get_function, + default=None): + conf_value = '' + + # if no possible METplus config variables are not set + if mp_config_name is None: + # if no default defined, return without doing anything + if not default: + return None + + # if METplus config variable is set, read the value + else: + conf_value = get_function('config', + mp_config_name, + '') + + # if variable is not set and there is a default defined, set default + if not conf_value and default: + conf_value = default + + return conf_value + +def set_met_config_list(config, c_dict, mp_config, met_config_name, + c_dict_key=None, **kwargs): + """! Get list from METplus configuration file and format it to be passed + into a MET configuration file. Set c_dict item with formatted string. + Args: + @param c_dict configuration dictionary to set + @param mp_config_name METplus configuration variable name. Assumed to be + in the [config] section. Value can be a comma-separated list of items. + @param met_config name of MET configuration variable to set. Also used + to determine the key in c_dict to set (upper-case) + @param c_dict_key optional argument to specify c_dict key to store result. If + set to None (default) then use upper-case of met_config_name + @param allow_empty if True, if METplus config variable is set + but is an empty string, then set the c_dict value to an empty + list. If False, behavior is the same as when the variable is + not set at all, which is to not set anything for the c_dict + value + @param remove_quotes if True, output value without quotes. + Default value is False + @param default (Optional) if set, use this value as default + if config is not set + """ + mp_config_name = config.get_mp_config_name(mp_config) + conf_value = _get_config_or_default( + mp_config_name, + get_function=config.getraw, + default=kwargs.get('default') + ) + if conf_value is None: + return True + + # convert value from config to a list + conf_values = getlist(conf_value) + if conf_values or kwargs.get('allow_empty', False): + out_values = [] + for conf_value in conf_values: + remove_quotes = kwargs.get('remove_quotes', False) + # if not removing quotes, escape any quotes found in list items + if not remove_quotes: + conf_value = conf_value.replace('"', '\\"') + + conf_value = util_remove_quotes(conf_value) + if not remove_quotes: + conf_value = f'"{conf_value}"' + + out_values.append(conf_value) + out_value = f"[{', '.join(out_values)}]" + + if not c_dict_key: + c_key = met_config_name.upper() + else: + c_key = c_dict_key + + if met_config_name: + out_value = f'{met_config_name} = {out_value};' + c_dict[c_key] = out_value + + return True + +def set_met_config_string(config, c_dict, mp_config, met_config_name, + c_dict_key=None, **kwargs): + """! Get string from METplus configuration file and format it to be passed + into a MET configuration file. Set c_dict item with formatted string. + + @param c_dict configuration dictionary to set + @param mp_config METplus configuration variable name. Assumed to be + in the [config] section. Value can be a comma-separated list of items. + @param met_config_name name of MET configuration variable to set. Also used + to determine the key in c_dict to set (upper-case) + @param c_dict_key optional argument to specify c_dict key to store result. If + set to None (default) then use upper-case of met_config_name + @param remove_quotes if True, output value without quotes. + Default value is False + @param to_grid if True, format to_grid value + Default value is False + @param default (Optional) if set, use this value as default + if config is not set + """ + mp_config_name = config.get_mp_config_name(mp_config) + conf_value = _get_config_or_default( + mp_config_name, + get_function=config.getraw, + default=kwargs.get('default') + ) + if not conf_value: + return True + + conf_value = util_remove_quotes(conf_value) + # add quotes back if remote quotes is False + if not kwargs.get('remove_quotes'): + conf_value = f'"{conf_value}"' + + if kwargs.get('uppercase', False): + conf_value = conf_value.upper() + + if kwargs.get('to_grid', False): + conf_value = format_regrid_to_grid(conf_value) + + c_key = c_dict_key if c_dict_key else met_config_name.upper() + if met_config_name: + conf_value = f'{met_config_name} = {conf_value};' + + c_dict[c_key] = conf_value + return True + +def set_met_config_number(config, c_dict, num_type, mp_config, + met_config_name, c_dict_key=None, **kwargs): + """! Get integer from METplus configuration file and format it to be passed + into a MET configuration file. Set c_dict item with formatted string. + Args: + @param c_dict configuration dictionary to set + @param num_type type of number to get from config. If set to 'int', call + getint function. If not, call getfloat function. + @param mp_config METplus configuration variable name. Assumed to be + in the [config] section. Value can be a comma-separated list of items. + @param met_config_name name of MET configuration variable to set. Also used + to determine the key in c_dict to set (upper-case) if c_dict_key is None + @param c_dict_key optional argument to specify c_dict key to store result. If + set to None (default) then use upper-case of met_config_name + @param default (Optional) if set, use this value as default + if config is not set + """ + mp_config_name = config.get_mp_config_name(mp_config) + if mp_config_name is None: + return True + + if num_type == 'int': + conf_value = config.getint('config', mp_config_name) + else: + conf_value = config.getfloat('config', mp_config_name) + + if conf_value is None: + return False + if conf_value != MISSING_DATA_VALUE: + if not c_dict_key: + c_key = met_config_name.upper() + else: + c_key = c_dict_key + + if met_config_name: + out_value = f"{met_config_name} = {str(conf_value)};" + else: + out_value = str(conf_value) + c_dict[c_key] = out_value + + return True + +def set_met_config_int(config, c_dict, mp_config_name, met_config_name, + c_dict_key=None, **kwargs): + return set_met_config_number(config, c_dict, 'int', + mp_config_name, + met_config_name, + c_dict_key=c_dict_key, + **kwargs) + +def set_met_config_float(config, c_dict, mp_config_name, + met_config_name, c_dict_key=None, **kwargs): + return set_met_config_number(config, c_dict, 'float', + mp_config_name, + met_config_name, + c_dict_key=c_dict_key, + **kwargs) + +def set_met_config_thresh(config, c_dict, mp_config, met_config_name, + c_dict_key=None, **kwargs): + mp_config_name = config.get_mp_config_name(mp_config) + if mp_config_name is None: + return True + + conf_value = config.getstr('config', mp_config_name, '') + if conf_value: + if get_threshold_via_regex(conf_value) is None: + config.logger.error(f"Incorrectly formatted threshold: {mp_config_name}") + return False + + if not c_dict_key: + c_key = met_config_name.upper() + else: + c_key = c_dict_key + + if met_config_name: + out_value = f"{met_config_name} = {str(conf_value)};" + else: + out_value = str(conf_value) + + c_dict[c_key] = out_value + return True + +def set_met_config_bool(config, c_dict, mp_config, met_config_name, + c_dict_key=None, **kwargs): + """! Get boolean from METplus configuration file and format it to be + passed into a MET configuration file. Set c_dict item with boolean + value expressed as a string. + Args: + @param c_dict configuration dictionary to set + @param mp_config METplus configuration variable name. + Assumed to be in the [config] section. + @param met_config_name name of MET configuration variable to + set. Also used to determine the key in c_dict to set + (upper-case) + @param c_dict_key optional argument to specify c_dict key to + store result. If set to None (default) then use upper-case of + met_config_name + @param uppercase If true, set value to TRUE or FALSE + """ + mp_config_name = config.get_mp_config_name(mp_config) + if mp_config_name is None: + return True + conf_value = config.getbool('config', mp_config_name, '') + if conf_value is None: + config.logger.error(f'Invalid boolean value set for {mp_config_name}') + return False + + # if not invalid but unset, return without setting c_dict with no error + if conf_value == '': + return True + + conf_value = str(conf_value) + if kwargs.get('uppercase', True): + conf_value = conf_value.upper() + + if not c_dict_key: + c_key = met_config_name.upper() + else: + c_key = c_dict_key + + conf_value = util_remove_quotes(conf_value) + if met_config_name: + conf_value = f'{met_config_name} = {conf_value};' + c_dict[c_key] = conf_value + return True + +def format_regrid_to_grid(to_grid): + to_grid = to_grid.strip('"') + if not to_grid: + to_grid = 'NONE' + + # if NONE, FCST, or OBS force uppercase, otherwise add quotes + if to_grid.upper() in ['NONE', 'FCST', 'OBS']: + to_grid = to_grid.upper() + else: + to_grid = f'"{to_grid}"' + + return to_grid + +def _parse_item_info(item_info): + """! Parses info about a MET config dictionary item. The input can + be a single string that is the data type of the item. It can also be + a tuple containing 2 to 4 values. The additional values must be + supplied in order: + * extra: string of extra information about item, i.e. + 'remove_quotes', 'uppercase', or 'allow_empty' + * kids: dictionary describing child values (used only for dict items) + where the key is the name of the variable and the value is item info + for the child variable in the same format as item_info that is + parsed in this function + * nicknames: list of other METplus config variable name that can be + used to set a value. The app name i.e. GRID_STAT_ is prepended to + each nickname in the list. Used for backwards compatibility for + METplus config variables whose name does not match the MET config + variable name + + @param item_info string or tuple containing information about a + dictionary item + @returns tuple of data type, extra info, children, and nicknames or + None for each tuple value that is not set + """ + if isinstance(item_info, tuple): + data_type, *rest = item_info + else: + data_type = item_info + rest = [] + + extra = rest.pop(0) if rest else None + kids = rest.pop(0) if rest else None + nicknames = rest.pop(0) if rest else None + + return data_type, extra, kids, nicknames + +def _parse_extra_args(extra): + """! Check string for extra option keywords and set them to True in + dictionary if they are found. Supports 'remove_quotes', 'uppercase' + and 'allow_empty' + + @param extra string to parse for keywords + @returns dictionary with extra args set if found in string + """ + extra_args = {} + if not extra: + return extra_args + + VALID_EXTRAS = ( + 'remove_quotes', + 'uppercase', + 'allow_empty', + 'to_grid', + 'default', + ) + for extra_option in VALID_EXTRAS: + if extra_option in extra: + extra_args[extra_option] = True + return extra_args diff --git a/metplus/util/met_dictionary_info.py b/metplus/util/met_dictionary_info.py deleted file mode 100644 index 2decdc0f22..0000000000 --- a/metplus/util/met_dictionary_info.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -Program Name: met_dictionary_info.py -Contact(s): George McCabe -Abstract: -History Log: Initial version -Usage: -Parameters: None -Input Files: N/A -Output Files: N/A -""" - - -class METConfigInfo: - """! Stores information for a member of a MET config variables that - can be used to set the value, the data type of the item, - optional name of environment variable to set (without METPLUS_ prefix) - if it differs from the name, - and any additional requirements such as remove quotes or make uppercase. - output_dict argument is ignored and only added to allow the argument - to the function that creates an instance of this object. - """ - def __init__(self, name, data_type, - env_var_name=None, - metplus_configs=None, - extra_args=None, - children=None, - output_dict=None): - self.name = name - self.data_type = data_type - self.metplus_configs = metplus_configs - self.extra_args = extra_args - self.env_var_name = env_var_name if env_var_name else name - self.children = children - - def __repr__(self): - return (f'{self.__class__.__name__}({self.name}, {self.data_type}, ' - f'{self.env_var_name}, ' - f'{self.metplus_configs}, ' - f'{self.extra_args}' - f', {self.children}' - ')') - - @property - def name(self): - return self._name - - @name.setter - def name(self, name): - if not isinstance(name, str): - raise TypeError("Name must be a string") - self._name = name - - @property - def data_type(self): - return self._data_type - - @data_type.setter - def data_type(self, data_type): - self._data_type = data_type - - @property - def env_var_name(self): - return self._env_var_name - - @env_var_name.setter - def env_var_name(self, env_var_name): - if not isinstance(env_var_name, str): - raise TypeError("Name must be a string") - self._env_var_name = env_var_name - - @property - def metplus_configs(self): - return self._metplus_configs - - @metplus_configs.setter - def metplus_configs(self, metplus_configs): - # convert to a list if input is a single value - config_names = metplus_configs - if config_names and not isinstance(config_names, list): - config_names = [config_names] - - self._metplus_configs = config_names - - @property - def extra_args(self): - return self._extra_args - - @extra_args.setter - def extra_args(self, extra_args): - args = extra_args if extra_args else {} - if not isinstance(args, dict): - raise TypeError("Expected a dictionary") - - self._extra_args = args - - @property - def children(self): - return self._children - - @children.setter - def children(self, children): - if not children and 'dict' in self.data_type: - raise TypeError("Must have children if data_type is dict.") - - if children: - if 'dict' not in self.data_type: - raise TypeError("data_type must be dict to have " - f"children. data_type is {self.data_type}") - - self._children = children diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index 105740e420..5b853807fd 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -1,42 +1,27 @@ -import logging import os import shutil import sys import datetime -import errno -import time -import calendar import re import gzip import bz2 import zipfile import struct -import getpass -from os import stat -from pwd import getpwuid from csv import reader -from os.path import dirname, realpath from dateutil.relativedelta import relativedelta from pathlib import Path from importlib import import_module -import produtil.setup -import produtil.log - from .string_template_substitution import do_string_sub from .string_template_substitution import parse_template -from .string_template_substitution import get_tags from . import time_util as time_util -from .doc_util import get_wrapper_name - from .. import get_metplus_version """!@namespace met_util @brief Provides Utility functions for METplus. """ -# list of compression extensions that are handled by METplus -VALID_EXTENSIONS = ['.gz', '.bz2', '.zip'] +from .constants import * PYTHON_EMBEDDING_TYPES = ['PYTHON_NUMPY', 'PYTHON_XARRAY', 'PYTHON_PANDAS'] @@ -75,7 +60,7 @@ def pre_run_setup(config_inputs): logger.info(f"Config Input: {config_item}") # validate configuration variables - isOK_A, isOK_B, isOK_C, isOK_D, all_sed_cmds = validate_configuration_variables(config) + isOK_A, isOK_B, isOK_C, isOK_D, all_sed_cmds = config_metplus.validate_configuration_variables(config) if not (isOK_A and isOK_B and isOK_C and isOK_D): # if any sed commands were generated, write them to the sed file if all_sed_cmds: @@ -158,7 +143,7 @@ def run_metplus(config, process_list): elif loop_order == "times": all_commands = loop_over_times_and_call(config, processes) else: - logger.error("Invalid LOOP_ORDER defined. " + \ + logger.error("Invalid LOOP_ORDER defined. " "Options are processes, times") return 1 @@ -238,493 +223,6 @@ def write_all_commands(all_commands, config): file_handle.write("COMMAND:\n") file_handle.write(f"{command}\n\n") -def check_for_deprecated_config(config): - """!Checks user configuration files and reports errors or warnings if any deprecated variable - is found. If an alternate variable name can be suggested, add it to the 'alt' section - If the alternate cannot be literally substituted for the old name, set copy to False - Args: - @config : METplusConfig object to evaluate - Returns: - A tuple containing a boolean if the configuration is suitable to run or not and - if it is not correct, the 2nd item is a list of sed commands that can be run to help - fix the incorrect configuration variables - """ - - # key is the name of the depreacted variable that is no longer allowed in any config files - # value is a dictionary containing information about what to do with the deprecated config - # 'sec' is the section of the config file where the replacement resides, i.e. config, dir, - # filename_templates - # 'alt' is the alternative name for the deprecated config. this can be a single variable name or - # text to describe multiple variables or how to handle it. Set to None to tell the user to - # just remove the variable - # 'copy' is an optional item (defaults to True). set this to False if one cannot simply replace - # the deprecated config variable name with the value in 'alt' - # 'req' is an optional item (defaults to True). this to False to report a warning for the - # deprecated config and allow execution to continue. this is generally no longer used - # because we are requiring users to update the config files. if used, the developer must - # modify the code to handle both variables accordingly - deprecated_dict = { - 'LOOP_BY_INIT' : {'sec' : 'config', 'alt' : 'LOOP_BY', 'copy': False}, - 'LOOP_METHOD' : {'sec' : 'config', 'alt' : 'LOOP_ORDER'}, - 'PREPBUFR_DIR_REGEX' : {'sec' : 'regex_pattern', 'alt' : None}, - 'PREPBUFR_FILE_REGEX' : {'sec' : 'regex_pattern', 'alt' : None}, - 'OBS_INPUT_DIR_REGEX' : {'sec' : 'regex_pattern', 'alt' : 'OBS_POINT_STAT_INPUT_DIR', 'copy': False}, - 'FCST_INPUT_DIR_REGEX' : {'sec' : 'regex_pattern', 'alt' : 'FCST_POINT_STAT_INPUT_DIR', 'copy': False}, - 'FCST_INPUT_FILE_REGEX' : - {'sec' : 'regex_pattern', 'alt' : 'FCST_POINT_STAT_INPUT_TEMPLATE', 'copy': False}, - 'OBS_INPUT_FILE_REGEX' : {'sec' : 'regex_pattern', 'alt' : 'OBS_POINT_STAT_INPUT_TEMPLATE', 'copy': False}, - 'PREPBUFR_DATA_DIR' : {'sec' : 'dir', 'alt' : 'PB2NC_INPUT_DIR'}, - 'PREPBUFR_MODEL_DIR_NAME' : {'sec' : 'dir', 'alt' : 'PB2NC_INPUT_DIR', 'copy': False}, - 'OBS_INPUT_FILE_TMPL' : - {'sec' : 'filename_templates', 'alt' : 'OBS_POINT_STAT_INPUT_TEMPLATE'}, - 'FCST_INPUT_FILE_TMPL' : - {'sec' : 'filename_templates', 'alt' : 'FCST_POINT_STAT_INPUT_TEMPLATE'}, - 'NC_FILE_TMPL' : {'sec' : 'filename_templates', 'alt' : 'PB2NC_OUTPUT_TEMPLATE'}, - 'FCST_INPUT_DIR' : {'sec' : 'dir', 'alt' : 'FCST_POINT_STAT_INPUT_DIR'}, - 'OBS_INPUT_DIR' : {'sec' : 'dir', 'alt' : 'OBS_POINT_STAT_INPUT_DIR'}, - 'REGRID_TO_GRID' : {'sec' : 'config', 'alt' : 'POINT_STAT_REGRID_TO_GRID'}, - 'FCST_HR_START' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, - 'FCST_HR_END' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, - 'FCST_HR_INTERVAL' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, - 'START_DATE' : {'sec' : 'config', 'alt' : 'INIT_BEG or VALID_BEG', 'copy': False}, - 'END_DATE' : {'sec' : 'config', 'alt' : 'INIT_END or VALID_END', 'copy': False}, - 'INTERVAL_TIME' : {'sec' : 'config', 'alt' : 'INIT_INCREMENT or VALID_INCREMENT', 'copy': False}, - 'BEG_TIME' : {'sec' : 'config', 'alt' : 'INIT_BEG or VALID_BEG', 'copy': False}, - 'END_TIME' : {'sec' : 'config', 'alt' : 'INIT_END or VALID_END', 'copy': False}, - 'START_HOUR' : {'sec' : 'config', 'alt' : 'INIT_BEG or VALID_BEG', 'copy': False}, - 'END_HOUR' : {'sec' : 'config', 'alt' : 'INIT_END or VALID_END', 'copy': False}, - 'OBS_BUFR_VAR_LIST' : {'sec' : 'config', 'alt' : 'PB2NC_OBS_BUFR_VAR_LIST'}, - 'TIME_SUMMARY_FLAG' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_FLAG'}, - 'TIME_SUMMARY_BEG' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_BEG'}, - 'TIME_SUMMARY_END' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_END'}, - 'TIME_SUMMARY_VAR_NAMES' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_VAR_NAMES'}, - 'TIME_SUMMARY_TYPE' : {'sec' : 'config', 'alt' : 'PB2NC_TIME_SUMMARY_TYPE'}, - 'OVERWRITE_NC_OUTPUT' : {'sec' : 'config', 'alt' : 'PB2NC_SKIP_IF_OUTPUT_EXISTS', 'copy': False}, - 'VERTICAL_LOCATION' : {'sec' : 'config', 'alt' : 'PB2NC_VERTICAL_LOCATION'}, - 'VERIFICATION_GRID' : {'sec' : 'config', 'alt' : 'REGRID_DATA_PLANE_VERIF_GRID'}, - 'WINDOW_RANGE_BEG' : {'sec' : 'config', 'alt' : 'OBS_WINDOW_BEGIN'}, - 'WINDOW_RANGE_END' : {'sec' : 'config', 'alt' : 'OBS_WINDOW_END'}, - 'OBS_EXACT_VALID_TIME' : - {'sec' : 'config', 'alt' : 'OBS_WINDOW_BEGIN and OBS_WINDOW_END', 'copy': False}, - 'FCST_EXACT_VALID_TIME' : - {'sec' : 'config', 'alt' : 'FCST_WINDOW_BEGIN and FCST_WINDOW_END', 'copy': False}, - 'PCP_COMBINE_METHOD' : - {'sec' : 'config', 'alt' : 'FCST_PCP_COMBINE_METHOD and/or OBS_PCP_COMBINE_METHOD', 'copy': False}, - 'FHR_BEG' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, - 'FHR_END' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, - 'FHR_INC' : {'sec' : 'config', 'alt' : 'LEAD_SEQ', 'copy': False}, - 'FHR_GROUP_BEG' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_[N]', 'copy': False}, - 'FHR_GROUP_END' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_[N]', 'copy': False}, - 'FHR_GROUP_LABELS' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_[N]_LABEL', 'copy': False}, - 'CYCLONE_OUT_DIR' : {'sec' : 'dir', 'alt' : 'CYCLONE_OUTPUT_DIR'}, - 'ENSEMBLE_STAT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'ENSEMBLE_STAT_OUTPUT_DIR'}, - 'EXTRACT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'EXTRACT_TILES_OUTPUT_DIR'}, - 'GRID_STAT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'GRID_STAT_OUTPUT_DIR'}, - 'MODE_OUT_DIR' : {'sec' : 'dir', 'alt' : 'MODE_OUTPUT_DIR'}, - 'MTD_OUT_DIR' : {'sec' : 'dir', 'alt' : 'MTD_OUTPUT_DIR'}, - 'SERIES_INIT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_OUTPUT_DIR'}, - 'SERIES_LEAD_OUT_DIR' : {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_OUTPUT_DIR'}, - 'SERIES_INIT_FILTERED_OUT_DIR' : - {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, - 'SERIES_LEAD_FILTERED_OUT_DIR' : - {'sec' : 'dir', 'alt' : 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, - 'STAT_ANALYSIS_OUT_DIR' : - {'sec' : 'dir', 'alt' : 'STAT_ANALYSIS_OUTPUT_DIR'}, - 'TCMPR_PLOT_OUT_DIR' : {'sec' : 'dir', 'alt' : 'TCMPR_PLOT_OUTPUT_DIR'}, - 'FCST_MIN_FORECAST' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_MIN'}, - 'FCST_MAX_FORECAST' : {'sec' : 'config', 'alt' : 'LEAD_SEQ_MAX'}, - 'OBS_MIN_FORECAST' : {'sec' : 'config', 'alt' : 'OBS_PCP_COMBINE_MIN_LEAD'}, - 'OBS_MAX_FORECAST' : {'sec' : 'config', 'alt' : 'OBS_PCP_COMBINE_MAX_LEAD'}, - 'FCST_INIT_INTERVAL' : {'sec' : 'config', 'alt' : None}, - 'OBS_INIT_INTERVAL' : {'sec' : 'config', 'alt' : None}, - 'FCST_DATA_INTERVAL' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_DATA_INTERVAL'}, - 'OBS_DATA_INTERVAL' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_DATA_INTERVAL'}, - 'FCST_IS_DAILY_FILE' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_IS_DAILY_FILE'}, - 'OBS_IS_DAILY_FILE' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_IS_DAILY_FILE'}, - 'FCST_TIMES_PER_FILE' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_TIMES_PER_FILE'}, - 'OBS_TIMES_PER_FILE' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_TIMES_PER_FILE'}, - 'FCST_LEVEL' : {'sec' : '', 'alt' : 'FCST_PCP_COMBINE_INPUT_ACCUMS', 'copy': False}, - 'OBS_LEVEL' : {'sec' : '', 'alt' : 'OBS_PCP_COMBINE_INPUT_ACCUMS', 'copy': False}, - 'MODE_FCST_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'FCST_MODE_CONV_RADIUS'}, - 'MODE_FCST_CONV_THRESH' : {'sec' : 'config', 'alt' : 'FCST_MODE_CONV_THRESH'}, - 'MODE_FCST_MERGE_FLAG' : {'sec' : 'config', 'alt' : 'FCST_MODE_MERGE_FLAG'}, - 'MODE_FCST_MERGE_THRESH' : {'sec' : 'config', 'alt' : 'FCST_MODE_MERGE_THRESH'}, - 'MODE_OBS_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'OBS_MODE_CONV_RADIUS'}, - 'MODE_OBS_CONV_THRESH' : {'sec' : 'config', 'alt' : 'OBS_MODE_CONV_THRESH'}, - 'MODE_OBS_MERGE_FLAG' : {'sec' : 'config', 'alt' : 'OBS_MODE_MERGE_FLAG'}, - 'MODE_OBS_MERGE_THRESH' : {'sec' : 'config', 'alt' : 'OBS_MODE_MERGE_THRESH'}, - 'MTD_FCST_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'FCST_MTD_CONV_RADIUS'}, - 'MTD_FCST_CONV_THRESH' : {'sec' : 'config', 'alt' : 'FCST_MTD_CONV_THRESH'}, - 'MTD_OBS_CONV_RADIUS' : {'sec' : 'config', 'alt' : 'OBS_MTD_CONV_RADIUS'}, - 'MTD_OBS_CONV_THRESH' : {'sec' : 'config', 'alt' : 'OBS_MTD_CONV_THRESH'}, - 'RM_EXE' : {'sec' : 'exe', 'alt' : 'RM'}, - 'CUT_EXE' : {'sec' : 'exe', 'alt' : 'CUT'}, - 'TR_EXE' : {'sec' : 'exe', 'alt' : 'TR'}, - 'NCAP2_EXE' : {'sec' : 'exe', 'alt' : 'NCAP2'}, - 'CONVERT_EXE' : {'sec' : 'exe', 'alt' : 'CONVERT'}, - 'NCDUMP_EXE' : {'sec' : 'exe', 'alt' : 'NCDUMP'}, - 'EGREP_EXE' : {'sec' : 'exe', 'alt' : 'EGREP'}, - 'ADECK_TRACK_DATA_DIR' : {'sec' : 'dir', 'alt' : 'TC_PAIRS_ADECK_INPUT_DIR'}, - 'BDECK_TRACK_DATA_DIR' : {'sec' : 'dir', 'alt' : 'TC_PAIRS_BDECK_INPUT_DIR'}, - 'MISSING_VAL_TO_REPLACE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_MISSING_VAL_TO_REPLACE'}, - 'MISSING_VAL' : {'sec' : 'config', 'alt' : 'TC_PAIRS_MISSING_VAL'}, - 'TRACK_DATA_SUBDIR_MOD' : {'sec' : 'dir', 'alt' : None}, - 'ADECK_FILE_PREFIX' : {'sec' : 'config', 'alt' : 'TC_PAIRS_ADECK_TEMPLATE', 'copy': False}, - 'BDECK_FILE_PREFIX' : {'sec' : 'config', 'alt' : 'TC_PAIRS_BDECK_TEMPLATE', 'copy': False}, - 'TOP_LEVEL_DIRS' : {'sec' : 'config', 'alt' : 'TC_PAIRS_READ_ALL_FILES'}, - 'TC_PAIRS_DIR' : {'sec' : 'dir', 'alt' : 'TC_PAIRS_OUTPUT_DIR'}, - 'CYCLONE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_CYCLONE'}, - 'STORM_ID' : {'sec' : 'config', 'alt' : 'TC_PAIRS_STORM_ID'}, - 'BASIN' : {'sec' : 'config', 'alt' : 'TC_PAIRS_BASIN'}, - 'STORM_NAME' : {'sec' : 'config', 'alt' : 'TC_PAIRS_STORM_NAME'}, - 'DLAND_FILE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_DLAND_FILE'}, - 'TRACK_TYPE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_REFORMAT_DECK'}, - 'FORECAST_TMPL' : {'sec' : 'filename_templates', 'alt' : 'TC_PAIRS_ADECK_TEMPLATE'}, - 'REFERENCE_TMPL' : {'sec' : 'filename_templates', 'alt' : 'TC_PAIRS_BDECK_TEMPLATE'}, - 'TRACK_DATA_MOD_FORCE_OVERWRITE' : - {'sec' : 'config', 'alt' : 'TC_PAIRS_SKIP_IF_REFORMAT_EXISTS', 'copy': False}, - 'TC_PAIRS_FORCE_OVERWRITE' : {'sec' : 'config', 'alt' : 'TC_PAIRS_SKIP_IF_OUTPUT_EXISTS', 'copy': False}, - 'GRID_STAT_CONFIG' : {'sec' : 'config', 'alt' : 'GRID_STAT_CONFIG_FILE'}, - 'MODE_CONFIG' : {'sec' : 'config', 'alt': 'MODE_CONFIG_FILE'}, - 'FCST_PCP_COMBINE_INPUT_LEVEL': {'sec': 'config', 'alt' : 'FCST_PCP_COMBINE_INPUT_ACCUMS'}, - 'OBS_PCP_COMBINE_INPUT_LEVEL': {'sec': 'config', 'alt' : 'OBS_PCP_COMBINE_INPUT_ACCUMS'}, - 'TIME_METHOD': {'sec': 'config', 'alt': 'LOOP_BY', 'copy': False}, - 'MODEL_DATA_DIR': {'sec': 'dir', 'alt': 'EXTRACT_TILES_GRID_INPUT_DIR'}, - 'STAT_LIST': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_STAT_LIST'}, - 'NLAT': {'sec': 'config', 'alt': 'EXTRACT_TILES_NLAT'}, - 'NLON': {'sec': 'config', 'alt': 'EXTRACT_TILES_NLON'}, - 'DLAT': {'sec': 'config', 'alt': 'EXTRACT_TILES_DLAT'}, - 'DLON': {'sec': 'config', 'alt': 'EXTRACT_TILES_DLON'}, - 'LON_ADJ': {'sec': 'config', 'alt': 'EXTRACT_TILES_LON_ADJ'}, - 'LAT_ADJ': {'sec': 'config', 'alt': 'EXTRACT_TILES_LAT_ADJ'}, - 'OVERWRITE_TRACK': {'sec': 'config', 'alt': 'EXTRACT_TILES_OVERWRITE_TRACK'}, - 'BACKGROUND_MAP': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_BACKGROUND_MAP'}, - 'GFS_FCST_FILE_TMPL': {'sec': 'filename_templates', 'alt': 'FCST_EXTRACT_TILES_INPUT_TEMPLATE'}, - 'GFS_ANLY_FILE_TMPL': {'sec': 'filename_templates', 'alt': 'OBS_EXTRACT_TILES_INPUT_TEMPLATE'}, - 'SERIES_BY_LEAD_FILTERED_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, - 'SERIES_BY_INIT_FILTERED_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_FILTERED_OUTPUT_DIR'}, - 'SERIES_BY_LEAD_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_OUTPUT_DIR'}, - 'SERIES_BY_INIT_OUTPUT_DIR': {'sec': 'dir', 'alt': 'SERIES_ANALYSIS_OUTPUT_DIR'}, - 'SERIES_BY_LEAD_GROUP_FCSTS': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_GROUP_FCSTS'}, - 'SERIES_ANALYSIS_BY_LEAD_CONFIG_FILE': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_CONFIG_FILE'}, - 'SERIES_ANALYSIS_BY_INIT_CONFIG_FILE': {'sec': 'config', 'alt': 'SERIES_ANALYSIS_CONFIG_FILE'}, - 'ENSEMBLE_STAT_MET_OBS_ERROR_TABLE': {'sec': 'config', 'alt': 'ENSEMBLE_STAT_MET_OBS_ERR_TABLE'}, - 'VAR_LIST': {'sec': 'config', 'alt': 'BOTH_VAR_NAME BOTH_VAR_LEVELS or SERIES_ANALYSIS_VAR_LIST', 'copy': False}, - 'SERIES_ANALYSIS_VAR_LIST': {'sec': 'config', 'alt': 'BOTH_VAR_NAME BOTH_VAR_LEVELS', 'copy': False}, - 'EXTRACT_TILES_VAR_LIST': {'sec': 'config', 'alt': ''}, - 'STAT_ANALYSIS_LOOKIN_DIR': {'sec': 'dir', 'alt': 'MODEL1_STAT_ANALYSIS_LOOKIN_DIR'}, - 'VALID_HOUR_METHOD': {'sec': 'config', 'alt': None}, - 'VALID_HOUR_BEG': {'sec': 'config', 'alt': None}, - 'VALID_HOUR_END': {'sec': 'config', 'alt': None}, - 'VALID_HOUR_INCREMENT': {'sec': 'config', 'alt': None}, - 'INIT_HOUR_METHOD': {'sec': 'config', 'alt': None}, - 'INIT_HOUR_BEG': {'sec': 'config', 'alt': None}, - 'INIT_HOUR_END': {'sec': 'config', 'alt': None}, - 'INIT_HOUR_INCREMENT': {'sec': 'config', 'alt': None}, - 'STAT_ANALYSIS_CONFIG': {'sec': 'config', 'alt': 'STAT_ANALYSIS_CONFIG_FILE'}, - 'JOB_NAME': {'sec': 'config', 'alt': 'STAT_ANALYSIS_JOB_NAME'}, - 'JOB_ARGS': {'sec': 'config', 'alt': 'STAT_ANALYSIS_JOB_ARGS'}, - 'FCST_LEAD': {'sec': 'config', 'alt': 'FCST_LEAD_LIST'}, - 'FCST_VAR_NAME': {'sec': 'config', 'alt': 'FCST_VAR_LIST'}, - 'FCST_VAR_LEVEL': {'sec': 'config', 'alt': 'FCST_VAR_LEVEL_LIST'}, - 'OBS_VAR_NAME': {'sec': 'config', 'alt': 'OBS_VAR_LIST'}, - 'OBS_VAR_LEVEL': {'sec': 'config', 'alt': 'OBS_VAR_LEVEL_LIST'}, - 'REGION': {'sec': 'config', 'alt': 'VX_MASK_LIST'}, - 'INTERP': {'sec': 'config', 'alt': 'INTERP_LIST'}, - 'INTERP_PTS': {'sec': 'config', 'alt': 'INTERP_PTS_LIST'}, - 'CONV_THRESH': {'sec': 'config', 'alt': 'CONV_THRESH_LIST'}, - 'FCST_THRESH': {'sec': 'config', 'alt': 'FCST_THRESH_LIST'}, - 'LINE_TYPE': {'sec': 'config', 'alt': 'LINE_TYPE_LIST'}, - 'STAT_ANALYSIS_DUMP_ROW_TMPL': {'sec': 'filename_templates', 'alt': 'STAT_ANALYSIS_DUMP_ROW_TEMPLATE'}, - 'STAT_ANALYSIS_OUT_STAT_TMPL': {'sec': 'filename_templates', 'alt': 'STAT_ANALYSIS_OUT_STAT_TEMPLATE'}, - 'PLOTTING_SCRIPTS_DIR': {'sec': 'dir', 'alt': 'MAKE_PLOTS_SCRIPTS_DIR'}, - 'STAT_FILES_INPUT_DIR': {'sec': 'dir', 'alt': 'MAKE_PLOTS_INPUT_DIR'}, - 'PLOTTING_OUTPUT_DIR': {'sec': 'dir', 'alt': 'MAKE_PLOTS_OUTPUT_DIR'}, - 'VERIF_CASE': {'sec': 'config', 'alt': 'MAKE_PLOTS_VERIF_CASE'}, - 'VERIF_TYPE': {'sec': 'config', 'alt': 'MAKE_PLOTS_VERIF_TYPE'}, - 'PLOT_TIME': {'sec': 'config', 'alt': 'DATE_TIME'}, - 'MODEL_NAME': {'sec': 'config', 'alt': 'MODEL'}, - 'MODEL_OBS_NAME': {'sec': 'config', 'alt': 'MODEL_OBTYPE'}, - 'MODEL_STAT_DIR': {'sec': 'dir', 'alt': 'MODEL_STAT_ANALYSIS_LOOKIN_DIR'}, - 'MODEL_NAME_ON_PLOT': {'sec': 'config', 'alt': 'MODEL_REFERENCE_NAME'}, - 'REGION_LIST': {'sec': 'config', 'alt': 'VX_MASK_LIST'}, - 'PLOT_STATS_LIST': {'sec': 'config', 'alt': 'MAKE_PLOT_STATS_LIST'}, - 'CI_METHOD': {'sec': 'config', 'alt': 'MAKE_PLOTS_CI_METHOD'}, - 'VERIF_GRID': {'sec': 'config', 'alt': 'MAKE_PLOTS_VERIF_GRID'}, - 'EVENT_EQUALIZATION': {'sec': 'config', 'alt': 'MAKE_PLOTS_EVENT_EQUALIZATION'}, - 'MTD_CONFIG': {'sec': 'config', 'alt': 'MTD_CONFIG_FILE'}, - 'CLIMO_GRID_STAT_INPUT_DIR': {'sec': 'dir', 'alt': 'GRID_STAT_CLIMO_MEAN_INPUT_DIR'}, - 'CLIMO_GRID_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', 'alt': 'GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE'}, - 'CLIMO_POINT_STAT_INPUT_DIR': {'sec': 'dir', 'alt': 'POINT_STAT_CLIMO_MEAN_INPUT_DIR'}, - 'CLIMO_POINT_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', 'alt': 'POINT_STAT_CLIMO_MEAN_INPUT_TEMPLATE'}, - 'GEMPAKTOCF_CLASSPATH': {'sec': 'exe', 'alt': 'GEMPAKTOCF_JAR', 'copy': False}, - 'CUSTOM_INGEST__OUTPUT_DIR': {'sec': 'dir', 'alt': 'PY_EMBED_INGEST__OUTPUT_DIR'}, - 'CUSTOM_INGEST__OUTPUT_TEMPLATE': {'sec': 'filename_templates', 'alt': 'PY_EMBED_INGEST__OUTPUT_TEMPLATE'}, - 'CUSTOM_INGEST__OUTPUT_GRID': {'sec': 'config', 'alt': 'PY_EMBED_INGEST__OUTPUT_GRID'}, - 'CUSTOM_INGEST__SCRIPT': {'sec': 'config', 'alt': 'PY_EMBED_INGEST__SCRIPT'}, - 'CUSTOM_INGEST__TYPE': {'sec': 'config', 'alt': 'PY_EMBED_INGEST__TYPE'}, - 'TC_STAT_RUN_VIA': {'sec': 'config', 'alt': 'TC_STAT_CONFIG_FILE', - 'copy': False}, - 'TC_STAT_CMD_LINE_JOB': {'sec': 'config', 'alt': 'TC_STAT_JOB_ARGS'}, - 'TC_STAT_JOBS_LIST': {'sec': 'config', 'alt': 'TC_STAT_JOB_ARGS'}, - 'EXTRACT_TILES_OVERWRITE_TRACK': {'sec': 'config', - 'alt': 'EXTRACT_TILES_SKIP_IF_OUTPUT_EXISTS', - 'copy': False}, - 'EXTRACT_TILES_PAIRS_INPUT_DIR': {'sec': 'dir', - 'alt': 'EXTRACT_TILES_STAT_INPUT_DIR', - 'copy': False}, - 'EXTRACT_TILES_FILTERED_OUTPUT_TEMPLATE': {'sec': 'filename_template', - 'alt': 'EXTRACT_TILES_STAT_INPUT_TEMPLATE',}, - 'EXTRACT_TILES_GRID_INPUT_DIR': {'sec': 'dir', - 'alt': 'FCST_EXTRACT_TILES_INPUT_DIR' - 'and ' - 'OBS_EXTRACT_TILES_INPUT_DIR', - 'copy': False}, - 'SERIES_ANALYSIS_FILTER_OPTS': {'sec': 'config', - 'alt': 'TC_STAT_JOB_ARGS', - 'copy': False}, - 'SERIES_ANALYSIS_INPUT_DIR': {'sec': 'dir', - 'alt': 'FCST_SERIES_ANALYSIS_INPUT_DIR ' - 'and ' - 'OBS_SERIES_ANALYSIS_INPUT_DIR'}, - 'FCST_SERIES_ANALYSIS_TILE_INPUT_TEMPLATE': {'sec': 'filename_templates', - 'alt': 'FCST_SERIES_ANALYSIS_INPUT_TEMPLATE '}, - 'OBS_SERIES_ANALYSIS_TILE_INPUT_TEMPLATE': {'sec': 'filename_templates', - 'alt': 'OBS_SERIES_ANALYSIS_INPUT_TEMPLATE '}, - 'EXTRACT_TILES_STAT_INPUT_DIR': {'sec': 'dir', - 'alt': 'EXTRACT_TILES_TC_STAT_INPUT_DIR',}, - 'EXTRACT_TILES_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', - 'alt': 'EXTRACT_TILES_TC_STAT_INPUT_TEMPLATE',}, - 'SERIES_ANALYSIS_STAT_INPUT_DIR': {'sec': 'dir', - 'alt': 'SERIES_ANALYSIS_TC_STAT_INPUT_DIR', }, - 'SERIES_ANALYSIS_STAT_INPUT_TEMPLATE': {'sec': 'filename_templates', - 'alt': 'SERIES_ANALYSIS_TC_STAT_INPUT_TEMPLATE', }, - } - - # template '' : {'sec' : '', 'alt' : '', 'copy': True}, - - logger = config.logger - - # create list of errors and warnings to report for deprecated configs - e_list = [] - w_list = [] - all_sed_cmds = [] - - for old, depr_info in deprecated_dict.items(): - if isinstance(depr_info, dict): - - # check if is found in the old item, use regex to find variables if found - if '' in old: - old_regex = old.replace('', r'(\d+)') - indicies = find_indices_in_config_section(old_regex, - config, - index_index=1).keys() - for index in indicies: - old_with_index = old.replace('', index) - if depr_info['alt']: - alt_with_index = depr_info['alt'].replace('', index) - else: - alt_with_index = '' - - handle_deprecated(old_with_index, alt_with_index, depr_info, - config, all_sed_cmds, w_list, e_list) - else: - handle_deprecated(old, depr_info['alt'], depr_info, - config, all_sed_cmds, w_list, e_list) - - - # check all templates and error if any deprecated tags are used - # value of dict is replacement tag, set to None if no replacement exists - # deprecated tags: region (replace with basin) - deprecated_tags = {'region' : 'basin'} - template_vars = config.keys('config') - template_vars = [tvar for tvar in template_vars if tvar.endswith('_TEMPLATE')] - for temp_var in template_vars: - template = config.getraw('filename_templates', temp_var) - tags = get_tags(template) - - for depr_tag, replace_tag in deprecated_tags.items(): - if depr_tag in tags: - e_msg = 'Deprecated tag {{{}}} found in {}.'.format(depr_tag, - temp_var) - if replace_tag is not None: - e_msg += ' Replace with {{{}}}'.format(replace_tag) - - e_list.append(e_msg) - - # if any warning exist, report them - if w_list: - for warning_msg in w_list: - logger.warning(warning_msg) - - # if any errors exist, report them and exit - if e_list: - logger.error('DEPRECATED CONFIG ITEMS WERE FOUND. ' +\ - 'PLEASE REMOVE/REPLACE THEM FROM CONFIG FILES') - for error_msg in e_list: - logger.error(error_msg) - return False, all_sed_cmds - - return True, [] - -def handle_deprecated(old, alt, depr_info, config, all_sed_cmds, w_list, e_list): - sec = depr_info['sec'] - config_files = config.getstr('config', 'CONFIG_INPUT', '').split(',') - # if deprecated config item is found - if config.has_option(sec, old): - # if it is not required to remove, add to warning list - if 'req' in depr_info.keys() and depr_info['req'] is False: - msg = '[{}] {} is deprecated and will be '.format(sec, old) + \ - 'removed in a future version of METplus' - if alt: - msg += ". Please replace with {}".format(alt) - w_list.append(msg) - # if it is required to remove, add to error list - else: - if not alt: - e_list.append("[{}] {} should be removed".format(sec, old)) - else: - e_list.append("[{}] {} should be replaced with {}".format(sec, old, alt)) - - if 'copy' not in depr_info.keys() or depr_info['copy']: - for config_file in config_files: - all_sed_cmds.append(f"sed -i 's|^{old}|{alt}|g' {config_file}") - all_sed_cmds.append(f"sed -i 's|{{{old}}}|{{{alt}}}|g' {config_file}") - -def check_for_deprecated_met_config(config): - sed_cmds = [] - all_good = True - - # set CURRENT_* METplus variables in case they are referenced in a - # METplus config variable and not already set - for fcst_or_obs in ['FCST', 'OBS']: - for name_or_level in ['NAME', 'LEVEL']: - current_var = f'CURRENT_{fcst_or_obs}_{name_or_level}' - if not config.has_option('config', current_var): - config.set('config', current_var, '') - - # check if *_CONFIG_FILE if set in the METplus config file and check for - # deprecated environment variables in those files - met_config_keys = [key for key in config.keys('config') - if key.endswith('CONFIG_FILE')] - - for met_config_key in met_config_keys: - met_tool = met_config_key.replace('_CONFIG_FILE', '') - - # get custom loop list to check if multiple config files are used based on the custom string - custom_list = get_custom_string_list(config, met_tool) - - for custom_string in custom_list: - met_config = config.getraw('config', met_config_key) - if not met_config: - continue - - met_config_file = do_string_sub(met_config, custom=custom_string) - - if not check_for_deprecated_met_config_file(config, met_config_file, sed_cmds, met_tool): - all_good = False - - return all_good, sed_cmds - -def check_for_deprecated_met_config_file(config, met_config, sed_cmds, met_tool): - - all_good = True - if not os.path.exists(met_config): - config.logger.error(f"Config file does not exist: {met_config}") - return False - - deprecated_met_list = ['MET_VALID_HHMM', 'GRID_VX', 'CONFIG_DIR'] - deprecated_output_prefix_list = ['FCST_VAR', 'OBS_VAR'] - config.logger.debug(f"Checking for deprecated environment variables in: {met_config}") - - with open(met_config, 'r') as file_handle: - lines = file_handle.read().splitlines() - - for line in lines: - for deprecated_item in deprecated_met_list: - if '${' + deprecated_item + '}' in line: - all_good = False - config.logger.error("Please remove deprecated environment variable " - f"${{{deprecated_item}}} found in MET config file: " - f"{met_config}") - - if deprecated_item == 'MET_VALID_HHMM' and 'file_name' in line: - config.logger.error(f"Set {met_tool}_CLIMO_MEAN_INPUT_[DIR/TEMPLATE] in a " - "METplus config file to set CLIMO_MEAN_FILE in a MET config") - new_line = " file_name = [ ${CLIMO_MEAN_FILE} ];" - - # escape [ and ] because they are special characters in sed commands - old_line = line.rstrip().replace('[', r'\[').replace(']', r'\]') - - sed_cmds.append(f"sed -i 's|^{old_line}|{new_line}|g' {met_config}") - add_line = f"{met_tool}_CLIMO_MEAN_INPUT_TEMPLATE" - sed_cmds.append(f"#Add {add_line}") - break - - if 'to_grid' in line: - config.logger.error("MET to_grid variable should reference " - "${REGRID_TO_GRID} environment variable") - new_line = " to_grid = ${REGRID_TO_GRID};" - - # escape [ and ] because they are special characters in sed commands - old_line = line.rstrip().replace('[', r'\[').replace(']', r'\]') - - sed_cmds.append(f"sed -i 's|^{old_line}|{new_line}|g' {met_config}") - config.logger.info(f"Be sure to set {met_tool}_REGRID_TO_GRID to the correct value.") - add_line = f"{met_tool}_REGRID_TO_GRID" - sed_cmds.append(f"#Add {add_line}") - break - - - for deprecated_item in deprecated_output_prefix_list: - # if deprecated item found in output prefix or to_grid line, replace line to use - # env var OUTPUT_PREFIX or REGRID_TO_GRID - if '${' + deprecated_item + '}' in line and 'output_prefix' in line: - config.logger.error("output_prefix variable should reference " - "${OUTPUT_PREFIX} environment variable") - new_line = "output_prefix = \"${OUTPUT_PREFIX}\";" - - # escape [ and ] because they are special characters in sed commands - old_line = line.rstrip().replace('[', r'\[').replace(']', r'\]') - - sed_cmds.append(f"sed -i 's|^{old_line}|{new_line}|g' {met_config}") - config.logger.info(f"You will need to add {met_tool}_OUTPUT_PREFIX to the METplus config file" - f" that sets {met_tool}_CONFIG_FILE. Set it to:") - output_prefix = replace_output_prefix(line) - add_line = f"{met_tool}_OUTPUT_PREFIX = {output_prefix}" - config.logger.info(add_line) - sed_cmds.append(f"#Add {add_line}") - all_good = False - break - - return all_good - -def replace_output_prefix(line): - op_replacements = {'${MODEL}': '{MODEL}', - '${FCST_VAR}': '{CURRENT_FCST_NAME}', - '${OBTYPE}': '{OBTYPE}', - '${OBS_VAR}': '{CURRENT_OBS_NAME}', - '${LEVEL}': '{CURRENT_FCST_LEVEL}', - '${FCST_TIME}': '{lead?fmt=%3H}', - } - prefix = line.split('=')[1].strip().rstrip(';').strip('"') - for key, value, in op_replacements.items(): - prefix = prefix.replace(key, value) - - return prefix - -def get_custom_string_list(config, met_tool): - custom_loop_list = config.getstr_nocheck('config', - f'{met_tool.upper()}_CUSTOM_LOOP_LIST', - config.getstr_nocheck('config', - 'CUSTOM_LOOP_LIST', - '')) - custom_loop_list = getlist(custom_loop_list) - if not custom_loop_list: - custom_loop_list.append('') - - return custom_loop_list - def handle_tmp_dir(config): """! if env var MET_TMP_DIR is set, override config TMP_DIR with value if it differs from what is set @@ -1276,18 +774,6 @@ def round_0p5(val): return round(val * 2) / 2 -def round_to_int(val): - """! Round to integer value - Args: - @param val: The value to round up - Returns: - rval: The rounded up value. - """ - val += 0.5 - rval = int(val) - return rval - - def mkdir_p(path): """! From stackoverflow.com/questions/600268/mkdir-p-functionality-in-python @@ -1301,138 +787,6 @@ def mkdir_p(path): """ Path(path).mkdir(parents=True, exist_ok=True) -def _rmtree_onerr(function, path, exc_info, logger=None): - """!Internal function used to log errors. - This is an internal implementation function called by - shutil.rmtree when an underlying function call failed. See - the Python documentation of shutil.rmtree for details. - @param function the funciton that failed - @param path the path to the function that caused problems - @param exc_info the exception information - @protected""" - if logger: - logger.warning('%s: %s failed: %s' % ( - str(path), str(function), str(exc_info))) - - -def rmtree(tree, logger=None): - """!Deletes the tree, if possible. - @protected - @param tree the directory tree to delete" - @param logger the logger, optional - """ - try: - # If it is a file, special file or symlink we can just - # delete it via unlink: - os.unlink(tree) - return - except EnvironmentError: - pass - # We get here for directories. - if logger: - logger.info('%s: rmtree' % (tree,)) - shutil.rmtree(tree, ignore_errors=False) - -def file_exists(filename): - """! Determines if a file exists - NOTE: Simply using os.path.isfile() is not a Pythonic way - to check if a file exists. You can - still encounter a TOCTTOU bug - "time of check to time of use" - Instead, use the raising of - exceptions, which is a Pythonic - approach: - try: - with open(filename) as fileobj: - pass # or do something fruitful - except IOError as e: - logger.error("your helpful error message goes here") - Args: - @param filename: the full filename (full path) - Returns: - boolean : True if file exists, False otherwise - """ - - try: - return os.path.isfile(filename) - except IOError: - pass - - -def is_dir_empty(directory): - """! Determines if a directory exists and is not empty - Args: - @param directory: The directory to check for existence - and for contents. - Returns: - True: If the directory is empty - False: If the directory exists and isn't empty - """ - return not os.listdir(directory) - -def grep(pattern, infile): - """! Python version of grep, searches the file line-by-line - to find a match to the pattern. Returns upon finding the - first match. - Args: - @param pattern: The pattern to be matched - @param infile: The filename with full filepath in which to - search for the pattern - Returns: - line (string): The matching string - """ - - matching_lines = [] - with open(infile, 'r') as file_handle: - for line in file_handle: - match = re.search(pattern, line) - if match: - matching_lines.append(line) - # if you got here, you didn't find anything - return matching_lines - - -def get_filepaths_for_grbfiles(base_dir): - """! Generates the grb2 file names in a directory tree - by walking the tree either top-down or bottom-up. - For each directory in the tree rooted at - the directory top (including top itself), it - produces a tuple: (dirpath, dirnames, filenames). - This solution was found on Stack Overflow: - http://stackoverflow.com/questions/3207219/how-to-list-all-files-of-a- - directory-in-python#3207973 - **scroll down to the section with "Getting Full File Paths From a - Directory and All Its Subdirectories" - Args: - @param base_dir: The base directory from which we - begin the search for grib2 filenames. - Returns: - file_paths (list): A list of the full filepaths - of the data to be processed. - """ - - # Create an empty list which will eventually store - # all the full filenames - file_paths = [] - - # pylint:disable=unused-variable - # os.walk returns tuple, we don't need to utilize all the returned - # values in the tuple. - - # Walk the tree - for root, directories, files in os.walk(base_dir): - for filename in files: - # add it to the list only if it is a grib file - match = re.match(r'.*(grib|grb|grib2|grb2)$', filename) - if match: - # Join the two strings to form the full - # filepath. - filepath = os.path.join(root, filename) - file_paths.append(filepath) - else: - continue - return file_paths - def get_storms(filter_filename, id_only=False, sort_column='STORM_ID'): """! Get each storm as identified by a column in the input file. Create dictionary storm ID as the key and a list of lines for that @@ -1476,18 +830,6 @@ def get_storms(filter_filename, id_only=False, sort_column='STORM_ID'): return storm_dict -def get_storm_ids(filter_filename): - """! Get each storm as identified by its STORM_ID in the filter file - save these in a set so we only save the unique ids and sort them. - Args: - @param filter_filename: The name of the filter file to read - and extract the storm id - @param logger: The name of the logger for logging useful info - Returns: - sorted_storms (List): a list of unique, sorted storm ids - """ - return get_storms(filter_filename, id_only=True) - def get_files(filedir, filename_regex, logger=None): """! Get all the files (with a particular naming format) by walking @@ -1742,68 +1084,6 @@ def camel_to_underscore(camel): s1 = re.sub(r'([^\d])([A-Z][a-z]+)', r'\1_\2', camel) return re.sub(r'([a-z])([A-Z])', r'\1_\2', s1).lower() -def get_process_list(config): - """!Read process list, Extract instance string if specified inside - parenthesis. Remove dashes/underscores and change to lower case, - then map the name to the correct wrapper name - - @param config METplusConfig object to read PROCESS_LIST value - @returns list of tuple containing process name and instance identifier - (None if no instance was set) - """ - # get list of processes - process_list = getlist(config.getstr('config', 'PROCESS_LIST')) - - out_process_list = [] - # for each item remove dashes, underscores, and cast to lower-case - for process in process_list: - # if instance is specified, extract the text inside parenthesis - match = re.match(r'(.*)\((.*)\)', process) - if match: - instance = match.group(2) - process_name = match.group(1) - else: - instance = None - process_name = process - - wrapper_name = get_wrapper_name(process_name) - if wrapper_name is None: - config.logger.warning(f"PROCESS_LIST item {process_name} " - "may be invalid.") - wrapper_name = process_name - - # if MakePlots is in process list, remove it because - # it will be called directly from StatAnalysis - if wrapper_name == 'MakePlots': - continue - - out_process_list.append((wrapper_name, instance)) - - return out_process_list - -# minutes -def shift_time(time_str, shift): - """ Adjust time by shift hours. Format is %Y%m%d%H%M%S - Args: - @param time_str: Start time in %Y%m%d%H%M%S - @param shift: Amount to adjust time in hours - Returns: - New time in format %Y%m%d%H%M%S - """ - return (datetime.datetime.strptime(time_str, "%Y%m%d%H%M%S") + - datetime.timedelta(hours=shift)).strftime("%Y%m%d%H%M%S") - -def shift_time_minutes(time_str, shift): - """ Adjust time by shift minutes. Format is %Y%m%d%H%M%S - Args: - @param time_str: Start time in %Y%m%d%H%M%S - @param shift: Amount to adjust time in minutes - Returns: - New time in format %Y%m%d%H%M%S - """ - return (datetime.datetime.strptime(time_str, "%Y%m%d%H%M%S") + - datetime.timedelta(minutes=shift)).strftime("%Y%m%d%H%M%S") - def shift_time_seconds(time_str, shift): """ Adjust time by shift seconds. Format is %Y%m%d%H%M%S Args: @@ -1903,298 +1183,6 @@ def write_list_to_file(filename, output_list): for line in output_list: f.write(f"{line}\n") -def validate_configuration_variables(config, force_check=False): - - all_sed_cmds = [] - # check for deprecated config items and warn user to remove/replace them - deprecated_isOK, sed_cmds = check_for_deprecated_config(config) - all_sed_cmds.extend(sed_cmds) - - # check for deprecated env vars in MET config files and warn user to remove/replace them - deprecatedMET_isOK, sed_cmds = check_for_deprecated_met_config(config) - all_sed_cmds.extend(sed_cmds) - - # validate configuration variables - field_isOK, sed_cmds = validate_field_info_configs(config, force_check) - all_sed_cmds.extend(sed_cmds) - - # check that OUTPUT_BASE is not set to the exact same value as INPUT_BASE - inoutbase_isOK = True - input_real_path = os.path.realpath(config.getdir_nocheck('INPUT_BASE', '')) - output_real_path = os.path.realpath(config.getdir('OUTPUT_BASE')) - if input_real_path == output_real_path: - config.logger.error(f"INPUT_BASE AND OUTPUT_BASE are set to the exact same path: {input_real_path}") - config.logger.error("Please change one of these paths to avoid risk of losing input data") - inoutbase_isOK = False - - check_user_environment(config) - - return deprecated_isOK, field_isOK, inoutbase_isOK, deprecatedMET_isOK, all_sed_cmds - -def skip_field_info_validation(config): - """!Check config to see if having corresponding FCST/OBS variables is necessary. If process list only - contains reformatter wrappers, don't validate field info. Also, if MTD is in the process list and - it is configured to only process either FCST or OBS, validation is unnecessary.""" - - reformatters = ['PCPCombine', 'RegridDataPlane'] - process_list = [item[0] for item in get_process_list(config)] - - # if running MTD in single mode, you don't need matching FCST/OBS - if 'MTD' in process_list and config.getbool('config', 'MTD_SINGLE_RUN'): - return True - - # if running any app other than the reformatters, you need matching FCST/OBS, so don't skip - if [item for item in process_list if item not in reformatters]: - return False - - return True - -def find_indices_in_config_section(regex, config, sec='config', - index_index=1, id_index=None): - """! Use regular expression to get all config variables that match and - are set in the user's configuration. This is used to handle config - variables that have multiple indices, i.e. FCST_VAR1_NAME, FCST_VAR2_NAME, - etc. - - @param regex regular expression to use to find variables - @param config METplusConfig object to search - @param sec (optional) config file section to search. Defaults to config - @param index_index 1 based number that is the regex match index for the - index number (default is 1) - @param id_index 1 based number that is the regex match index for the - identifier. Defaults to None which does not extract an indentifier - - number and the first match is used as an identifier - @returns dictionary where keys are the index number and the value is a - list of identifiers (if noID=True) or a list containing None - """ - # regex expression must have 2 () items and the 2nd item must be the index - all_conf = config.keys(sec) - indices = {} - regex = re.compile(regex) - for conf in all_conf: - result = regex.match(conf) - if result is not None: - index = result.group(index_index) - if id_index: - identifier = result.group(id_index) - else: - identifier = None - - if index not in indices: - indices[index] = [identifier] - else: - indices[index].append(identifier) - - return indices - -def is_var_item_valid(item_list, index, ext, config): - """!Given a list of data types (FCST, OBS, ENS, or BOTH) check if the - combination is valid. - If BOTH is found, FCST and OBS should not be found. - If FCST or OBS is found, the other must also be found. - @param item_list list of data types that were found for a given index - @param index number following _VAR in the variable name - @param ext extension to check, i.e. NAME, LEVELS, THRESH, or OPTIONS - @param config METplusConfig instance - @returns tuple containing boolean if var item is valid, list of error - messages and list of sed commands to help the user update their old - configuration files - """ - - full_ext = f"_VAR{index}_{ext}" - msg = [] - sed_cmds = [] - if 'BOTH' in item_list and ('FCST' in item_list or 'OBS' in item_list): - - msg.append(f"Cannot set FCST{full_ext} or OBS{full_ext} if BOTH{full_ext} is set.") - elif ext == 'THRESH': - # allow thresholds unless BOTH and (FCST or OBS) are set - pass - - elif 'FCST' in item_list and 'OBS' not in item_list: - # if FCST level has 1 item and OBS name is a python embedding script, - # don't report error - level_list = getlist(config.getraw('config', - f'FCST_VAR{index}_LEVELS', - '')) - other_name = config.getraw('config', f'OBS_VAR{index}_NAME', '') - skip_error_for_py_embed = ext == 'LEVELS' and is_python_script(other_name) and len(level_list) == 1 - # do not report error for OPTIONS since it isn't required to be the same length - if ext not in ['OPTIONS'] and not skip_error_for_py_embed: - msg.append(f"If FCST{full_ext} is set, you must either set OBS{full_ext} or " - f"change FCST{full_ext} to BOTH{full_ext}") - - config_files = config.getstr('config', 'CONFIG_INPUT', '').split(',') - for config_file in config_files: - sed_cmds.append(f"sed -i 's|^FCST{full_ext}|BOTH{full_ext}|g' {config_file}") - sed_cmds.append(f"sed -i 's|{{FCST{full_ext}}}|{{BOTH{full_ext}}}|g' {config_file}") - - elif 'OBS' in item_list and 'FCST' not in item_list: - # if OBS level has 1 item and FCST name is a python embedding script, - # don't report error - level_list = getlist(config.getraw('config', - f'OBS_VAR{index}_LEVELS', - '')) - other_name = config.getraw('config', f'FCST_VAR{index}_NAME', '') - skip_error_for_py_embed = ext == 'LEVELS' and is_python_script(other_name) and len(level_list) == 1 - - if ext not in ['OPTIONS'] and not skip_error_for_py_embed: - msg.append(f"If OBS{full_ext} is set, you must either set FCST{full_ext} or " - f"change OBS{full_ext} to BOTH{full_ext}") - - config_files = config.getstr('config', 'CONFIG_INPUT', '').split(',') - for config_file in config_files: - sed_cmds.append(f"sed -i 's|^OBS{full_ext}|BOTH{full_ext}|g' {config_file}") - sed_cmds.append(f"sed -i 's|{{OBS{full_ext}}}|{{BOTH{full_ext}}}|g' {config_file}") - - return not bool(msg), msg, sed_cmds - -def validate_field_info_configs(config, force_check=False): - """!Verify that config variables with _VAR_ in them are valid. Returns True if all are valid. - Returns False if any items are invalid""" - - variable_extensions = ['NAME', 'LEVELS', 'THRESH', 'OPTIONS'] - all_good = True, [] - - if skip_field_info_validation(config) and not force_check: - return True, [] - - # keep track of all sed commands to replace config variable names - all_sed_cmds = [] - - for ext in variable_extensions: - # find all _VAR_ keys in the conf files - data_types_and_indices = find_indices_in_config_section(r"(\w+)_VAR(\d+)_"+ext, - config, - index_index=2, - id_index=1) - - # if BOTH_VAR_ is used, set FCST and OBS to the same value - # if FCST or OBS is used, the other must be present as well - # if BOTH and either FCST or OBS are set, report an error - # get other data type - for index, data_type_list in data_types_and_indices.items(): - - is_valid, err_msgs, sed_cmds = is_var_item_valid(data_type_list, index, ext, config) - if not is_valid: - for err_msg in err_msgs: - config.logger.error(err_msg) - all_sed_cmds.extend(sed_cmds) - all_good = False - - # make sure FCST and OBS have the same number of levels if coming from separate variables - elif ext == 'LEVELS' and all(item in ['FCST', 'OBS'] for item in data_type_list): - fcst_levels = getlist(config.getraw('config', f"FCST_VAR{index}_LEVELS", '')) - - # add empty string if no levels are found because python embedding items do not need - # to include a level, but the other item may have a level and the numbers need to match - if not fcst_levels: - fcst_levels.append('') - - obs_levels = getlist(config.getraw('config', f"OBS_VAR{index}_LEVELS", '')) - if not obs_levels: - obs_levels.append('') - - if len(fcst_levels) != len(obs_levels): - config.logger.error(f"FCST_VAR{index}_LEVELS and OBS_VAR{index}_LEVELS do not have " - "the same number of elements") - all_good = False - - return all_good, all_sed_cmds - -def get_field_search_prefixes(data_type, met_tool=None): - """! Get list of prefixes to search for field variables. - - @param data_type type of field to search for, i.e. FCST, OBS, ENS, etc. - Check for BOTH_ variables first only if data type is FCST or OBS - @param met_tool name of tool to search for variable or None if looking - for generic field info - @returns list of prefixes to search, i.e. [BOTH_, FCST_] or - [ENS_] or [BOTH_GRID_STAT_, OBS_GRID_STAT_] - """ - search_prefixes = [] - var_strings = [] - - # if met tool name is set, prioritize - # wrapper-specific configs before generic configs - if met_tool: - var_strings.append(f'{met_tool.upper()}_') - - var_strings.append('') - - for var_string in var_strings: - search_prefixes.append(f"{data_type}_{var_string}") - - # if looking for FCST or OBS, also check for BOTH prefix - if data_type in ['FCST', 'OBS']: - search_prefixes.append(f"BOTH_{var_string}") - - return search_prefixes - -def get_field_config_variables(config, index, search_prefixes): - """! Search for variables that are set in the config that correspond to - the fields requested. Some field info items have - synonyms that can be used if the typical name is not set. This is used - in RegridDataPlane wrapper. - - @param config METplusConfig object to search - @param index of field (VAR) to find - @param search_prefixes list of valid prefixes to search for variables - in the config, i.e. FCST_VAR1_ or OBS_GRID_STAT_VAR2_ - @returns dictionary containing a config variable name to be used for - each field info value. If a valid config variable was not set for a - field info value, the value for that key will be set to None. - """ - # list of field info variables to find from config - # used as keys for dictionaries - field_info_items = ['name', - 'levels', - 'thresh', - 'options', - 'output_names', - ] - - field_configs = {} - search_suffixes = {} - - # initialize field configs dictionary values to None - # initialize dictionary of valid suffixes to search for with - # the capitalized version of field info name - for field_info_item in field_info_items: - field_configs[field_info_item] = None - search_suffixes[field_info_item] = [field_info_item.upper()] - - # add alternate suffixes for config variable names to attempt - search_suffixes['name'].append('INPUT_FIELD_NAME') - search_suffixes['name'].append('FIELD_NAME') - search_suffixes['levels'].append('INPUT_LEVEL') - search_suffixes['levels'].append('FIELD_LEVEL') - search_suffixes['output_names'].append('OUTPUT_FIELD_NAME') - search_suffixes['output_names'].append('FIELD_NAME') - - # look through field config keys and obtain highest priority - # variable name for each field config - for search_var, suffixes in search_suffixes.items(): - for prefix in search_prefixes: - - found = False - for suffix in suffixes: - var_name = f"{prefix}VAR{index}_{suffix}" - # if variable is found in config, - # get the value and break out of suffix loop - if config.has_option('config', var_name): - field_configs[search_var] = config.getraw('config', - var_name) - found = True - break - - # if config variable was found, break out of prefix loop - if found: - break - - return field_configs - def format_var_items(field_configs, time_info=None): """! Substitute time information into field information and format values. @@ -2281,188 +1269,6 @@ def format_var_items(field_configs, time_info=None): return var_items -def find_var_name_indices(config, data_types, met_tool=None): - data_type_regex = f"{'|'.join(data_types)}" - - # if data_types includes FCST or OBS, also search for BOTH - if any([item for item in ['FCST', 'OBS'] if item in data_types]): - data_type_regex += '|BOTH' - - regex_string = f"({data_type_regex})" - - # if MET tool is specified, get tool specific items - if met_tool: - regex_string += f"_{met_tool.upper()}" - - regex_string += r"_VAR(\d+)_(NAME|INPUT_FIELD_NAME|FIELD_NAME)" - - # find all _VAR_NAME keys in the conf files - return find_indices_in_config_section(regex_string, - config, - index_index=2, - id_index=1) - -def parse_var_list(config, time_info=None, data_type=None, met_tool=None, - levels_as_list=False): - """ read conf items and populate list of dictionaries containing - information about each variable to be compared - - @param config: METplusConfig object - @param time_info: time object for string sub, optional - @param data_type: data type to find. Can be FCST, OBS, or ENS. - If not set, get FCST/OBS/BOTH - @param met_tool: optional name of MET tool to look for wrapper - specific var items - @param levels_as_list If true, store levels and output names as - a list instead of creating a field info dict for each name/level - @returns list of dictionaries with variable information - """ - - # validate configs again in case wrapper is not running from run_metplus - # this does not need to be done if parsing a specific data type, - # i.e. ENS or FCST - if data_type is None: - if not validate_field_info_configs(config)[0]: - return [] - elif data_type == 'BOTH': - config.logger.error("Cannot request BOTH explicitly in parse_var_list") - return [] - - # var_list is a list containing an list of dictionaries - var_list = [] - - # if specific data type is requested, only get that type - if data_type: - data_types = [data_type] - # otherwise get both FCST and OBS - else: - data_types = ['FCST', 'OBS'] - - # get indices of VAR items for data type and/or met tool - indices = [] - if met_tool: - indices = find_var_name_indices(config, data_types, met_tool).keys() - if not indices: - indices = find_var_name_indices(config, data_types).keys() - - # get config name prefixes for each data type to find - dt_search_prefixes = {} - for current_type in data_types: - # get list of variable prefixes to search - prefixes = get_field_search_prefixes(current_type, met_tool) - dt_search_prefixes[current_type] = prefixes - - # loop over all possible variables and add them to list - for index in indices: - field_info_list = [] - for current_type in data_types: - # get dictionary of existing config variables to use - search_prefixes = dt_search_prefixes[current_type] - field_configs = get_field_config_variables(config, - index, - search_prefixes) - - field_info = format_var_items(field_configs, time_info) - if not isinstance(field_info, dict): - config.logger.error(f'Could not process {current_type}_' - f'VAR{index} variables: {field_info}') - continue - - field_info['data_type'] = current_type.lower() - field_info_list.append(field_info) - - # check that all fields types were found - if not field_info_list or len(data_types) != len(field_info_list): - continue - - # check if number of levels for each field type matches - n_levels = len(field_info_list[0]['levels']) - if len(data_types) > 1: - if (n_levels != len(field_info_list[1]['levels'])): - continue - - # if requested, put all field levels in a single item - if levels_as_list: - var_dict = {} - for field_info in field_info_list: - current_type = field_info.get('data_type') - var_dict[f"{current_type}_name"] = field_info.get('name') - var_dict[f"{current_type}_level"] = field_info.get('levels') - var_dict[f"{current_type}_thresh"] = field_info.get('thresh') - var_dict[f"{current_type}_extra"] = field_info.get('extra') - var_dict[f"{current_type}_output_name"] = field_info.get('output_names') - - var_dict['index'] = index - var_list.append(var_dict) - continue - - # loop over levels and add all values to output dictionary - for level_index in range(n_levels): - var_dict = {} - - # get level values to use for string substitution in name - # used for python embedding calls that read the level value - sub_info = {} - for field_info in field_info_list: - dt_level = f"{field_info.get('data_type')}_level" - sub_info[dt_level] = field_info.get('levels')[level_index] - - for field_info in field_info_list: - current_type = field_info.get('data_type') - name = field_info.get('name') - level = field_info.get('levels')[level_index] - thresh = field_info.get('thresh') - extra = field_info.get('extra') - output_name = field_info.get('output_names')[level_index] - - # substitute level in name if filename template is specified - subbed_name = do_string_sub(name, - skip_missing_tags=True, - **sub_info) - - var_dict[f"{current_type}_name"] = subbed_name - var_dict[f"{current_type}_level"] = level - var_dict[f"{current_type}_thresh"] = thresh - var_dict[f"{current_type}_extra"] = extra - var_dict[f"{current_type}_output_name"] = output_name - - var_dict['index'] = index - var_list.append(var_dict) - - # extra debugging information used for developer debugging only - ''' - for v in var_list: - config.logger.debug(f"VAR{v['index']}:") - if 'fcst_name' in v.keys(): - config.logger.debug(" fcst_name:"+v['fcst_name']) - config.logger.debug(" fcst_level:"+v['fcst_level']) - if 'fcst_thresh' in v.keys(): - config.logger.debug(" fcst_thresh:"+str(v['fcst_thresh'])) - if 'fcst_extra' in v.keys(): - config.logger.debug(" fcst_extra:"+v['fcst_extra']) - if 'fcst_output_name' in v.keys(): - config.logger.debug(" fcst_output_name:"+v['fcst_output_name']) - if 'obs_name' in v.keys(): - config.logger.debug(" obs_name:"+v['obs_name']) - config.logger.debug(" obs_level:"+v['obs_level']) - if 'obs_thresh' in v.keys(): - config.logger.debug(" obs_thresh:"+str(v['obs_thresh'])) - if 'obs_extra' in v.keys(): - config.logger.debug(" obs_extra:"+v['obs_extra']) - if 'obs_output_name' in v.keys(): - config.logger.debug(" obs_output_name:"+v['obs_output_name']) - if 'ens_name' in v.keys(): - config.logger.debug(" ens_name:"+v['ens_name']) - config.logger.debug(" ens_level:"+v['ens_level']) - if 'ens_thresh' in v.keys(): - config.logger.debug(" ens_thresh:"+str(v['ens_thresh'])) - if 'ens_extra' in v.keys(): - config.logger.debug(" ens_extra:"+v['ens_extra']) - if 'ens_output_name' in v.keys(): - config.logger.debug(" ens_output_name:"+v['ens_output_name']) - ''' - return sorted(var_list, key=lambda x: x['index']) - def sub_var_info(var_info, time_info): if not var_info: return {} @@ -2652,31 +1458,6 @@ def get_filetype(filepath, logger=None): #else: # return None - - -def get_time_from_file(filepath, template, logger=None): - """! Extract time information from path using the filename template - Args: - @param filepath path to examine - @param template filename template to use to extract time information - @returns time_info dictionary with time information if successful, None if not - """ - if os.path.isdir(filepath): - return None - - out = parse_template(template, filepath, logger) - if out is not None: - return out - - # check to see if zip extension ends file path, try again without extension - for ext in VALID_EXTENSIONS: - if filepath.endswith(ext): - out = parse_template(template, filepath[:-len(ext)], logger) - if out is not None: - return out - - return None - def preprocess_file(filename, data_type, config, allow_dir=False): """ Decompress gzip, bzip, or zip files or convert Gempak files to NetCDF Args: @@ -2707,7 +1488,7 @@ def preprocess_file(filename, data_type, config, allow_dir=False): # the function will handle files passed to it with an # extension the same way as files passed # without an extension but the compressed equivalent exists - for ext in VALID_EXTENSIONS: + for ext in COMPRESSION_EXTENSIONS: if filename.endswith(ext): return preprocess_file(filename[:-len(ext)], data_type, config) # if extension is grd (Gempak), then look in staging dir for nc file @@ -2751,8 +1532,8 @@ def preprocess_file(filename, data_type, config, allow_dir=False): return outpath # Create staging area directory only if file has compression extension - valid_extensions = ['gz', 'bz2', 'zip'] - if any([os.path.isfile(f'{filename}.{ext}') for ext in valid_extensions]): + if any([os.path.isfile(f'{filename}{ext}') + for ext in COMPRESSION_EXTENSIONS]): outdir = os.path.dirname(outpath) if not os.path.exists(outdir): os.makedirs(outdir, mode=0o0775) @@ -2812,18 +1593,6 @@ def is_python_script(name): return False -def check_user_environment(config): - """!Check if any environment variables set in [user_env_vars] are already set in - the user's environment. Warn them that it will be overwritten from the conf if it is""" - if not config.has_section('user_env_vars'): - return - - for env_var in config.keys('user_env_vars'): - if env_var in os.environ: - msg = '{} is already set in the environment. '.format(env_var) +\ - 'Overwriting from conf file' - config.logger.warning(msg) - def expand_int_string_to_list(int_string): """! Expand string into a list of integer values. Items are separated by commas. Items that are formatted X-Y will be expanded into each number diff --git a/metplus/util/string_template_substitution.py b/metplus/util/string_template_substitution.py index 05dd19fc73..e4085ebc50 100644 --- a/metplus/util/string_template_substitution.py +++ b/metplus/util/string_template_substitution.py @@ -12,11 +12,13 @@ """ +import os import re import datetime from dateutil.relativedelta import relativedelta from . import time_util +from .constants import * TEMPLATE_IDENTIFIER_BEGIN = "{" TEMPLATE_IDENTIFIER_END = "}" @@ -828,10 +830,26 @@ def add_offset_matches_to_output_dict(match_dict, output_dict): output_dict['offset_hours'] = offset -def extract_lead(template, filename): - new_template = template - new_template = new_template.replace('/', '\/').replace('.', '\.') - match_tags = re.findall(r'{(.*?)}', new_template) - for match_tag in match_tags: - if match_tag.split('?') != 'lead': - new_template = new_template.replace('{' + match_tag + '}', '.*') +def get_time_from_file(filepath, template, logger=None): + """! Extract time information from path using the filename template + + @param filepath path to examine + @param template filename template to use to extract time information + @param logger optional logging object + @returns dictionary with time information if successful, None if not + """ + if os.path.isdir(filepath): + return None + + out = parse_template(template, filepath, logger) + if out is not None: + return out + + # check to see if zip extension ends file path, try again without extension + for ext in COMPRESSION_EXTENSIONS: + if filepath.endswith(ext): + out = parse_template(template, filepath[:-len(ext)], logger) + if out is not None: + return out + + return None diff --git a/metplus/wrappers/ascii2nc_wrapper.py b/metplus/wrappers/ascii2nc_wrapper.py index 01e675833f..e68d78ac23 100755 --- a/metplus/wrappers/ascii2nc_wrapper.py +++ b/metplus/wrappers/ascii2nc_wrapper.py @@ -76,11 +76,8 @@ def create_c_dict(self): ) # MET config variables - self.handle_time_summary_legacy(c_dict, - ['TIME_SUMMARY_GRIB_CODES', - 'TIME_SUMMARY_VAR_NAMES', - 'TIME_SUMMARY_TYPES'] - ) + self.handle_time_summary_dict() + self.handle_time_summary_legacy() # handle file window variables for edge in ['BEGIN', 'END']: @@ -100,34 +97,115 @@ def create_c_dict(self): return c_dict + def handle_time_summary_legacy(self): + """! Read METplusConfig variables for the MET config time_summary + dictionary and format values into environment variable + METPLUS_TIME_SUMMARY_DICT as well as other environment variables + that contain individuals items of the time_summary dictionary + that were referenced in wrapped MET config files prior to METplus 4.0. + Developer note: If we discontinue support for legacy wrapped MET + config files + + @param c_dict dictionary to store time_summary item values + @param remove_bracket_list (optional) list of items that need the + square brackets around the value removed because the legacy (pre 4.0) + wrapped MET config includes square braces around the environment + variable. + """ + # handle legacy time summary variables + self.add_met_config(name='', + data_type='bool', + env_var_name='TIME_SUMMARY_FLAG', + metplus_configs=['ASCII2NC_TIME_SUMMARY_FLAG']) + + self.add_met_config(name='', + data_type='bool', + env_var_name='TIME_SUMMARY_RAW_DATA', + metplus_configs=['ASCII2NC_TIME_SUMMARY_RAW_DATA']) + + self.add_met_config(name='', + data_type='string', + env_var_name='TIME_SUMMARY_BEG', + metplus_configs=['ASCII2NC_TIME_SUMMARY_BEG']) + + self.add_met_config(name='', + data_type='string', + env_var_name='TIME_SUMMARY_END', + metplus_configs=['ASCII2NC_TIME_SUMMARY_END']) + + self.add_met_config(name='', + data_type='int', + env_var_name='TIME_SUMMARY_STEP', + metplus_configs=['ASCII2NC_TIME_SUMMARY_STEP']) + + self.add_met_config(name='', + data_type='string', + env_var_name='TIME_SUMMARY_WIDTH', + metplus_configs=['ASCII2NC_TIME_SUMMARY_WIDTH'], + extra_args={'remove_quotes': True}) + + self.add_met_config(name='', + data_type='list', + env_var_name='TIME_SUMMARY_GRIB_CODES', + metplus_configs=['ASCII2NC_TIME_SUMMARY_GRIB_CODES', + 'ASCII2NC_TIME_SUMMARY_GRIB_CODE'], + extra_args={'remove_quotes': True, + 'allow_empty': True}) + + self.add_met_config(name='', + data_type='list', + env_var_name='TIME_SUMMARY_VAR_NAMES', + metplus_configs=['ASCII2NC_TIME_SUMMARY_OBS_VAR', + 'ASCII2NC_TIME_SUMMARY_VAR_NAMES'], + extra_args={'allow_empty': True}) + + self.add_met_config(name='', + data_type='list', + env_var_name='TIME_SUMMARY_TYPES', + metplus_configs=['ASCII2NC_TIME_SUMMARY_TYPE', + 'ASCII2NC_TIME_SUMMARY_TYPES'], + extra_args={'allow_empty': True}) + + self.add_met_config(name='', + data_type='int', + env_var_name='TIME_SUMMARY_VALID_FREQ', + metplus_configs=['ASCII2NC_TIME_SUMMARY_VLD_FREQ', + 'ASCII2NC_TIME_SUMMARY_VALID_FREQ']) + + self.add_met_config(name='', + data_type='float', + env_var_name='TIME_SUMMARY_VALID_THRESH', + metplus_configs=['ASCII2NC_TIME_SUMMARY_VLD_THRESH', + 'ASCII2NC_TIME_SUMMARY_VALID_THRESH']) + def set_environment_variables(self, time_info): """!Set environment variables that will be read by the MET config file. Reformat as needed. Print list of variables that were set and their values. Args: @param time_info dictionary containing timing info from current run""" - # set environment variables needed for MET application + # set environment variables needed for legacy MET config file self.add_env_var('TIME_SUMMARY_FLAG', - self.c_dict['TIME_SUMMARY_FLAG']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_FLAG', '')) self.add_env_var('TIME_SUMMARY_RAW_DATA', - self.c_dict['TIME_SUMMARY_RAW_DATA']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_RAW_DATA', '')) self.add_env_var('TIME_SUMMARY_BEG', - self.c_dict['TIME_SUMMARY_BEG']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_BEG', '')) self.add_env_var('TIME_SUMMARY_END', - self.c_dict['TIME_SUMMARY_END']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_END', '')) self.add_env_var('TIME_SUMMARY_STEP', - self.c_dict['TIME_SUMMARY_STEP']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_STEP', '')) self.add_env_var('TIME_SUMMARY_WIDTH', - self.c_dict['TIME_SUMMARY_WIDTH']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_WIDTH', '')) self.add_env_var('TIME_SUMMARY_GRIB_CODES', - self.c_dict['TIME_SUMMARY_GRIB_CODES']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_GRIB_CODES', '').strip('[]')) self.add_env_var('TIME_SUMMARY_VAR_NAMES', - self.c_dict['TIME_SUMMARY_VAR_NAMES']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_VAR_NAMES', '').strip('[]')) self.add_env_var('TIME_SUMMARY_TYPES', - self.c_dict['TIME_SUMMARY_TYPES']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_TYPES', '').strip('[]')) self.add_env_var('TIME_SUMMARY_VALID_FREQ', - self.c_dict['TIME_SUMMARY_VALID_FREQ']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_VALID_FREQ', '')) self.add_env_var('TIME_SUMMARY_VALID_THRESH', - self.c_dict['TIME_SUMMARY_VALID_THRESH']) + self.env_var_dict.get('METPLUS_TIME_SUMMARY_VALID_THRESH', '')) # set user environment variables super().set_environment_variables(time_info) diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 22b1052721..d62707317b 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -20,9 +20,13 @@ from .command_runner import CommandRunner from ..util import met_util as util from ..util import do_string_sub, ti_calculate, get_seconds_from_string +from ..util import get_time_from_file from ..util import config_metplus -from ..util import METConfigInfo as met_config +from ..util import METConfig from ..util import MISSING_DATA_VALUE +from ..util import get_custom_string_list +from ..util import get_wrapped_met_config_file, add_met_config_item, format_met_config +from ..util.met_config import add_met_config_dict # pylint:disable=pointless-string-statement '''!@namespace CommandBuilder @@ -162,7 +166,6 @@ def check_for_unused_env_vars(self): "is not utilized in MET config file: " f"{config_file}") - def create_c_dict(self): c_dict = dict() # set skip if output exists to False for all wrappers @@ -176,8 +179,8 @@ def create_c_dict(self): if hasattr(self, 'app_name'): app_name = self.app_name - c_dict['CUSTOM_LOOP_LIST'] = util.get_custom_string_list(self.config, - app_name) + c_dict['CUSTOM_LOOP_LIST'] = get_custom_string_list(self.config, + app_name) c_dict['SKIP_TIMES'] = util.get_skip_times(self.config, app_name) @@ -280,20 +283,6 @@ def set_user_environment(self, time_info): **time_info) self.add_env_var(env_var, env_var_value) - @staticmethod - def format_regrid_to_grid(to_grid): - to_grid = to_grid.strip('"') - if not to_grid: - to_grid = 'NONE' - - # if NONE, FCST, or OBS force uppercase, otherwise add quotes - if to_grid.upper() in ['NONE', 'FCST', 'OBS']: - to_grid = to_grid.upper() - else: - to_grid = f'"{to_grid}"' - - return to_grid - def print_all_envs(self, print_copyable=True, print_each_item=True): """! Create list of log messages that output all environment variables that were set by this wrapper. @@ -319,7 +308,7 @@ def print_all_envs(self, print_copyable=True, print_each_item=True): def handle_window_once(self, input_list, default_val=0): """! Check and set window dictionary variables like - OBS_WINDOW_BEG or FCST_FILE_WINDW_END + OBS_WINDOW_BEG or FCST_FILE_WINDOW_END @param input_list list of config keys to check for value @param default_val value to use if none of the input keys found @@ -330,7 +319,7 @@ def handle_window_once(self, input_list, default_val=0): return default_val - def handle_obs_window_variables(self, c_dict): + def handle_obs_window_legacy(self, c_dict): """! Handle obs window config variables like OBS__WINDOW_[BEGIN/END]. Set c_dict values for begin and end to handle old method of setting env vars in MET config files, i.e. @@ -343,8 +332,6 @@ def handle_obs_window_variables(self, c_dict): ('END', 5400)] app = self.app_name.upper() - keys = [] - tmp_dict = {} for edge, default_val in edges: input_list = [f'OBS_{app}_WINDOW_{edge}', f'{app}_OBS_WINDOW_{edge}', @@ -353,16 +340,6 @@ def handle_obs_window_variables(self, c_dict): output_key = f'OBS_WINDOW_{edge}' value = self.handle_window_once(input_list, default_val) c_dict[output_key] = value - if edge == 'BEGIN': - edge = 'BEG' - tmp_dict[output_key] = f'{edge.lower()} = {value};' - # if something other than the default is used, add output key - # to the list of items to add to the env_var_dict value - if value != default_val: - keys.append(output_key) - - window_str = self.format_met_config_dict(tmp_dict, 'obs_window', keys) - self.env_var_dict['METPLUS_OBS_WINDOW_DICT'] = window_str def handle_file_window_variables(self, c_dict, dtypes=['FCST', 'OBS']): """! Handle all window config variables like @@ -795,7 +772,7 @@ def find_file_in_window(self, level, data_type, time_info, mandatory=True, # remove input data directory to get relative path rel_path = fullpath.replace(f'{data_dir}/', "") # extract time information from relative path using template - file_time_info = util.get_time_from_file(rel_path, template, self.logger) + file_time_info = get_time_from_file(rel_path, template, self.logger) if file_time_info is None: continue @@ -1448,297 +1425,6 @@ def set_time_dict_for_single_runtime(self): return time_info - def _get_config_or_default(self, mp_config_name, get_function, - default=None): - conf_value = '' - - # if no possible METplus config variables are not set - if mp_config_name is None: - # if no default defined, return without doing anything - if not default: - return None - - # if METplus config variable is set, read the value - else: - conf_value = get_function('config', - mp_config_name, - '') - - # if variable is not set and there is a default defined, set default - if not conf_value and default: - conf_value = default - - return conf_value - - def set_met_config_list(self, c_dict, mp_config, met_config_name, - c_dict_key=None, **kwargs): - """! Get list from METplus configuration file and format it to be passed - into a MET configuration file. Set c_dict item with formatted string. - Args: - @param c_dict configuration dictionary to set - @param mp_config_name METplus configuration variable name. Assumed to be - in the [config] section. Value can be a comma-separated list of items. - @param met_config name of MET configuration variable to set. Also used - to determine the key in c_dict to set (upper-case) - @param c_dict_key optional argument to specify c_dict key to store result. If - set to None (default) then use upper-case of met_config_name - @param allow_empty if True, if METplus config variable is set - but is an empty string, then set the c_dict value to an empty - list. If False, behavior is the same as when the variable is - not set at all, which is to not set anything for the c_dict - value - @param remove_quotes if True, output value without quotes. - Default value is False - @param default (Optional) if set, use this value as default - if config is not set - """ - mp_config_name = self.get_mp_config_name(mp_config) - conf_value = self._get_config_or_default( - mp_config_name, - get_function=self.config.getraw, - default=kwargs.get('default') - ) - if conf_value is None: - return - - # convert value from config to a list - conf_values = util.getlist(conf_value) - if conf_values or kwargs.get('allow_empty', False): - out_values = [] - for conf_value in conf_values: - remove_quotes = kwargs.get('remove_quotes', False) - # if not removing quotes, escape any quotes found in list items - if not remove_quotes: - conf_value = conf_value.replace('"', '\\"') - - conf_value = util.remove_quotes(conf_value) - if not remove_quotes: - conf_value = f'"{conf_value}"' - - out_values.append(conf_value) - out_value = f"[{', '.join(out_values)}]" - - if not c_dict_key: - c_key = met_config_name.upper() - else: - c_key = c_dict_key - - conf_value = f'{met_config_name} = {out_value};' - c_dict[c_key] = conf_value - - def set_met_config_string(self, c_dict, mp_config, met_config_name, - c_dict_key=None, **kwargs): - """! Get string from METplus configuration file and format it to be passed - into a MET configuration file. Set c_dict item with formatted string. - - @param c_dict configuration dictionary to set - @param mp_config METplus configuration variable name. Assumed to be - in the [config] section. Value can be a comma-separated list of items. - @param met_config_name name of MET configuration variable to set. Also used - to determine the key in c_dict to set (upper-case) - @param c_dict_key optional argument to specify c_dict key to store result. If - set to None (default) then use upper-case of met_config_name - @param remove_quotes if True, output value without quotes. - Default value is False - @param to_grid if True, format to_grid value - Default value is False - @param default (Optional) if set, use this value as default - if config is not set - """ - mp_config_name = self.get_mp_config_name(mp_config) - conf_value = self._get_config_or_default( - mp_config_name, - get_function=self.config.getraw, - default=kwargs.get('default') - ) - if not conf_value: - return - - conf_value = util.remove_quotes(conf_value) - # add quotes back if remote quotes is False - if not kwargs.get('remove_quotes'): - conf_value = f'"{conf_value}"' - - if kwargs.get('uppercase', False): - conf_value = conf_value.upper() - - if kwargs.get('to_grid', False): - conf_value = self.format_regrid_to_grid(conf_value) - - c_key = c_dict_key if c_dict_key else met_config_name.upper() - c_dict[c_key] = f'{met_config_name} = {conf_value};' - - def set_met_config_number(self, c_dict, num_type, mp_config, - met_config_name, c_dict_key=None, **kwargs): - """! Get integer from METplus configuration file and format it to be passed - into a MET configuration file. Set c_dict item with formatted string. - Args: - @param c_dict configuration dictionary to set - @param num_type type of number to get from config. If set to 'int', call - getint function. If not, call getfloat function. - @param mp_config METplus configuration variable name. Assumed to be - in the [config] section. Value can be a comma-separated list of items. - @param met_config_name name of MET configuration variable to set. Also used - to determine the key in c_dict to set (upper-case) if c_dict_key is None - @param c_dict_key optional argument to specify c_dict key to store result. If - set to None (default) then use upper-case of met_config_name - @param default (Optional) if set, use this value as default - if config is not set - """ - mp_config_name = self.get_mp_config_name(mp_config) - if mp_config_name is None: - return - - if num_type == 'int': - conf_value = self.config.getint('config', mp_config_name) - else: - conf_value = self.config.getfloat('config', mp_config_name) - - if conf_value is None: - self.isOK = False - elif conf_value != util.MISSING_DATA_VALUE: - if not c_dict_key: - c_key = met_config_name.upper() - else: - c_key = c_dict_key - - c_dict[c_key] = f"{met_config_name} = {str(conf_value)};" - - def set_met_config_int(self, c_dict, mp_config_name, met_config_name, - c_dict_key=None, **kwargs): - self.set_met_config_number(c_dict, 'int', - mp_config_name, - met_config_name, - c_dict_key=c_dict_key, - **kwargs) - - def set_met_config_float(self, c_dict, mp_config_name, - met_config_name, c_dict_key=None, **kwargs): - self.set_met_config_number(c_dict, 'float', - mp_config_name, - met_config_name, - c_dict_key=c_dict_key, - **kwargs) - - def set_met_config_thresh(self, c_dict, mp_config, met_config_name, - c_dict_key=None, **kwargs): - mp_config_name = self.get_mp_config_name(mp_config) - if mp_config_name is None: - return - - conf_value = self.config.getstr('config', mp_config_name, '') - if conf_value: - if util.get_threshold_via_regex(conf_value) is None: - self.log_error(f"Incorrectly formatted threshold: {mp_config_name}") - return - - if not c_dict_key: - c_key = met_config_name.upper() - else: - c_key = c_dict_key - - c_dict[c_key] = f"{met_config_name} = {str(conf_value)};" - - def set_met_config_bool(self, c_dict, mp_config, met_config_name, - c_dict_key=None, **kwargs): - """! Get boolean from METplus configuration file and format it to be - passed into a MET configuration file. Set c_dict item with boolean - value expressed as a string. - Args: - @param c_dict configuration dictionary to set - @param mp_config METplus configuration variable name. - Assumed to be in the [config] section. - @param met_config_name name of MET configuration variable to - set. Also used to determine the key in c_dict to set - (upper-case) - @param c_dict_key optional argument to specify c_dict key to - store result. If set to None (default) then use upper-case of - met_config_name - @param uppercase If true, set value to TRUE or FALSE - """ - mp_config_name = self.get_mp_config_name(mp_config) - if mp_config_name is None: - return - conf_value = self.config.getbool('config', mp_config_name, '') - if conf_value is None: - self.log_error(f'Invalid boolean value set for {mp_config_name}') - return - - # if not invalid but unset, return without setting c_dict with no error - if conf_value == '': - return - - conf_value = str(conf_value) - if kwargs.get('uppercase', True): - conf_value = conf_value.upper() - - if not c_dict_key: - c_key = met_config_name.upper() - else: - c_key = c_dict_key - - c_dict[c_key] = (f'{met_config_name} = ' - f'{util.remove_quotes(conf_value)};') - - def get_mp_config_name(self, mp_config): - """! Get first name of METplus config variable that is set. - - @param mp_config list of METplus config keys to check. Can also be a - single item - @returns Name of first METplus config name in list that is set in the - METplusConfig object. None if none keys in the list are set. - """ - if not isinstance(mp_config, list): - mp_configs = [mp_config] - else: - mp_configs = mp_config - - for mp_config_name in mp_configs: - if self.config.has_option('config', mp_config_name): - return mp_config_name - - return None - - @staticmethod - def format_met_config(data_type, c_dict, name, keys=None): - """! Return formatted variable named with any if they - are set to a value. If none of the items are set, return empty string - - @param data_type type of value to format - @param c_dict config dictionary to read values from - @param name name of dictionary to create - @param keys list of c_dict keys to use if they are set. If unset (None) - then read all keys from c_dict - @returns MET config formatted dictionary/list - if any items are set, or empty string if not - """ - values = [] - if keys is None: - keys = c_dict.keys() - - for key in keys: - value = c_dict.get(key) - if value: - values.append(str(value)) - - # if none of the keys are set to a value in dict, return empty string - if not values: - return '' - - output = ''.join(values) - # add curly braces if dictionary - if 'dict' in data_type: - output = f"{{{output}}}" - - # add square braces if list - if 'list' in data_type: - output = f"[{output}];" - - # if name is not empty, add variable name and equals sign - if name: - output = f'{name} = {output}' - return output - @staticmethod def format_met_config_dict(c_dict, name, keys=None): """! Return formatted dictionary named with any if they @@ -1751,59 +1437,36 @@ def format_met_config_dict(c_dict, name, keys=None): @returns MET config formatted dictionary if any items are set, or empty string if not """ - return CommandBuilder.format_met_config('dict', c_dict=c_dict, name=name, keys=keys) + return format_met_config('dict', c_dict=c_dict, name=name, keys=keys) def handle_regrid(self, c_dict, set_to_grid=True): - app_name_upper = self.app_name.upper() - - # dictionary to hold regrid values as they are read - tmp_dict = {} - + dict_items = {} if set_to_grid: - conf_value = ( - self.config.getstr('config', - f'{app_name_upper}_REGRID_TO_GRID', '') + dict_items['to_grid'] = ('string', 'to_grid') + + # handle legacy format of to_grid + self.add_met_config( + name='', + data_type='string', + env_var_name='REGRID_TO_GRID', + metplus_configs=[f'{self.app_name.upper()}_REGRID_TO_GRID'], + extra_args={'to_grid': True}, + output_dict=c_dict, ) - # set to_grid without formatting for backwards compatibility - formatted_to_grid = self.format_regrid_to_grid(conf_value) - c_dict['REGRID_TO_GRID'] = formatted_to_grid - - if conf_value: - tmp_dict['REGRID_TO_GRID'] = ( - f"to_grid = {formatted_to_grid};" - ) - - self.set_met_config_string(tmp_dict, - f'{app_name_upper}_REGRID_METHOD', - 'method', - c_dict_key='REGRID_METHOD', - remove_quotes=True) - - self.set_met_config_int(tmp_dict, - f'{app_name_upper}_REGRID_WIDTH', - 'width', - c_dict_key='REGRID_WIDTH') - - self.set_met_config_float(tmp_dict, - f'{app_name_upper}_REGRID_VLD_THRESH', - 'vld_thresh', - c_dict_key='REGRID_VLD_THRESH') - self.set_met_config_string(tmp_dict, - f'{app_name_upper}_REGRID_SHAPE', - 'shape', - c_dict_key='REGRID_SHAPE', - remove_quotes=True) - - regrid_string = self.format_met_config_dict(tmp_dict, - 'regrid', - ['REGRID_TO_GRID', - 'REGRID_METHOD', - 'REGRID_WIDTH', - 'REGRID_VLD_THRESH', - 'REGRID_SHAPE', - ]) - self.env_var_dict['METPLUS_REGRID_DICT'] = regrid_string + # set REGRID_TO_GRID to NONE if unset + regrid_value = c_dict.get('METPLUS_REGRID_TO_GRID', '') + if not regrid_value: + regrid_value = 'NONE' + c_dict['REGRID_TO_GRID'] = regrid_value + if 'METPLUS_REGRID_TO_GRID' in c_dict: + del c_dict['METPLUS_REGRID_TO_GRID'] + + dict_items['method'] = ('string', 'uppercase,remove_quotes') + dict_items['width'] = 'int' + dict_items['vld_thresh'] = 'float' + dict_items['shape'] = ('string', 'uppercase,remove_quotes') + self.add_met_config_dict('regrid', dict_items) def handle_description(self): """! Get description from config. If _DESC is set, use @@ -1859,29 +1522,6 @@ def get_output_prefix(self, time_info=None, set_env_vars=True): return output_prefix - def _parse_extra_args(self, extra): - """! Check string for extra option keywords and set them to True in - dictionary if they are found. Supports 'remove_quotes', 'uppercase' - and 'allow_empty' - - @param extra string to parse for keywords - @returns dictionary with extra args set if found in string - """ - extra_args = {} - if not extra: - return extra_args - - VALID_EXTRAS = ( - 'remove_quotes', - 'uppercase', - 'allow_empty', - 'to_grid', - ) - for extra_option in VALID_EXTRAS: - if extra_option in extra: - extra_args[extra_option] = True - return extra_args - def handle_climo_dict(self): """! Read climo mean/stdev variables with and set env_var_dict appropriately. Handle previous environment variables that are used @@ -1908,7 +1548,7 @@ def handle_climo_dict(self): # make sure _FILE_NAME is set from INPUT_TEMPLATE/DIR if used self.read_climo_file_name(climo_type) - self.handle_met_config_dict(dict_name, items) + self.add_met_config_dict(dict_name, items) # handle deprecated env vars CLIMO_MEAN_FILE and CLIMO_STDEV_FILE # that are used by pre v4.0.0 wrapped MET config files @@ -2021,41 +1661,24 @@ def handle_flags(self, flag_type): if not hasattr(self, f'{flag_type_upper}_FLAGS'): return - tmp_dict = {} - flag_list = [] + flag_info_dict = {} for flag in getattr(self, f'{flag_type_upper}_FLAGS'): - flag_name = f'{flag_type_upper}_FLAG_{flag.upper()}' - flag_list.append(flag_name) - self.set_met_config_string(tmp_dict, - f'{self.app_name.upper()}_{flag_name}', - flag, - c_dict_key=f'{flag_name}', - remove_quotes=True, - uppercase=True) - - flag_fmt = ( - self.format_met_config_dict(tmp_dict, - f'{flag_type_lower}_flag', - flag_list) - ) - self.env_var_dict[f'METPLUS_{flag_type_upper}_FLAG_DICT'] = flag_fmt + flag_info_dict[flag] = ('string', 'remove_quotes,uppercase') + + self.add_met_config_dict(f'{flag_type_lower}_flag', flag_info_dict) def handle_censor_val_and_thresh(self): """! Read {APP_NAME}_CENSOR_[VAL/THRESH] and set METPLUS_CENSOR_[VAL/THRESH] in self.env_var_dict so it can be referenced in a MET config file """ - self.set_met_config_list(self.env_var_dict, - f'{self.app_name.upper()}_CENSOR_THRESH', - 'censor_thresh', - c_dict_key='METPLUS_CENSOR_THRESH', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - f'{self.app_name.upper()}_CENSOR_VAL', - 'censor_val', - c_dict_key='METPLUS_CENSOR_VAL', - remove_quotes=True) + self.add_met_config(name='censor_thresh', + data_type='list', + extra_args={'remove_quotes': True}) + + self.add_met_config(name='censor_val', + data_type='list', + extra_args={'remove_quotes': True}) def get_env_var_value(self, env_var_name, read_dict=None, item_type=None): """! Read env var value, get text after the equals sign and remove the @@ -2086,7 +1709,7 @@ def handle_time_summary_dict(self): METPLUS_TIME_SUMMARY_DICT that is referenced in the wrapped MET config files. """ - self.handle_met_config_dict('time_summary', { + self.add_met_config_dict('time_summary', { 'flag': 'bool', 'raw_data': 'bool', 'beg': 'string', @@ -2102,105 +1725,6 @@ def handle_time_summary_dict(self): 'vld_thresh': ('float', None, None, ['TIME_SUMMARY_VALID_THRESH']), }) - def handle_time_summary_legacy(self, c_dict, remove_bracket_list=None): - """! Read METplusConfig variables for the MET config time_summary - dictionary and format values into environment variable - METPLUS_TIME_SUMMARY_DICT as well as other environment variables - that contain individuals items of the time_summary dictionary - that were referenced in wrapped MET config files prior to METplus 4.0. - Developer note: If we discontinue support for legacy wrapped MET - config files - - @param c_dict dictionary to store time_summary item values - @param remove_bracket_list (optional) list of items that need the - square brackets around the value removed because the legacy (pre 4.0) - wrapped MET config includes square braces around the environment - variable. - """ - tmp_dict = {} - app = self.app_name.upper() - self.set_met_config_bool(tmp_dict, - f'{app}_TIME_SUMMARY_FLAG', - 'flag', - 'TIME_SUMMARY_FLAG') - - self.set_met_config_bool(tmp_dict, - f'{app}_TIME_SUMMARY_RAW_DATA', - 'raw_data', - 'TIME_SUMMARY_RAW_DATA') - - self.set_met_config_string(tmp_dict, - f'{app}_TIME_SUMMARY_BEG', - 'beg', - 'TIME_SUMMARY_BEG') - - self.set_met_config_string(tmp_dict, - f'{app}_TIME_SUMMARY_END', - 'end', - 'TIME_SUMMARY_END') - - self.set_met_config_int(tmp_dict, - f'{app}_TIME_SUMMARY_STEP', - 'step', - 'TIME_SUMMARY_STEP') - - self.set_met_config_string(tmp_dict, - f'{app}_TIME_SUMMARY_WIDTH', - 'width', - 'TIME_SUMMARY_WIDTH', - remove_quotes=True) - - self.set_met_config_list(tmp_dict, - [f'{app}_TIME_SUMMARY_GRIB_CODES', - f'{app}_TIME_SUMMARY_GRIB_CODE'], - 'grib_code', - 'TIME_SUMMARY_GRIB_CODES', - remove_quotes=True, - allow_empty=True) - - self.set_met_config_list(tmp_dict, - [f'{app}_TIME_SUMMARY_OBS_VAR', - f'{app}_TIME_SUMMARY_VAR_NAMES'], - 'obs_var', - 'TIME_SUMMARY_VAR_NAMES', - allow_empty=True) - - self.set_met_config_list(tmp_dict, - [f'{app}_TIME_SUMMARY_TYPE', - f'{app}_TIME_SUMMARY_TYPES'], - 'type', - 'TIME_SUMMARY_TYPES', - allow_empty=True) - - self.set_met_config_int(tmp_dict, - [f'{app}_TIME_SUMMARY_VLD_FREQ', - f'{app}_TIME_SUMMARY_VALID_FREQ'], - 'vld_freq', - 'TIME_SUMMARY_VALID_FREQ') - - self.set_met_config_float(tmp_dict, - [f'{app}_TIME_SUMMARY_VLD_THRESH', - f'{app}_TIME_SUMMARY_VALID_THRESH'], - 'vld_thresh', - 'TIME_SUMMARY_VALID_THRESH') - - time_summary = self.format_met_config_dict(tmp_dict, - 'time_summary', - keys=None) - self.env_var_dict['METPLUS_TIME_SUMMARY_DICT'] = time_summary - - # set c_dict values to support old method of setting env vars - for key, value in tmp_dict.items(): - c_dict[key] = self.get_env_var_value(key, read_dict=tmp_dict) - - # remove brackets [] from lists - if not remove_bracket_list: - return - - for list_value in remove_bracket_list: - if c_dict.get(list_value): - c_dict[list_value] = c_dict[list_value].strip('[]') - def handle_mask(self, single_value=False, get_flags=False): """! Read mask dictionary values and set them into env_var_list @@ -2221,85 +1745,9 @@ def handle_mask(self, single_value=False, get_flags=False): items['grid_flag'] = ('string', 'remove_quotes,uppercase') items['poly_flag'] = ('string', 'remove_quotes,uppercase') - self.handle_met_config_dict('mask', items) + self.add_met_config_dict('mask', items) - def set_met_config_function(self, item_type): - """! Return function to use based on item type - - @param item_type type of MET config variable to obtain - Valid values: list, string, int, float, thresh, bool - @returns function to use or None if invalid type provided - """ - if item_type == 'int': - return self.set_met_config_int - elif item_type == 'string': - return self.set_met_config_string - elif item_type == 'list': - return self.set_met_config_list - elif item_type == 'float': - return self.set_met_config_float - elif item_type == 'thresh': - return self.set_met_config_thresh - elif item_type == 'bool': - return self.set_met_config_bool - else: - self.log_error("Invalid argument for item type: " - f"{item_type}") - return None - - def handle_met_config_item(self, item, output_dict=None, depth=0): - """! Reads info from METConfigInfo object, gets value from - METplusConfig, and formats it based on the specifications. Sets - value in output dictionary with key starting with METPLUS_. - - @param item METConfigInfo object to read and determine what to get - @param output_dict (optional) dictionary to save formatted output - If unset, use self.env_var_dict. - @param depth counter to check if item being processed is nested within - another variable or not. If depth is 0, it is a top level variable. - This is used internally by this function and shouldn't be supplied - outside of calls within this function. - """ - if output_dict is None: - output_dict = self.env_var_dict - - env_var_name = item.env_var_name.upper() - if not env_var_name.startswith('METPLUS_'): - env_var_name = f'METPLUS_{env_var_name}' - - # handle dictionary or dictionary list item - if 'dict' in item.data_type: - tmp_dict = {} - for child in item.children: - if not self.handle_met_config_item(child, tmp_dict, - depth=depth+1): - return False - - dict_string = self.format_met_config(item.data_type, - tmp_dict, - item.name, - keys=None) - - # if handling dict MET config that is not nested inside another - if not depth and item.data_type == 'dict': - env_var_name = f'{env_var_name}_DICT' - - output_dict[env_var_name] = dict_string - return True - - # handle non-dictionary item - set_met_config = self.set_met_config_function(item.data_type) - if not set_met_config: - return False - - set_met_config(output_dict, - item.metplus_configs, - item.name, - c_dict_key=env_var_name, - **item.extra_args) - return True - - def handle_met_config_dict(self, dict_name, items): + def add_met_config_dict(self, dict_name, items): """! Read config variables for MET config dictionary and set env_var_dict with formatted values @@ -2308,127 +1756,30 @@ def handle_met_config_dict(self, dict_name, items): dictionary and the value is info about the item (see parse_item_info function for more information) """ - dict_items = [] - - # config prefix i.e GRID_STAT_CLIMO_MEAN_ - metplus_prefix = f'{self.app_name}_{dict_name}_'.upper() - for name, item_info in items.items(): - data_type, extra, kids, nicknames = self.parse_item_info(item_info) - - # config name i.e. GRID_STAT_CLIMO_MEAN_FILE_NAME - metplus_name = f'{metplus_prefix}{name.upper()}' - - # change (n) to _N i.e. distance_map.beta_value(n) - metplus_name = metplus_name.replace('(N)', '_N') - metplus_configs = [] - - if 'dict' not in data_type: - children = None - # if variable ends with _BEG, read _BEGIN first - if metplus_name.endswith('BEG'): - metplus_configs.append(f'{metplus_name}IN') - - metplus_configs.append(metplus_name) - if nicknames: - for nickname in nicknames: - metplus_configs.append( - f'{self.app_name}_{nickname}'.upper() - ) - - # if dictionary, read get children from MET config - else: - children = [] - for kid_name, kid_info in kids.items(): - kid_upper = kid_name.upper() - kid_type, kid_extra, _, _ = self.parse_item_info(kid_info) - - metplus_configs.append(f'{metplus_name}_{kid_upper}') - metplus_configs.append(f'{metplus_prefix}{kid_upper}') - - kid_args = self._parse_extra_args(kid_extra) - child_item = self.get_met_config( - name=kid_name, - data_type=kid_type, - metplus_configs=metplus_configs.copy(), - extra_args=kid_args, - ) - children.append(child_item) - - # reset metplus config list for next kid - metplus_configs.clear() - - # set metplus_configs - metplus_configs = None - - extra_args = self._parse_extra_args(extra) - dict_item = ( - self.get_met_config( - name=name, - data_type=data_type, - metplus_configs=metplus_configs, - extra_args=extra_args, - children=children, - ) - ) - dict_items.append(dict_item) - - final_met_config = self.get_met_config( - name=dict_name, - data_type='dict', - children=dict_items, - ) - - return self.handle_met_config_item(final_met_config, self.env_var_dict) - - @staticmethod - def parse_item_info(item_info): - """! Parses info about a MET config dictionary item. The input can - be a single string that is the data type of the item. It can also be - a tuple containing 2 to 4 values. The additional values must be - supplied in order: - * extra: string of extra information about item, i.e. - 'remove_quotes', 'uppercase', or 'allow_empty' - * kids: dictionary describing child values (used only for dict items) - where the key is the name of the variable and the value is item info - for the child variable in the same format as item_info that is - parsed in this function - * nicknames: list of other METplus config variable name that can be - used to set a value. The app name i.e. GRID_STAT_ is prepended to - each nickname in the list. Used for backwards compatibility for - METplus config variables whose name does not match the MET config - variable name - - @param item_info string or tuple containing information about a - dictionary item - @returns tuple of data type, extra info, children, and nicknames or - None for each tuple value that is not set - """ - if isinstance(item_info, tuple): - data_type, *rest = item_info - else: - data_type = item_info - rest = [] - - extra = rest.pop(0) if rest else None - kids = rest.pop(0) if rest else None - nicknames = rest.pop(0) if rest else None + return_code = add_met_config_dict(config=self.config, + app_name=self.app_name, + output_dict=self.env_var_dict, + dict_name=dict_name, + items=items) + if not return_code: + self.isOK = False - return data_type, extra, kids, nicknames + return return_code - def handle_met_config_window(self, dict_name): + def add_met_config_window(self, dict_name): """! Handle a MET config window dictionary. It is assumed that the dictionary only contains 'beg' and 'end' entries that are integers. @param dict_name name of MET dictionary """ - self.handle_met_config_dict(dict_name, { + self.add_met_config_dict(dict_name, { 'beg': 'int', 'end': 'int', }) def add_met_config(self, **kwargs): - """! Create METConfigInfo object from arguments and process - @param kwargs key arguments that should match METConfigInfo + """! Create METConfig object from arguments and process + @param kwargs key arguments that should match METConfig arguments, which includes the following: @param name MET config variable name to set @param data_type type of variable to set, i.e. string, list, bool @@ -2444,17 +1795,10 @@ def add_met_config(self, **kwargs): kwargs['metplus_configs'] = [ f"{self.app_name}_{kwargs.get('name')}".upper() ] - item = met_config(**kwargs) - output_dict = kwargs.get('output_dict') - self.handle_met_config_item(item, output_dict) - - def get_met_config(self, **kwargs): - """! Get METConfigInfo object from arguments and return it - @param kwargs key arguments that should match METConfigInfo - arguments - @returns METConfigInfo object - """ - return met_config(**kwargs) + item = METConfig(**kwargs) + output_dict = kwargs.get('output_dict', self.env_var_dict) + if not add_met_config_item(self.config, item, output_dict): + self.isOK = False def get_config_file(self, default_config_file=None): """! Get the MET config file path for the wrapper from the @@ -2464,20 +1808,9 @@ def get_config_file(self, default_config_file=None): file found in parm/met_config to use if config file is not set @returns path to wrapped config file or None if no default is provided """ - config_name = f'{self.app_name.upper()}_CONFIG_FILE' - config_file = self.config.getraw('config', config_name, '') - if config_file: - return config_file - - if not default_config_file: - return None - - default_config_path = os.path.join(self.config.getdir('PARM_BASE'), - 'met_config', + return get_wrapped_met_config_file(self.config, + self.app_name, default_config_file) - self.logger.debug(f"{config_name} is not set. " - f"Using {default_config_path}") - return default_config_path def get_start_time_input_dict(self): """! Get the first run time specified in config. Used if only running diff --git a/metplus/wrappers/compare_gridded_wrapper.py b/metplus/wrappers/compare_gridded_wrapper.py index 244f95e99e..f90e7c0567 100755 --- a/metplus/wrappers/compare_gridded_wrapper.py +++ b/metplus/wrappers/compare_gridded_wrapper.py @@ -14,6 +14,7 @@ from ..util import met_util as util from ..util import do_string_sub, ti_calculate +from ..util import parse_var_list from . import CommandBuilder '''!@namespace CompareGriddedWrapper @@ -50,8 +51,13 @@ def create_c_dict(self): which config variables are used in the wrapper""" c_dict = super().create_c_dict() - self.set_met_config_string(self.env_var_dict, 'MODEL', 'model', 'METPLUS_MODEL') - self.set_met_config_string(self.env_var_dict, 'OBTYPE', 'obtype', 'METPLUS_OBTYPE') + self.add_met_config(name='model', + data_type='string', + metplus_configs=['MODEL']) + + self.add_met_config(name='obtype', + data_type='string', + metplus_configs=['OBTYPE']) # set old MET config items for backwards compatibility c_dict['MODEL_OLD'] = self.config.getstr('config', 'MODEL', 'FCST') @@ -88,13 +94,11 @@ def create_c_dict(self): # handle window variables [FCST/OBS]_[FILE_]_WINDOW_[BEGIN/END] self.handle_file_window_variables(c_dict) - self.set_met_config_string(self.env_var_dict, - f'{self.app_name.upper()}_OUTPUT_PREFIX', - 'output_prefix', - 'METPLUS_OUTPUT_PREFIX') + self.add_met_config(name='output_prefix', + data_type='string') - c_dict['VAR_LIST_TEMP'] = util.parse_var_list(self.config, - met_tool=self.app_name) + c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, + met_tool=self.app_name) return c_dict @@ -111,8 +115,7 @@ def set_environment_variables(self, time_info): self.add_env_var('MODEL', self.c_dict.get('MODEL_OLD', '')) self.add_env_var('OBTYPE', self.c_dict.get('OBTYPE_OLD', '')) self.add_env_var('REGRID_TO_GRID', - self.c_dict.get('REGRID_TO_GRID', - 'NONE')) + self.c_dict.get('REGRID_TO_GRID', 'NONE')) super().set_environment_variables(time_info) @@ -400,7 +403,7 @@ def get_command(self): return cmd def handle_climo_cdf_dict(self): - self.handle_met_config_dict('climo_cdf', { + self.add_met_config_dict('climo_cdf', { 'cdf_bins': ('float', None, None, ['CLIMO_CDF_BINS']), 'center_bins': 'bool', 'write_bins': 'bool', @@ -425,4 +428,4 @@ def handle_interp_dict(self, uses_field=False): if uses_field: items['field'] = ('string', 'remove_quotes') - self.handle_met_config_dict('interp', items) + self.add_met_config_dict('interp', items) diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index 2c532ec05f..0bc64cd76b 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -16,6 +16,7 @@ from ..util import met_util as util from . import CompareGriddedWrapper from ..util import do_string_sub +from ..util import parse_var_list """!@namespace EnsembleStatWrapper @brief Wraps the MET tool ensemble_stat to compare ensemble datasets @@ -220,43 +221,39 @@ def create_c_dict(self): c_dict['MET_OBS_ERR_TABLE'] = \ self.config.getstr('config', 'ENSEMBLE_STAT_MET_OBS_ERR_TABLE', '') - self.set_met_config_float(self.env_var_dict, - 'ENSEMBLE_STAT_ENS_VLD_THRESH', - 'vld_thresh', - 'METPLUS_ENS_VLD_THRESH') - - self.set_met_config_list(self.env_var_dict, - 'ENSEMBLE_STAT_ENS_OBS_THRESH', - 'obs_thresh', - 'METPLUS_ENS_OBS_THRESH', - remove_quotes=True) - - self.set_met_config_float(self.env_var_dict, - 'ENSEMBLE_STAT_ENS_SSVAR_BIN_SIZE', - 'ens_ssvar_bin_size', - 'METPLUS_ENS_SSVAR_BIN_SIZE') - self.set_met_config_float(self.env_var_dict, - 'ENSEMBLE_STAT_ENS_PHIST_BIN_SIZE', - 'ens_phist_bin_size', - 'METPLUS_ENS_PHIST_BIN_SIZE') + self.add_met_config(name='vld_thresh', + data_type='float', + env_var_name='METPLUS_ENS_VLD_THRESH', + metplus_configs=['ENSEMBLE_STAT_ENS_VLD_THRESH', + 'ENSEMBLE_STAT_VLD_THRESH', + 'ENSEMBLE_STAT_ENS_VALID_THRESH', + 'ENSEMBLE_STAT_VALID_THRESH', + ]) + + self.add_met_config(name='obs_thresh', + data_type='list', + env_var_name='METPLUS_ENS_OBS_THRESH', + metplus_configs=['ENSEMBLE_STAT_ENS_OBS_THRESH', + 'ENSEMBLE_STAT_OBS_THRESH'], + extra_args={'remove_quotes': True}) + + self.add_met_config(name='ens_ssvar_bin_size', + data_type='float') + + self.add_met_config(name='ens_phist_bin_size', + data_type='float') self.handle_nbrhd_prob_dict() - self.set_met_config_float(self.env_var_dict, - 'ENSEMBLE_STAT_ENS_THRESH', - 'ens_thresh', - 'METPLUS_ENS_THRESH') + self.add_met_config(name='ens_thresh', + data_type='float') - self.set_met_config_string(self.env_var_dict, - 'ENSEMBLE_STAT_DUPLICATE_FLAG', - 'duplicate_flag', - 'METPLUS_DUPLICATE_FLAG', - remove_quotes=True) + self.add_met_config(name='duplicate_flag', + data_type='string', + extra_args={'remove_quotes': True}) - self.set_met_config_bool(self.env_var_dict, - 'ENSEMBLE_STAT_SKIP_CONST', - 'skip_const', - 'METPLUS_SKIP_CONST') + self.add_met_config(name='skip_const', + data_type='bool') # set climo_cdf dictionary variables self.handle_climo_cdf_dict() @@ -270,39 +267,35 @@ def create_c_dict(self): self.handle_flags('OUTPUT') self.handle_flags('ENSEMBLE') - self.set_met_config_bool(self.env_var_dict, - 'ENSEMBLE_STAT_OBS_ERROR_FLAG', - 'flag', - 'METPLUS_OBS_ERROR_FLAG') - self.set_met_config_list(self.env_var_dict, - 'ENSEMBLE_STAT_MASK_GRID', - 'grid', - 'METPLUS_MASK_GRID', - allow_empty=True) - self.set_met_config_list(self.env_var_dict, - 'ENSEMBLE_STAT_CI_ALPHA', - 'ci_alpha', - 'METPLUS_CI_ALPHA', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - 'ENSEMBLE_STAT_CENSOR_THRESH', - 'censor_thresh', - 'METPLUS_CENSOR_THRESH', - remove_quotes=True) - self.set_met_config_list(self.env_var_dict, - 'ENSEMBLE_STAT_CENSOR_VAL', - 'censor_val', - 'METPLUS_CENSOR_VAL', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - 'ENSEMBLE_STAT_MESSAGE_TYPE', - 'message_type', - 'METPLUS_MESSAGE_TYPE', - allow_empty=True) - - self.handle_obs_window_variables(c_dict) + self.add_met_config(name='flag', + data_type='bool', + env_var_name='METPLUS_OBS_ERROR_FLAG', + metplus_configs=['ENSEMBLE_STAT_OBS_ERROR_FLAG']) + + self.add_met_config(name='grid', + data_type='list', + env_var_name='METPLUS_MASK_GRID', + metplus_configs=['ENSEMBLE_STAT_MASK_GRID'], + extra_args={'allow_empty': True}) + + self.add_met_config(name='ci_alpha', + data_type='list', + extra_args={'remove_quotes': True}) + + self.add_met_config(name='censor_thresh', + data_type='list', + extra_args={'remove_quotes': True}) + + self.add_met_config(name='censor_val', + data_type='list', + extra_args={'remove_quotes': True}) + + self.add_met_config(name='message_type', + data_type='list', + extra_args={'allow_empty': True}) + + self.add_met_config_window('obs_window') + self.handle_obs_window_legacy(c_dict) c_dict['MASK_POLY_TEMPLATE'] = self.read_mask_poly() @@ -329,14 +322,14 @@ def create_c_dict(self): c_dict['VAR_LIST_OPTIONAL'] = True # parse var list for ENS fields - c_dict['ENS_VAR_LIST_TEMP'] = util.parse_var_list( + c_dict['ENS_VAR_LIST_TEMP'] = parse_var_list( self.config, data_type='ENS', met_tool=self.app_name ) # parse optional var list for FCST and/or OBS fields - c_dict['VAR_LIST_TEMP'] = util.parse_var_list( + c_dict['VAR_LIST_TEMP'] = parse_var_list( self.config, met_tool=self.app_name ) @@ -344,7 +337,7 @@ def create_c_dict(self): return c_dict def handle_nmep_smooth_dict(self): - self.handle_met_config_dict('nmep_smooth', { + self.add_met_config_dict('nmep_smooth', { 'vld_thresh': 'float', 'shape': ('string', 'uppercase,remove_quotes'), 'gaussian_dx': 'float', @@ -357,7 +350,7 @@ def handle_nmep_smooth_dict(self): }) def handle_nbrhd_prob_dict(self): - self.handle_met_config_dict('nbrhd_prob', { + self.add_met_config_dict('nbrhd_prob', { 'width': ('list', 'remove_quotes'), 'shape': ('string', 'uppercase,remove_quotes'), 'vld_thresh': 'float', diff --git a/metplus/wrappers/extract_tiles_wrapper.py b/metplus/wrappers/extract_tiles_wrapper.py index f9ad6dcc8a..6dd37a9684 100755 --- a/metplus/wrappers/extract_tiles_wrapper.py +++ b/metplus/wrappers/extract_tiles_wrapper.py @@ -15,6 +15,7 @@ from ..util import met_util as util from ..util import do_string_sub, ti_calculate +from ..util import parse_var_list from .regrid_data_plane_wrapper import RegridDataPlaneWrapper from . import CommandBuilder @@ -152,8 +153,8 @@ def create_c_dict(self): c_dict['LON_ADJ'] = self.config.getfloat('config', 'EXTRACT_TILES_LON_ADJ') - c_dict['VAR_LIST_TEMP'] = util.parse_var_list(self.config, - met_tool=self.app_name) + c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, + met_tool=self.app_name) return c_dict def regrid_data_plane_init(self): diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index 3beaebc66e..e8011fb0bd 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -118,7 +118,7 @@ def create_c_dict(self): metplus_configs=['DESC', 'GEN_ENS_PROD_DESC'], ) - self.handle_met_config_dict('regrid', { + self.add_met_config_dict('regrid', { 'to_grid': ('string', 'to_grid'), 'method': ('string', 'uppercase,remove_quotes'), 'width': 'int', @@ -176,13 +176,13 @@ def create_c_dict(self): extra_args={'remove_quotes': True, 'uppercase': True}) - self.handle_met_config_dict('nbrhd_prob', { + self.add_met_config_dict('nbrhd_prob', { 'width': ('list', 'remove_quotes'), 'shape': ('string', 'uppercase,remove_quotes'), 'vld_thresh': 'float', }) - self.handle_met_config_dict('nmep_smooth', { + self.add_met_config_dict('nmep_smooth', { 'vld_thresh': 'float', 'shape': ('string', 'uppercase,remove_quotes'), 'gaussian_dx': 'float', diff --git a/metplus/wrappers/grid_diag_wrapper.py b/metplus/wrappers/grid_diag_wrapper.py index b17de1dfd7..d6d306f758 100755 --- a/metplus/wrappers/grid_diag_wrapper.py +++ b/metplus/wrappers/grid_diag_wrapper.py @@ -16,6 +16,7 @@ from ..util import time_util from . import RuntimeFreqWrapper from ..util import do_string_sub +from ..util import parse_var_list '''!@namespace GridDiagWrapper @brief Wraps the Grid-Diag tool @@ -78,9 +79,9 @@ def create_c_dict(self): self.handle_censor_val_and_thresh() - c_dict['VAR_LIST_TEMP'] = util.parse_var_list(self.config, - data_type='FCST', - met_tool=self.app_name) + c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, + data_type='FCST', + met_tool=self.app_name) c_dict['MASK_POLY_TEMPLATE'] = self.read_mask_poly() diff --git a/metplus/wrappers/grid_stat_wrapper.py b/metplus/wrappers/grid_stat_wrapper.py index c020ae2dd0..2ad1d011a2 100755 --- a/metplus/wrappers/grid_stat_wrapper.py +++ b/metplus/wrappers/grid_stat_wrapper.py @@ -160,25 +160,31 @@ def create_c_dict(self): c_dict['ALLOW_MULTIPLE_FILES'] = False + self.add_met_config(name='cov_thresh', + data_type='list', + env_var_name='METPLUS_NBRHD_COV_THRESH', + metplus_configs=[ + 'GRID_STAT_NEIGHBORHOOD_COV_THRESH' + ], + extra_args={'remove_quotes': True}) + + self.add_met_config(name='width', + data_type='list', + env_var_name='METPLUS_NBRHD_WIDTH', + metplus_configs=[ + 'GRID_STAT_NEIGHBORHOOD_WIDTH' + ], + extra_args={'remove_quotes': True}) + + self.add_met_config(name='shape', + data_type='string', + env_var_name='METPLUS_NBRHD_SHAPE', + metplus_configs=[ + 'GRID_STAT_NEIGHBORHOOD_SHAPE' + ], + extra_args={'remove_quotes': True}) - self.set_met_config_list(self.env_var_dict, - f'GRID_STAT_NEIGHBORHOOD_COV_THRESH', - 'cov_thresh', - 'METPLUS_NBRHD_COV_THRESH', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - f'GRID_STAT_NEIGHBORHOOD_WIDTH', - 'width', - 'METPLUS_NBRHD_WIDTH', - remove_quotes=True) - - self.set_met_config_string(self.env_var_dict, - 'GRID_STAT_NEIGHBORHOOD_SHAPE', - 'shape', - 'METPLUS_NBRHD_SHAPE', - remove_quotes=True) - + # handle legacy environment variables used by old MET configs c_dict['NEIGHBORHOOD_WIDTH'] = ( self.config.getstr('config', 'GRID_STAT_NEIGHBORHOOD_WIDTH', '1') @@ -232,7 +238,7 @@ def create_c_dict(self): data_type='float', metplus_configs=['GRID_STAT_HSS_EC_VALUE']) - self.handle_met_config_dict('distance_map', { + self.add_met_config_dict('distance_map', { 'baddeley_p': 'int', 'baddeley_max_dist': 'float', 'fom_alpha': 'float', @@ -262,8 +268,9 @@ def set_environment_variables(self, time_info): self.add_env_var('NEIGHBORHOOD_SHAPE', self.c_dict['NEIGHBORHOOD_SHAPE']) + cov_thresh = self.get_env_var_value('METPLUS_NBRHD_COV_THRESH') self.add_env_var('NEIGHBORHOOD_COV_THRESH', - self.c_dict.get('NBRHD_COV_THRESH', '')) + cov_thresh) self.add_env_var('VERIF_MASK', self.c_dict.get('VERIFICATION_MASK', '')) diff --git a/metplus/wrappers/ioda2nc_wrapper.py b/metplus/wrappers/ioda2nc_wrapper.py index 3fecb4a4b0..bfdd2e42df 100755 --- a/metplus/wrappers/ioda2nc_wrapper.py +++ b/metplus/wrappers/ioda2nc_wrapper.py @@ -86,10 +86,10 @@ def create_c_dict(self): self.add_met_config(name='message_type_group_map', data_type='list', extra_args={'remove_quotes': True}) self.add_met_config(name='station_id', data_type='list') - self.handle_met_config_window('obs_window') + self.add_met_config_window('obs_window') self.handle_mask(single_value=True) - self.handle_met_config_window('elevation_range') - self.handle_met_config_window('level_range') + self.add_met_config_window('elevation_range') + self.add_met_config_window('level_range') self.add_met_config(name='obs_var', data_type='list') self.add_met_config(name='obs_name_map', data_type='list', extra_args={'remove_quotes': True}) diff --git a/metplus/wrappers/make_plots_wrapper.py b/metplus/wrappers/make_plots_wrapper.py index f33419f7ea..b73d660028 100755 --- a/metplus/wrappers/make_plots_wrapper.py +++ b/metplus/wrappers/make_plots_wrapper.py @@ -19,6 +19,7 @@ import itertools from ..util import met_util as util +from ..util import parse_var_list from . import CommandBuilder # handle if module can't be loaded to run wrapper @@ -120,7 +121,7 @@ def create_c_dict(self): c_dict['LOOP_LIST_ITEMS'] = util.getlist( self.config.getstr('config', 'LOOP_LIST_ITEMS') ) - c_dict['VAR_LIST'] = util.parse_var_list(self.config) + c_dict['VAR_LIST'] = parse_var_list(self.config) c_dict['MODEL_LIST'] = util.getlist( self.config.getstr('config', 'MODEL_LIST', '') ) diff --git a/metplus/wrappers/mode_wrapper.py b/metplus/wrappers/mode_wrapper.py index 0fd39b5387..af4237c4d7 100755 --- a/metplus/wrappers/mode_wrapper.py +++ b/metplus/wrappers/mode_wrapper.py @@ -150,15 +150,11 @@ def create_c_dict(self): ) c_dict['ONCE_PER_FIELD'] = True - self.set_met_config_bool(self.env_var_dict, - 'MODE_QUILT', - 'quilt', - 'METPLUS_QUILT') + self.add_met_config(name='quilt', + data_type='bool') - self.set_met_config_float(self.env_var_dict, - 'MODE_GRID_RES', - 'grid_res', - 'METPLUS_GRID_RES') + self.add_met_config(name='grid_res', + data_type='float') # if MODE_GRID_RES is not set, then unset the default values defaults = self.DEFAULT_VALUES.copy() @@ -168,83 +164,124 @@ def create_c_dict(self): # read forecast and observation field variables for data_type in ['FCST', 'OBS']: - self.set_met_config_list( - self.env_var_dict, - [f'{data_type}_MODE_CONV_RADIUS', - f'MODE_{data_type}_CONV_RADIUS', - 'MODE_CONV_RADIUS'], - 'conv_radius', - f'METPLUS_{data_type}_CONV_RADIUS', - remove_quotes=True, - default=defaults.get(f'{data_type}_CONV_RADIUS'), + self.add_met_config( + name='conv_radius', + data_type='list', + env_var_name=f'METPLUS_{data_type}_CONV_RADIUS', + metplus_configs=[f'{data_type}_MODE_CONV_RADIUS', + f'MODE_{data_type}_CONV_RADIUS', + 'MODE_CONV_RADIUS' + ], + extra_args={ + 'remove_quotes': True, + 'default': defaults.get(f'{data_type}_CONV_RADIUS') + } ) - self.set_met_config_list(self.env_var_dict, - [f'{data_type}_MODE_CONV_THRESH', - f'MODE_{data_type}_CONV_THRESH', - 'MODE_CONV_THRESH'], - 'conv_thresh', - f'METPLUS_{data_type}_CONV_THRESH', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - [f'{data_type}_MODE_MERGE_THRESH', - f'MODE_{data_type}_MERGE_THRESH', - 'MODE_MERGE_THRESH'], - 'merge_thresh', - f'METPLUS_{data_type}_MERGE_THRESH', - remove_quotes=True) - - self.set_met_config_string(self.env_var_dict, - [f'{data_type}_MODE_MERGE_FLAG', - f'MODE_{data_type}_MERGE_FLAG', - 'MODE_MERGE_FLAG'], - 'merge_flag', - f'METPLUS_{data_type}_MERGE_FLAG', - remove_quotes=True, - uppercase=True) - - self.set_met_config_list(self.env_var_dict, - [f'{data_type}_MODE_FILTER_ATTR_NAME', - f'MODE_{data_type}_FILTER_ATTR_NAME', - 'MODE_FILTER_ATTR_NAME'], - 'filter_attr_name', - f'METPLUS_{data_type}_FILTER_ATTR_NAME') - - self.set_met_config_list(self.env_var_dict, - [f'{data_type}_MODE_FILTER_ATTR_THRESH', - f'MODE_{data_type}_FILTER_ATTR_THRESH', - 'MODE_FILTER_ATTR_THRESH'], - 'filter_attr_thresh', - f'METPLUS_{data_type}_FILTER_ATTR_THRESH', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - [f'{data_type}_MODE_CENSOR_THRESH', - f'MODE_{data_type}_CENSOR_THRESH', - 'MODE_CENSOR_THRESH'], - 'censor_thresh', - f'METPLUS_{data_type}_CENSOR_THRESH', - remove_quotes=True) - - self.set_met_config_list(self.env_var_dict, - [f'{data_type}_MODE_CENSOR_VAL', - f'MODE_{data_type}_CENSOR_VAL', - f'{data_type}_MODE_CENSOR_VALUE', - f'MODE_{data_type}_CENSOR_VALUE', - 'MODE_CENSOR_VAL', - 'MODE_CENSOR_VALUE'], - 'censor_val', - f'METPLUS_{data_type}_CENSOR_VAL', - remove_quotes=True) - - self.set_met_config_float(self.env_var_dict, - [f'{data_type}_MODE_VLD_THRESH', - f'{data_type}_MODE_VALID_THRESH', - f'MODE_{data_type}_VLD_THRESH', - f'MODE_{data_type}_VALID_THRESH'], - 'vld_thresh', - f'METPLUS_{data_type}_VLD_THRESH') + self.add_met_config( + name='conv_thresh', + data_type='list', + env_var_name=f'METPLUS_{data_type}_CONV_THRESH', + metplus_configs=[f'{data_type}_MODE_CONV_THRESH', + f'MODE_{data_type}_CONV_THRESH', + 'MODE_CONV_THRESH' + ], + extra_args={ + 'remove_quotes': True, + } + ) + + self.add_met_config( + name='merge_thresh', + data_type='list', + env_var_name=f'METPLUS_{data_type}_MERGE_THRESH', + metplus_configs=[f'{data_type}_MODE_MERGE_THRESH', + f'MODE_{data_type}_MERGE_THRESH', + 'MODE_MERGE_THRESH' + ], + extra_args={ + 'remove_quotes': True, + } + ) + + self.add_met_config( + name='merge_flag', + data_type='string', + env_var_name=f'METPLUS_{data_type}_MERGE_FLAG', + metplus_configs=[f'{data_type}_MODE_MERGE_FLAG', + f'MODE_{data_type}_MERGE_FLAG', + 'MODE_MERGE_FLAG' + ], + extra_args={ + 'remove_quotes': True, + 'uppercase': True, + } + ) + + self.add_met_config( + name='filter_attr_name', + data_type='list', + env_var_name=f'METPLUS_{data_type}_FILTER_ATTR_NAME', + metplus_configs=[f'{data_type}_MODE_FILTER_ATTR_NAME', + f'MODE_{data_type}_FILTER_ATTR_NAME', + 'MODE_FILTER_ATTR_NAME' + ], + ) + + self.add_met_config( + name='filter_attr_thresh', + data_type='list', + env_var_name=f'METPLUS_{data_type}_FILTER_ATTR_THRESH', + metplus_configs=[f'{data_type}_MODE_FILTER_ATTR_THRESH', + f'MODE_{data_type}_FILTER_ATTR_THRESH', + 'MODE_FILTER_ATTR_THRESH' + ], + extra_args={ + 'remove_quotes': True, + } + ) + + self.add_met_config( + name='censor_thresh', + data_type='list', + env_var_name=f'METPLUS_{data_type}_CENSOR_THRESH', + metplus_configs=[f'{data_type}_MODE_CENSOR_THRESH', + f'MODE_{data_type}_CENSOR_THRESH', + 'MODE_CENSOR_THRESH' + ], + extra_args={ + 'remove_quotes': True, + } + ) + + self.add_met_config( + name='censor_val', + data_type='list', + env_var_name=f'METPLUS_{data_type}_CENSOR_VAL', + metplus_configs=[f'{data_type}_MODE_CENSOR_VAL', + f'MODE_{data_type}_CENSOR_VAL', + f'{data_type}_MODE_CENSOR_VALUE', + f'MODE_{data_type}_CENSOR_VALUE', + 'MODE_CENSOR_VAL', + 'MODE_CENSOR_VALUE', + ], + extra_args={ + 'remove_quotes': True, + } + ) + + self.add_met_config( + name='vld_thresh', + data_type='float', + env_var_name=f'METPLUS_{data_type}_VLD_THRESH', + metplus_configs=[f'{data_type}_MODE_VLD_THRESH', + f'MODE_{data_type}_VLD_THRESH', + f'{data_type}_MODE_VALID_THRESH', + f'MODE_{data_type}_VALID_THRESH', + 'MODE_VLD_THRESH', + 'MODE_VALID_THRESH' + ], + ) # set c_dict values for old method of setting env vars for name in ['CONV_RADIUS', @@ -254,14 +291,16 @@ def create_c_dict(self): value = self.get_env_var_value(f'METPLUS_{data_type}_{name}') c_dict[f'{data_type}_{name}'] = value - self.set_met_config_string(self.env_var_dict, - ['MODE_MATCH_FLAG'], - 'match_flag', - 'METPLUS_MATCH_FLAG', - remove_quotes=True, - uppercase=True) + self.add_met_config( + name='match_flag', + data_type='string', + extra_args={ + 'remove_quotes': True, + 'uppercase': True, + } + ) - self.handle_met_config_dict('weight', self.WEIGHTS) + self.add_met_config_dict('weight', self.WEIGHTS) self.handle_flags('nc_pairs') self.add_met_config(name='total_interest_thresh', diff --git a/metplus/wrappers/mtd_wrapper.py b/metplus/wrappers/mtd_wrapper.py index 328824ecbf..6bf2bc19be 100755 --- a/metplus/wrappers/mtd_wrapper.py +++ b/metplus/wrappers/mtd_wrapper.py @@ -15,6 +15,7 @@ from ..util import met_util as util from ..util import time_util from ..util import do_string_sub +from ..util import parse_var_list from . import CompareGriddedWrapper class MTDWrapper(CompareGriddedWrapper): @@ -66,8 +67,8 @@ def create_c_dict(self): c_dict['CONFIG_FILE'] = self.get_config_file('MTDConfig_wrapped') # new method of reading/setting MET config values - self.set_met_config_int(self.env_var_dict, 'MTD_MIN_VOLUME', - 'min_volume', 'METPLUS_MIN_VOLUME') + self.add_met_config(name='min_volume', + data_type='int') # old approach to reading/setting MET config values c_dict['MIN_VOLUME'] = self.config.getstr('config', @@ -124,9 +125,9 @@ def create_c_dict(self): self.read_field_values(c_dict, 'OBS', 'OBS') c_dict['VAR_LIST_TEMP'] = ( - util.parse_var_list(self.config, - data_type=c_dict.get('SINGLE_DATA_SRC'), - met_tool=self.app_name) + parse_var_list(self.config, + data_type=c_dict.get('SINGLE_DATA_SRC'), + met_tool=self.app_name) ) return c_dict @@ -137,17 +138,17 @@ def read_field_values(self, c_dict, read_type, write_type): self.config.getstr('config', f'{read_type}_MTD_INPUT_DATATYPE', '') ) - self.set_met_config_int(self.env_var_dict, - [f'{read_type}_MTD_CONV_RADIUS', - 'MTD_CONV_RADIUS'], - 'conv_radius', - f'METPLUS_{write_type}_CONV_RADIUS') - - self.set_met_config_thresh(self.env_var_dict, - [f'{read_type}_MTD_CONV_THRESH', - 'MTD_CONV_THRESH'], - 'conv_thresh', - f'METPLUS_{write_type}_CONV_THRESH') + self.add_met_config(name='conv_radius', + data_type='int', + env_var_name=f'METPLUS_{write_type}_CONV_RADIUS', + metplus_configs=[f'{read_type}_MTD_CONV_RADIUS', + 'MTD_CONV_RADIUS']) + + self.add_met_config(name='conv_thresh', + data_type='thresh', + env_var_name=f'METPLUS_{write_type}_CONV_THRESH', + metplus_configs=[f'{read_type}_MTD_CONV_THRESH', + 'MTD_CONV_THRESH']) # support old method of setting env vars conf_value = ( diff --git a/metplus/wrappers/pb2nc_wrapper.py b/metplus/wrappers/pb2nc_wrapper.py index 891fc367bc..5e8622a2bd 100755 --- a/metplus/wrappers/pb2nc_wrapper.py +++ b/metplus/wrappers/pb2nc_wrapper.py @@ -91,27 +91,55 @@ def create_c_dict(self): # get the MET config file path or use default c_dict['CONFIG_FILE'] = self.get_config_file('PB2NCConfig_wrapped') - self.set_met_config_list(self.env_var_dict, - 'PB2NC_MESSAGE_TYPE', - 'message_type', - 'METPLUS_MESSAGE_TYPE',) + self.add_met_config(name='message_type', + data_type='list') - self.set_met_config_list(self.env_var_dict, - 'PB2NC_STATION_ID', - 'station_id', - 'METPLUS_STATION_ID',) + self.add_met_config(name='station_id', + data_type='list') - self.handle_obs_window_variables(c_dict) + self.add_met_config_window('obs_window') + self.handle_obs_window_legacy(c_dict) self.handle_mask(single_value=True) - self.set_met_config_list(self.env_var_dict, - 'PB2NC_OBS_BUFR_VAR_LIST', - 'obs_bufr_var', - 'METPLUS_OBS_BUFR_VAR', - allow_empty=True) + self.add_met_config(name='obs_bufr_var', + data_type='list', + metplus_configs=['PB2NC_OBS_BUFR_VAR_LIST', + 'PB2NC_OBS_BUFR_VAR'], + extra_args={'allow_empty': True}) + + #self.handle_time_summary_legacy(c_dict) + self.handle_time_summary_dict() + + # handle legacy time summary variables + self.add_met_config(name='', + data_type='bool', + env_var_name='TIME_SUMMARY_FLAG', + metplus_configs=['PB2NC_TIME_SUMMARY_FLAG']) + + self.add_met_config(name='', + data_type='string', + env_var_name='TIME_SUMMARY_BEG', + metplus_configs=['PB2NC_TIME_SUMMARY_BEG']) + + self.add_met_config(name='', + data_type='string', + env_var_name='TIME_SUMMARY_END', + metplus_configs=['PB2NC_TIME_SUMMARY_END']) + + self.add_met_config(name='', + data_type='list', + env_var_name='TIME_SUMMARY_VAR_NAMES', + metplus_configs=['PB2NC_TIME_SUMMARY_OBS_VAR', + 'PB2NC_TIME_SUMMARY_VAR_NAMES'], + extra_args={'allow_empty': True}) - self.handle_time_summary_legacy(c_dict) + self.add_met_config(name='', + data_type='list', + env_var_name='TIME_SUMMARY_TYPES', + metplus_configs=['PB2NC_TIME_SUMMARY_TYPE', + 'PB2NC_TIME_SUMMARY_TYPES'], + extra_args={'allow_empty': True}) self.handle_file_window_variables(c_dict, dtypes=['OBS']) @@ -147,22 +175,7 @@ def create_c_dict(self): extra_args={'remove_quotes': True}) # get level_range beg and end - level_range_items = [] - level_range_items.append( - self.get_met_config(name='beg', - data_type='int', - metplus_configs=['PB2NC_LEVEL_RANGE_BEG', - 'PB2NC_LEVEL_RANGE_BEGIN']) - ) - level_range_items.append( - self.get_met_config(name='end', - data_type='int', - metplus_configs=['PB2NC_LEVEL_RANGE_END']) - ) - - self.add_met_config(name='level_range', - data_type='dict', - children=level_range_items) + self.add_met_config_window('level_range') self.add_met_config(name='level_category', data_type='list', @@ -173,7 +186,6 @@ def create_c_dict(self): data_type='int', metplus_configs=['PB2NC_QUALITY_MARK_THRESH']) - return c_dict def set_environment_variables(self, time_info): @@ -196,16 +208,10 @@ def set_environment_variables(self, time_info): self.add_env_var("OBS_BUFR_VAR_LIST", self.c_dict.get('BUFR_VAR_LIST', '')) - self.add_env_var('TIME_SUMMARY_FLAG', - self.c_dict.get('TIME_SUMMARY_FLAG', '')) - self.add_env_var('TIME_SUMMARY_BEG', - self.c_dict.get('TIME_SUMMARY_BEG', '')) - self.add_env_var('TIME_SUMMARY_END', - self.c_dict.get('TIME_SUMMARY_END', '')) - self.add_env_var('TIME_SUMMARY_VAR_NAMES', - self.c_dict.get('TIME_SUMMARY_VAR_NAMES', '')) - self.add_env_var('TIME_SUMMARY_TYPES', - self.c_dict.get('TIME_SUMMARY_TYPES', '')) + for item in ['FLAG', 'BEG', 'END', 'VAR_NAMES', 'TYPES']: + ts_item = f'TIME_SUMMARY_{item}' + self.add_env_var(f'{ts_item}', + self.env_var_dict.get(f'METPLUS_{ts_item}', '')) super().set_environment_variables(time_info) diff --git a/metplus/wrappers/pcp_combine_wrapper.py b/metplus/wrappers/pcp_combine_wrapper.py index 9d90a494ae..d34eaa609b 100755 --- a/metplus/wrappers/pcp_combine_wrapper.py +++ b/metplus/wrappers/pcp_combine_wrapper.py @@ -12,6 +12,7 @@ from ..util import get_seconds_from_string, ti_get_lead_string, ti_calculate from ..util import get_relativedelta, ti_get_seconds_from_relativedelta from ..util import time_string_to_met_time, seconds_to_met_time +from ..util import parse_var_list from . import ReformatGriddedWrapper '''!@namespace PCPCombineWrapper @@ -56,14 +57,14 @@ def create_c_dict(self): if fcst_run: c_dict = self.set_fcst_or_obs_dict_items('FCST', c_dict) - c_dict['VAR_LIST_FCST'] = util.parse_var_list( + c_dict['VAR_LIST_FCST'] = parse_var_list( self.config, data_type='FCST', met_tool=self.app_name ) if obs_run: c_dict = self.set_fcst_or_obs_dict_items('OBS', c_dict) - c_dict['VAR_LIST_OBS'] = util.parse_var_list( + c_dict['VAR_LIST_OBS'] = parse_var_list( self.config, data_type='OBS', met_tool=self.app_name diff --git a/metplus/wrappers/point_stat_wrapper.py b/metplus/wrappers/point_stat_wrapper.py index a21df7caad..02c97debee 100755 --- a/metplus/wrappers/point_stat_wrapper.py +++ b/metplus/wrappers/point_stat_wrapper.py @@ -140,34 +140,32 @@ def create_c_dict(self): # get the MET config file path or use default c_dict['CONFIG_FILE'] = self.get_config_file('PointStatConfig_wrapped') - self.handle_obs_window_variables(c_dict) - - self.set_met_config_list(self.env_var_dict, - ['POINT_STAT_MASK_GRID', - 'POINT_STAT_GRID'], - 'grid', - 'METPLUS_MASK_GRID', - allow_empty=True) - - self.set_met_config_list(self.env_var_dict, - ['POINT_STAT_MASK_POLY', - 'POINT_STAT_POLY'], - 'poly', - 'METPLUS_MASK_POLY', - allow_empty=True) - - self.set_met_config_list(self.env_var_dict, - ['POINT_STAT_MASK_SID', - 'POINT_STAT_STATION_ID'], - 'sid', - 'METPLUS_MASK_SID', - allow_empty=True) - - - self.set_met_config_list(self.env_var_dict, - 'POINT_STAT_MESSAGE_TYPE', - 'message_type', - 'METPLUS_MESSAGE_TYPE',) + self.add_met_config_window('obs_window') + self.handle_obs_window_legacy(c_dict) + + self.add_met_config(name='grid', + data_type='list', + env_var_name='METPLUS_MASK_GRID', + metplus_configs=['POINT_STAT_MASK_GRID', + 'POINT_STAT_GRID'], + extra_args={'allow_empty': True}) + + self.add_met_config(name='poly', + data_type='list', + env_var_name='METPLUS_MASK_POLY', + metplus_configs=['POINT_STAT_MASK_POLY', + 'POINT_STAT_POLY'], + extra_args={'allow_empty': True}) + + self.add_met_config(name='sid', + data_type='list', + env_var_name='METPLUS_MASK_SID', + metplus_configs=['POINT_STAT_MASK_SID', + 'POINT_STAT_STATION_ID'], + extra_args={'allow_empty': True}) + + self.add_met_config(name='message_type', + data_type='list') self.handle_climo_cdf_dict() diff --git a/metplus/wrappers/regrid_data_plane_wrapper.py b/metplus/wrappers/regrid_data_plane_wrapper.py index b3f0711fcc..a3d544a0db 100755 --- a/metplus/wrappers/regrid_data_plane_wrapper.py +++ b/metplus/wrappers/regrid_data_plane_wrapper.py @@ -15,6 +15,8 @@ from ..util import met_util as util from ..util import time_util from ..util import do_string_sub +from ..util import parse_var_list +from ..util import get_process_list from . import ReformatGriddedWrapper # pylint:disable=pointless-string-statement @@ -106,7 +108,7 @@ def create_c_dict(self): self.log_error("FCST_REGRID_DATA_PLANE_OUTPUT_TEMPLATE must be set if " "FCST_REGRID_DATA_PLANE_RUN is True") - c_dict['VAR_LIST_FCST'] = util.parse_var_list( + c_dict['VAR_LIST_FCST'] = parse_var_list( self.config, data_type='FCST', met_tool=self.app_name @@ -129,7 +131,7 @@ def create_c_dict(self): self.log_error("OBS_REGRID_DATA_PLANE_OUTPUT_TEMPLATE must be set if " "OBS_REGRID_DATA_PLANE_RUN is True") - c_dict['VAR_LIST_OBS'] = util.parse_var_list( + c_dict['VAR_LIST_OBS'] = parse_var_list( self.config, data_type='OBS', met_tool=self.app_name @@ -155,7 +157,7 @@ def create_c_dict(self): # only check if VERIFICATION_GRID is set if running the tool from the process list # RegridDataPlane can be called from other tools like CustomIngest, which sets the # verification grid itself - if 'RegridDataPlane' in util.get_process_list(self.config): + if 'RegridDataPlane' in get_process_list(self.config): if not c_dict['VERIFICATION_GRID']: self.log_error("REGRID_DATA_PLANE_VERIF_GRID must be set.") diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 972fa1c376..429b139c39 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -27,6 +27,7 @@ from ..util import get_lead_sequence, get_lead_sequence_groups, set_input_dict from ..util import ti_get_hours_from_lead, ti_get_seconds_from_lead from ..util import ti_get_lead_string +from ..util import parse_var_list from .plot_data_plane_wrapper import PlotDataPlaneWrapper from . import RuntimeFreqWrapper @@ -86,14 +87,13 @@ def create_c_dict(self): c_dict['VERBOSITY']) ) - self.set_met_config_string(self.env_var_dict, - 'MODEL', - 'model', - 'METPLUS_MODEL') - self.set_met_config_string(self.env_var_dict, - 'OBTYPE', - 'obtype', - 'METPLUS_OBTYPE') + self.add_met_config(name='model', + data_type='string', + metplus_configs=['MODEL']) + + self.add_met_config(name='obtype', + data_type='string', + metplus_configs=['OBTYPE']) # handle old format of MODEL and OBTYPE c_dict['MODEL'] = self.config.getstr('config', 'MODEL', 'WRF') @@ -103,22 +103,18 @@ def create_c_dict(self): self.handle_regrid(c_dict) - self.set_met_config_list(self.env_var_dict, - 'SERIES_ANALYSIS_CAT_THRESH', - 'cat_thresh', - 'METPLUS_CAT_THRESH', - remove_quotes=True) + self.add_met_config(name='cat_thresh', + data_type='list', + extra_args={'remove_quotes': True}) - self.set_met_config_float(self.env_var_dict, - 'SERIES_ANALYSIS_VLD_THRESH', - 'vld_thresh', - 'METPLUS_VLD_THRESH') + self.add_met_config(name='vld_thresh', + data_type='float', + metplus_configs=['SERIES_ANALYSIS_VLD_THRESH', + 'SERIES_ANALYSIS_VALID_THRESH',]) - self.set_met_config_string(self.env_var_dict, - 'SERIES_ANALYSIS_BLOCK_SIZE', - 'block_size', - 'METPLUS_BLOCK_SIZE', - remove_quotes=True) + self.add_met_config(name='block_size', + data_type='string', + extra_args={'remove_quotes': True}) # get stat list to loop over c_dict['STAT_LIST'] = util.getlist( @@ -130,16 +126,18 @@ def create_c_dict(self): self.log_error("Must set SERIES_ANALYSIS_STAT_LIST to run.") # set stat list to set output_stats.cnt in MET config file - self.set_met_config_list(self.env_var_dict, - 'SERIES_ANALYSIS_STAT_LIST', - 'cnt', - 'METPLUS_STAT_LIST') + self.add_met_config(name='cnt', + data_type='list', + env_var_name='METPLUS_STAT_LIST', + metplus_configs=['SERIES_ANALYSIS_STAT_LIST', + 'SERIES_ANALYSIS_CNT']) # set cts list to set output_stats.cts in MET config file - self.set_met_config_list(self.env_var_dict, - 'SERIES_ANALYSIS_CTS_LIST', - 'cts', - 'METPLUS_CTS_LIST') + self.add_met_config(name='cts', + data_type='list', + env_var_name='METPLUS_CTS_LIST', + metplus_configs=['SERIES_ANALYSIS_CTS_LIST', + 'SERIES_ANALYSIS_CTS']) c_dict['PAIRED'] = self.config.getbool('config', 'SERIES_ANALYSIS_IS_PAIRED', @@ -233,8 +231,8 @@ def create_c_dict(self): False) ) - c_dict['VAR_LIST_TEMP'] = util.parse_var_list(self.config, - met_tool=self.app_name) + c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, + met_tool=self.app_name) if not c_dict['VAR_LIST_TEMP']: self.log_error("No fields specified. Please set " "[FCST/OBS]_VAR_[NAME/LEVELS]") @@ -444,7 +442,7 @@ def get_storm_list(self, time_info): # Now that we have the filter filename for the init time, let's # extract all the storm ids in this filter file. - storm_list = util.get_storm_ids(filter_file) + storm_list = util.get_storms(filter_file, id_only=True) if not storm_list: # No storms for this init time, check next init time in list self.logger.debug("No storms found for current runtime") diff --git a/metplus/wrappers/stat_analysis_wrapper.py b/metplus/wrappers/stat_analysis_wrapper.py index 1d8289aaa2..44fabeb66e 100755 --- a/metplus/wrappers/stat_analysis_wrapper.py +++ b/metplus/wrappers/stat_analysis_wrapper.py @@ -19,7 +19,8 @@ import itertools from ..util import met_util as util -from ..util import do_string_sub +from ..util import do_string_sub, find_indices_in_config_section +from ..util import parse_var_list from . import CommandBuilder class StatAnalysisWrapper(CommandBuilder): @@ -215,7 +216,7 @@ def create_c_dict(self): if not self.MakePlotsWrapper.isOK: self.log_error("MakePlotsWrapper was not initialized correctly.") - c_dict['VAR_LIST'] = util.parse_var_list(self.config) + c_dict['VAR_LIST'] = parse_var_list(self.config) c_dict['MODEL_INFO_LIST'] = self.parse_model_info() if not c_dict['MODEL_LIST'] and c_dict['MODEL_INFO_LIST']: @@ -1229,9 +1230,9 @@ def parse_model_info(self): """ model_info_list = [] model_indices = list( - util.find_indices_in_config_section(r'MODEL(\d+)$', - self.config, - index_index=1).keys() + find_indices_in_config_section(r'MODEL(\d+)$', + self.config, + index_index=1).keys() ) for m in model_indices: model_name = self.config.getstr('config', f'MODEL{m}') diff --git a/metplus/wrappers/tc_gen_wrapper.py b/metplus/wrappers/tc_gen_wrapper.py index 94c6a0596e..91168a0e96 100755 --- a/metplus/wrappers/tc_gen_wrapper.py +++ b/metplus/wrappers/tc_gen_wrapper.py @@ -148,16 +148,16 @@ def create_c_dict(self): data_type='int', metplus_configs=['TC_GEN_VALID_FREQUENCY', 'TC_GEN_VALID_FREQ']) - self.handle_met_config_window('fcst_hr_window') + self.add_met_config_window('fcst_hr_window') self.add_met_config(name='min_duration', data_type='int', metplus_configs=['TC_GEN_MIN_DURATION']) - self.handle_met_config_dict('fcst_genesis', { + self.add_met_config_dict('fcst_genesis', { 'vmax_thresh': 'thresh', 'mslp_thresh': 'thresh', }) - self.handle_met_config_dict('best_genesis', { + self.add_met_config_dict('best_genesis', { 'technique': 'string', 'category': 'list', 'vmax_thresh': 'thresh', @@ -220,8 +220,8 @@ def create_c_dict(self): self.add_met_config(name='dev_hit_radius', data_type='int', metplus_configs=['TC_GEN_DEV_HIT_RADIUS']) - self.handle_met_config_window('dev_hit_window') - self.handle_met_config_window('ops_hit_window') + self.add_met_config_window('dev_hit_window') + self.add_met_config_window('ops_hit_window') self.add_met_config(name='discard_init_post_genesis_flag', data_type='bool', metplus_configs=[ @@ -260,7 +260,7 @@ def create_c_dict(self): data_type='bool', metplus_configs=['TC_GEN_GENESIS_MATCH_POINT_TO_TRACK'] ) - self.handle_met_config_window('genesis_match_window') + self.add_met_config_window('genesis_match_window') # get INPUT_TIME_DICT values since wrapper only runs # once (doesn't look over time) diff --git a/metplus/wrappers/tc_pairs_wrapper.py b/metplus/wrappers/tc_pairs_wrapper.py index 2764202f59..f86e814e13 100755 --- a/metplus/wrappers/tc_pairs_wrapper.py +++ b/metplus/wrappers/tc_pairs_wrapper.py @@ -24,6 +24,7 @@ from ..util import met_util as util from ..util import do_string_sub from ..util import get_tags +from ..util.met_config import add_met_config_dict_list from . import CommandBuilder '''!@namespace TCPairsWrapper @@ -315,66 +316,21 @@ def _read_storm_info(self, c_dict): c_dict['BASIN_LIST'] = basin_list def handle_consensus(self): - children = [ - 'NAME', - 'MEMBERS', - 'REQUIRED', - 'MIN_REQ' - ] - regex = r'^TC_PAIRS_CONSENSUS(\d+)_(\w+)$' - indices = util.find_indices_in_config_section(regex, self.config, - index_index=1, - id_index=2) - - consensus_dict = {} - for index, items in indices.items(): - # read all variables for each index - consensus_items = {} - - # check if any variable found doesn't match valid variables - if any([item for item in items if item not in children]): - self.log_error("Invalid variable: " - f"TC_PAIRS_CONSENSUS{index}_{item}") - - self.add_met_config( - name='name', - data_type='string', - metplus_configs=[f'TC_PAIRS_CONSENSUS{index}_NAME'], - output_dict=consensus_items - ) - self.add_met_config( - name='members', - data_type='list', - metplus_configs=[f'TC_PAIRS_CONSENSUS{index}_MEMBERS'], - output_dict=consensus_items - ) - self.add_met_config( - name='required', - data_type='list', - metplus_configs=[f'TC_PAIRS_CONSENSUS{index}_REQUIRED'], - extra_args={'remove_quotes': True}, - output_dict=consensus_items - ) - self.add_met_config( - name='min_req', - data_type='int', - metplus_configs=[f'TC_PAIRS_CONSENSUS{index}_MIN_REQ'], - output_dict=consensus_items - ) - - self.logger.debug(f'Consensus Items: {consensus_items}') - # format dictionary, then add it to consensus_dict - dict_string = self.format_met_config('dict', - consensus_items, - name='') - consensus_dict[index] = dict_string - - # format list of dictionaries - output_string = self.format_met_config('list', - consensus_dict, - 'consensus') - - self.env_var_dict['METPLUS_CONSENSUS_LIST'] = output_string + dict_items = { + 'name': 'string', + 'members': 'list', + 'required': ('list', 'remove_quotes'), + 'min_req': 'int', + } + return_code = add_met_config_dict_list(config=self.config, + app_name=self.app_name, + output_dict=self.env_var_dict, + dict_name='consensus', + dict_items=dict_items) + if not return_code: + self.isOK = False + + return return_code def run_all_times(self): """! Build up the command to invoke the MET tool tc_pairs. diff --git a/metplus/wrappers/tc_stat_wrapper.py b/metplus/wrappers/tc_stat_wrapper.py index c8a6c52a01..bb3c33e62c 100755 --- a/metplus/wrappers/tc_stat_wrapper.py +++ b/metplus/wrappers/tc_stat_wrapper.py @@ -129,15 +129,77 @@ def create_c_dict(self): # get the MET config file path or use default c_dict['CONFIG_FILE'] = self.get_config_file('TCStatConfig_wrapped') + self.set_met_config_for_environment_variables() + + return c_dict + + def set_met_config_for_environment_variables(self): + """! Set c_dict dictionary entries that will be set as environment + variables to be read by the MET config file. + @param c_dict dictionary to add key/value pairs + """ self.handle_description() - self.set_met_config_for_environment_variables() + for config_list in ['amodel', + 'bmodel', + 'storm_id', + 'basin', + 'cyclone', + 'storm_name', + 'init_hour', + 'lead_req', + 'init_mask', + 'valid_mask', + 'valid_hour', + 'lead', + 'track_watch_warn', + 'column_thresh_name', + 'column_thresh_val', + 'column_str_name', + 'column_str_val', + 'init_thresh_name', + 'init_thresh_val', + 'init_str_name', + 'init_str_val', + ]: + self.add_met_config(name=config_list, + data_type='list') + + for iv_list in ['INIT', 'VALID']: + self.add_met_config(name=f'{iv_list.lower()}_inc', + data_type='list', + metplus_configs=[f'TC_STAT_{iv_list}_INC', + f'TC_STAT_{iv_list}_INCLUDE']) + self.add_met_config(name=f'{iv_list.lower()}_exc', + data_type='list', + metplus_configs=[f'TC_STAT_{iv_list}_EXC', + f'TC_STAT_{iv_list}_EXCLUDE']) + + for config_str in ['INIT_BEG', + 'INIT_END', + 'VALID_BEG', + 'VALID_END', + 'LANDFALL_BEG', + 'LANDFALL_END', + ]: + self.add_met_config(name=config_str.lower(), + data_type='string', + metplus_configs=[f'TC_STAT_{config_str}', + config_str]) + + for config_bool in ['water_only', + 'landfall', + 'match_points', + ]: + + self.add_met_config(name=config_bool, + data_type='bool') self.add_met_config(name='column_str_exc_name', data_type='list', metplus_configs=['TC_STAT_COLUMN_STR_EXC_NAME', 'TC_STAT_COLUMN_STR_EXCLUDE_NAME', - ]) + ]) self.add_met_config(name='column_str_exc_val', data_type='list', metplus_configs=['TC_STAT_COLUMN_STR_EXC_VAL', @@ -154,77 +216,6 @@ def create_c_dict(self): 'TC_STAT_INIT_STR_EXCLUDE_VAL', ]) - return c_dict - - def set_met_config_for_environment_variables(self): - """! Set c_dict dictionary entries that will be set as environment - variables to be read by the MET config file. - @param c_dict dictionary to add key/value pairs - """ - app_name_upper = self.app_name.upper() - - for config_list in ['AMODEL', - 'BMODEL', - 'STORM_ID', - 'BASIN', - 'CYCLONE', - 'STORM_NAME', - 'INIT_HOUR', - 'LEAD_REQ', - 'INIT_MASK', - 'VALID_MASK', - 'VALID_HOUR', - 'LEAD', - 'TRACK_WATCH_WARN', - 'COLUMN_THRESH_NAME', - 'COLUMN_THRESH_VAL', - 'COLUMN_STR_NAME', - 'COLUMN_STR_VAL', - 'INIT_THRESH_NAME', - 'INIT_THRESH_VAL', - 'INIT_STR_NAME', - 'INIT_STR_VAL', - ]: - self.set_met_config_list(self.env_var_dict, - f'{app_name_upper}_{config_list}', - config_list.lower(), - f'METPLUS_{config_list}') - - for iv_list in ['INIT', 'VALID',]: - self.set_met_config_list(self.env_var_dict, - f'{app_name_upper}_{iv_list}_INCLUDE', - f'{iv_list.lower()}_inc', - f'METPLUS_{iv_list}_INC' - ) - self.set_met_config_list(self.env_var_dict, - f'{app_name_upper}_{iv_list}_EXCLUDE', - f'{iv_list.lower()}_exc', - f'METPLUS_{iv_list}_EXC' - ) - - for config_str in ['INIT_BEG', - 'INIT_END', - 'VALID_BEG', - 'VALID_END', - 'LANDFALL_BEG', - 'LANDFALL_END', - ]: - self.set_met_config_string(self.env_var_dict, - [f'{app_name_upper}_{config_str}', - f'{config_str}'], - config_str.lower(), - f'METPLUS_{config_str}') - - for config_bool in ['WATER_ONLY', - 'LANDFALL', - 'MATCH_POINTS', - ]: - - self.set_met_config_bool(self.env_var_dict, - f'{app_name_upper}_{config_bool}', - config_bool.lower(), - f'METPLUS_{config_bool}') - def run_at_time(self, input_dict=None): """! Builds the call to the MET tool TC-STAT for all requested initialization times (init or valid). Called from run_metplus diff --git a/metplus/wrappers/tcrmw_wrapper.py b/metplus/wrappers/tcrmw_wrapper.py index 5cf562017f..a5ce97553d 100755 --- a/metplus/wrappers/tcrmw_wrapper.py +++ b/metplus/wrappers/tcrmw_wrapper.py @@ -16,6 +16,7 @@ from ..util import time_util from . import CommandBuilder from ..util import do_string_sub +from ..util import parse_var_list '''!@namespace TCRMWWrapper @brief Wraps the TC-RMW tool @@ -81,92 +82,85 @@ def create_c_dict(self): 'TC_RMW_DECK_TEMPLATE') ) - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_INPUT_DATATYPE', - 'file_type', - 'METPLUS_DATA_FILE_TYPE') + self.add_met_config(name='file_type', + data_type='string', + env_var_name='METPLUS_DATA_FILE_TYPE', + metplus_configs=['TC_RMW_INPUT_DATATYPE', + 'TC_RMW_FILE_TYPE']) - # values used in configuration file - self.set_met_config_string(self.env_var_dict, - 'MODEL', - 'model', - 'METPLUS_MODEL') + self.add_met_config(name='model', + data_type='string', + metplus_configs=['MODEL']) self.handle_regrid(c_dict, set_to_grid=False) - self.set_met_config_int(self.env_var_dict, - 'TC_RMW_N_RANGE', - 'n_range', - 'METPLUS_N_RANGE') - - self.set_met_config_int(self.env_var_dict, - 'TC_RMW_N_AZIMUTH', - 'n_azimuth', - 'METPLUS_N_AZIMUTH') - - self.set_met_config_float(self.env_var_dict, - 'TC_RMW_MAX_RANGE_KM', - 'max_range_km', - 'METPLUS_MAX_RANGE_KM') - - self.set_met_config_float(self.env_var_dict, - 'TC_RMW_DELTA_RANGE_KM', - 'delta_range_km', - 'METPLUS_DELTA_RANGE_KM') - - self.set_met_config_float(self.env_var_dict, - 'TC_RMW_SCALE', - 'rmw_scale', - 'METPLUS_RMW_SCALE') - - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_STORM_ID', - 'storm_id', - 'METPLUS_STORM_ID') - - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_BASIN', - 'basin', - 'METPLUS_BASIN') - - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_CYCLONE', - 'cyclone', - 'METPLUS_CYCLONE') - - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_INIT_INCLUDE', - 'init_inc', - 'METPLUS_INIT_INCLUDE') - - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_VALID_BEG', - 'valid_beg', - 'METPLUS_VALID_BEG') - - self.set_met_config_string(self.env_var_dict, - 'TC_RMW_VALID_END', - 'valid_end', - 'METPLUS_VALID_END') - - self.set_met_config_list(self.env_var_dict, - 'TC_RMW_VALID_INCLUDE_LIST', - 'valid_inc', - 'METPLUS_VALID_INCLUDE_LIST') - - self.set_met_config_list(self.env_var_dict, - 'TC_RMW_VALID_EXCLUDE_LIST', - 'valid_exc', - 'METPLUS_VALID_EXCLUDE_LIST') - - self.set_met_config_list(self.env_var_dict, - 'TC_RMW_VALID_HOUR_LIST', - 'valid_hour', - 'METPLUS_VALID_HOUR_LIST') - - c_dict['VAR_LIST_TEMP'] = util.parse_var_list(self.config, - data_type='FCST', - met_tool=self.app_name) + self.add_met_config(name='n_range', + data_type='int') + + self.add_met_config(name='n_azimuth', + data_type='int') + + self.add_met_config(name='max_range_km', + data_type='float') + + self.add_met_config(name='delta_range_km', + data_type='float') + + self.add_met_config(name='rmw_scale', + data_type='float') + + self.add_met_config(name='storm_id', + data_type='string') + + self.add_met_config(name='basin', + data_type='string') + + self.add_met_config(name='cyclone', + data_type='string') + + self.add_met_config(name='init_inc', + data_type='string', + env_var_name='METPLUS_INIT_INCLUDE', + metplus_configs=['TC_RMW_INIT_INC', + 'TC_RMW_INIT_INCLUDE']) + + self.add_met_config(name='valid_beg', + data_type='string', + metplus_configs=['TC_RMW_VALID_BEG', + 'TC_RMW_VALID_BEGIN']) + + self.add_met_config(name='valid_end', + data_type='string', + metplus_configs=['TC_RMW_VALID_END']) + + self.add_met_config(name='valid_inc', + data_type='list', + env_var_name='METPLUS_VALID_INCLUDE_LIST', + metplus_configs=['TC_RMW_VALID_INCLUDE_LIST', + 'TC_RMW_VALID_INC_LIST', + 'TC_RMW_VALID_INCLUDE', + 'TC_RMW_VALID_INC', + ]) + + self.add_met_config(name='valid_exc', + data_type='list', + env_var_name='METPLUS_VALID_EXCLUDE_LIST', + metplus_configs=['TC_RMW_VALID_EXCLUDE_LIST', + 'TC_RMW_VALID_EXC_LIST', + 'TC_RMW_VALID_EXCLUDE', + 'TC_RMW_VALID_EXC', + ]) + + self.add_met_config(name='valid_hour', + data_type='list', + env_var_name='METPLUS_VALID_HOUR_LIST', + metplus_configs=['TC_RMW_VALID_HOUR_LIST', + 'TC_RMW_VALID_HOUR', + ]) + + c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, + data_type='FCST', + met_tool=self.app_name) return c_dict From cfc1d0968f33fa37e46be9ac1c56f7a30315b00a Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 6 Dec 2021 15:20:53 -0700 Subject: [PATCH 241/770] removed deprecated sections from config examples --- docs/Users_Guide/systemconfiguration.rst | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/docs/Users_Guide/systemconfiguration.rst b/docs/Users_Guide/systemconfiguration.rst index e1aeaa9389..5e96db659b 100644 --- a/docs/Users_Guide/systemconfiguration.rst +++ b/docs/Users_Guide/systemconfiguration.rst @@ -1059,7 +1059,6 @@ no space between the process name and the parenthesis. [config] PROCESS_LIST = GridStat, GridStat(my_instance_name) - [dir] GRID_STAT_OUTPUT_DIR = /grid/stat/output/dir [my_instance_name] @@ -1164,10 +1163,8 @@ CUSTOM_LOOP_LIST for that wrapper only. PCP_COMBINE_CUSTOM_LOOP_LIST = mem_001, mem_002 - [dir] FCST_PCP_COMBINE_INPUT_DIR = /d1/ensemble - [filename_templates] FCST_PCP_COMBINE_INPUT_TEMPLATE = {custom?fmt=%s}/{valid?fmt=%Y%m%d}.nc This configuration will run the following: @@ -1193,7 +1190,6 @@ This configuration will run the following: SERIES_ANALYSIS_CONFIG_FILE = {CONFIG_DIR}/SAConfig_{custom?fmt=%s} - [dir] SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/SA/{custom?fmt=%s} This configuration will run SeriesAnalysis: @@ -1470,10 +1466,9 @@ Using Templates to find Observation Data The following configuration variables describe input observation data:: - [dir] + [config] OBS_GRID_STAT_INPUT_DIR = /my/path/to/grid_stat/input/obs - [filename_templates] OBS_GRID_STAT_INPUT_TEMPLATE = {valid?fmt=%Y%m%d}/prefix.{valid?fmt=%Y%m%d%H}.ext The input directory is the top level directory containing all of the @@ -1511,10 +1506,9 @@ Most forecast files contain the initialization time and the forecast lead in the filename. The keywords 'init' and 'lead' can be used to describe the template of these files:: - [dir] + [config] FCST_GRID_STAT_INPUT_DIR = /my/path/to/grid_stat/input/fcst - [filename_templates] FCST_GRID_STAT_INPUT_TEMPLATE = prefix.{init?fmt=%Y%m%d%H}_f{lead?fmt=%3H}.ext For a valid time of 20190201_00Z and a forecast lead of 3, METplus Wrappers @@ -1533,10 +1527,8 @@ the valid time of the data. Consider the following configuration:: [config] PB2NC_OFFSETS = 6, 3 - [dir] PB2NC_INPUT_DIR = /my/path/to/prepbufr - [filename_templates] PB2NC_INPUT_TEMPLATE = prefix.{da_init?fmt=%Y%m%d}_{cycle?fmt=%H}_off{offset?fmt=%2H}.ext The PB2NC_OFFSETS list tells METplus Wrappers the order in which to @@ -1565,7 +1557,7 @@ the following day. In this example, for a run at 00Z you want to use the file from the previous day and for the 01Z to 23Z runs you want to use the file that corresponds to the current day. Here is an example:: - [filename_templates] + [config] OBS_POINT_STAT_INPUT_TEMPLATE = {valid?fmt=%Y%m%d?shift=-3600}.ext Running the above configuration at a valid time of 20190201_12Z will shift @@ -1593,10 +1585,8 @@ configuration:: OBS_FILE_WINDOW_BEGIN = -7200 OBS_FILE_WINDOW_END = 7200 - [dir] OBS_GRID_STAT_INPUT_DIR = /my/grid_stat/input/obs - [filename_templates] OBS_GRID_STAT_INPUT_TEMPLATE = {valid?fmt=%Y%m%d}/pre.{valid?fmt=%Y%m%d}_{valid?fmt=%H}.ext For a run time of 20190201_00Z, and a set of files in the input directory @@ -1710,10 +1700,8 @@ how these variables affect how the data is processed. PROCESS_LIST = SeriesAnalysis - [dir] FCST_SERIES_ANALYSIS_INPUT_DIR = /my/fcst/dir - [filename_templates] FCST_SERIES_ANALYSIS_INPUT_TEMPLATE = I{init?fmt=%Y%m%d%H}_F{lead?fmt=%3H}_V{valid?fmt=%H} In this example, the wrapper will go through all initialization and forecast From dd0d474b82e8cbfabba5945e30c92adc854c53c9 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 7 Dec 2021 12:58:15 -0700 Subject: [PATCH 242/770] minor change to METplus release guide to add a link to the PDF of the User's Guide instead of downloading it and attaching it to the release --- docs/Release_Guide/release_steps/create_release_on_github.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Release_Guide/release_steps/create_release_on_github.rst b/docs/Release_Guide/release_steps/create_release_on_github.rst index b1eea4fa3f..a20cd4f28a 100644 --- a/docs/Release_Guide/release_steps/create_release_on_github.rst +++ b/docs/Release_Guide/release_steps/create_release_on_github.rst @@ -13,7 +13,7 @@ Create Release on GitHub https://|projectRepo|.readthedocs.io/en/vX.Y.Z-betaN/Users_Guide/release-notes.html (Note: the URL will not be active until the release is created) -* Attach a PDF of the |projectRepo| User's Guide, if available. +* Add a link to the PDF of the |projectRepo| User's Guide, if available. The PDF can be downloaded from ReadTheDocs if it is available, i.e. https://|projectRepo|.readthedocs.io/_/downloads/en/vX.Y.Z-betaN/pdf/ (Note: the URL will not be active until the release is created) From 1af654cedf59dbad72487885009bd4faa8d964c2 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 16 Dec 2021 13:54:59 -0700 Subject: [PATCH 243/770] Feature 1285 extract tiles mtd times (#1315) --- .github/jobs/get_use_case_commands.py | 8 +- docs/Users_Guide/glossary.rst | 20 + docs/Users_Guide/systemconfiguration.rst | 61 ++- .../pytests/met_util/test_met_util.py | 134 ------- .../pytests/time_util/test_time_util.py | 2 +- .../string_manip/test_util_string_manip.py | 112 ++++++ .../util/time_looping/test_time_looping.py | 124 ++++++ metplus/util/__init__.py | 2 + metplus/util/config_metplus.py | 3 +- metplus/util/met_config.py | 3 +- metplus/util/met_util.py | 359 ++---------------- metplus/util/string_manip.py | 189 +++++++++ metplus/util/time_looping.py | 164 ++++++++ metplus/wrappers/command_builder.py | 50 +-- metplus/wrappers/cyclone_plotter_wrapper.py | 24 +- metplus/wrappers/extract_tiles_wrapper.py | 39 +- metplus/wrappers/gen_vx_mask_wrapper.py | 11 +- metplus/wrappers/make_plots_wrapper.py | 37 +- metplus/wrappers/pb2nc_wrapper.py | 7 +- metplus/wrappers/point_stat_wrapper.py | 3 +- metplus/wrappers/runtime_freq_wrapper.py | 140 +++---- metplus/wrappers/series_analysis_wrapper.py | 21 +- metplus/wrappers/stat_analysis_wrapper.py | 9 +- metplus/wrappers/tc_gen_wrapper.py | 8 +- metplus/wrappers/tc_pairs_wrapper.py | 27 +- metplus/wrappers/tcmpr_plotter_wrapper.py | 16 +- ...pt_obsPrecip_obsOnly_CrossSpectraPlot.conf | 41 +- ...erScript_obsPrecip_obsOnly_Hovmoeller.conf | 41 +- 28 files changed, 877 insertions(+), 778 deletions(-) create mode 100644 internal_tests/pytests/util/string_manip/test_util_string_manip.py create mode 100644 internal_tests/pytests/util/time_looping/test_time_looping.py create mode 100644 metplus/util/string_manip.py create mode 100644 metplus/util/time_looping.py diff --git a/.github/jobs/get_use_case_commands.py b/.github/jobs/get_use_case_commands.py index 70381d135d..63e86ce21e 100755 --- a/.github/jobs/get_use_case_commands.py +++ b/.github/jobs/get_use_case_commands.py @@ -129,7 +129,8 @@ def main(categories, subset_list, work_dir=None, setup_env, py_embed_arg = handle_automation_env(host_name, reqs, work_dir) - use_case_cmds = [] + # use status variable to track if any use cases failed + use_case_cmds = ['status=0'] for use_case in use_case_by_requirement.use_cases: # add parm/use_cases path to config args if they are conf files config_args = [] @@ -147,7 +148,12 @@ def main(categories, subset_list, work_dir=None, f" {py_embed_arg}{test_settings_conf}" f" config.OUTPUT_BASE={output_base}") use_case_cmds.append(use_case_cmd) + # check exit code from use case command and + # set status to non-zero value on error + use_case_cmds.append("if [ $? != 0 ]; then status=1; fi") + # if any use cases failed, force non-zero exit code with false + use_case_cmds.append("if [ $status != 0 ]; then false; fi") # add commands to set up environment before use case commands group_commands = f"{setup_env}{';'.join(use_case_cmds)}" all_commands.append((group_commands, reqs)) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 3b5cee3f5f..ca701205ce 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8529,3 +8529,23 @@ METplus Configuration Glossary Specify the value for 'obs_quality_exc' in the MET configuration file for EnsembleStat. | *Used by:* EnsembleStat + + INIT_LIST + List of initialization times to process. + This variable is used when intervals between run times are irregular. + It is only read if :term:`LOOP_BY` = INIT. If it is set, then + :term:`INIT_BEG`, :term:`INIT_END`, and :term:`INIT_INCREMENT` + are ignored. All values in the list must match the format of + :term:`INIT_TIME_FMT` or they will be skipped. + + | *Used by:* All + + VALID_LIST + List of valid times to process. + This variable is used when intervals between run times are irregular. + It is only read if :term:`LOOP_BY` = VALID. If it is set, then + :term:`VALID_BEG`, :term:`VALID_END`, and :term:`VALID_INCREMENT` + are ignored. All values in the list must match the format of + :term:`VALID_TIME_FMT` or they will be skipped. + + | *Used by:* All diff --git a/docs/Users_Guide/systemconfiguration.rst b/docs/Users_Guide/systemconfiguration.rst index 5e96db659b..1bcb65ad82 100644 --- a/docs/Users_Guide/systemconfiguration.rst +++ b/docs/Users_Guide/systemconfiguration.rst @@ -632,7 +632,9 @@ Looping by Valid Time When looping over valid time (`LOOP_BY` = VALID or REALTIME), the following variables must be set: -:term:`VALID_TIME_FMT`: +:term:`VALID_TIME_FMT` +"""""""""""""""""""""" + This is the format of the valid times the user can configure in the METplus Wrappers. The value of `VALID_BEG` and `VALID_END` must correspond to this format. @@ -644,13 +646,17 @@ Example:: Using this format, the valid time range values specified must be defined as YYYYMMDDHH, i.e. 2019020112. -:term:`VALID_BEG`: +:term:`VALID_BEG` +""""""""""""""""" + This is the first valid time that will be processed. The format of this variable is controlled by :term:`VALID_TIME_FMT`. For example, if VALID_TIME_FMT=%Y%m%d, then VALID_BEG must be set to a valid time matching YYYYMMDD, such as 20190201. -:term:`VALID_END`: +:term:`VALID_END` +""""""""""""""""" + This is the last valid time that can be processed. The format of this variable is controlled by :term:`VALID_TIME_FMT`. For example, if VALID_TIME_FMT=%Y%m%d, then VALID_END must be set to a valid time matching @@ -667,7 +673,9 @@ YYYYMMDD, such as 20190202. Wrappers will process valid times 20190201 and 20190202 before ending execution. -:term:`VALID_INCREMENT`: +:term:`VALID_INCREMENT` +""""""""""""""""""""""" + This is the time interval to add to each run time to determine the next run time to process. See :ref:`time-interval-units` for information on time interval formatting. Units of hours are assumed if no units are specified. @@ -689,6 +697,20 @@ The following is a configuration that will process valid time 2019-02-01 at This will process data valid on 2019-02-01 at 00Z, 06Z, 12Z, and 18Z as well as 2019-02-02 at 00Z. For each of these valid times, the METplus wrappers can also loop over a set of forecast leads that are all valid at the current run time. See :ref:`looping_over_forecast_leads` for more information. +:term:`VALID_LIST` +"""""""""""""""""" + +If the intervals between run times are irregular, then an explicit list of +times can be defined. The following example will process the same times +as the previous example:: + + [config] + LOOP_BY = VALID + VALID_TIME_FMT = %Y%m%d%H + VALID_LIST = 2019020100, 2019020106, 2019020112, 2019020118, 2019020200 + +See the glossary entry for :term:`VALID_LIST` for more information. + .. _Looping_by_Initialization_Time: Looping by Initialization Time @@ -696,19 +718,27 @@ Looping by Initialization Time When looping over initialization time (:term:`LOOP_BY` = INIT or LOOP_BY = RETRO), the following variables must be set: -:term:`INIT_TIME_FMT`: +:term:`INIT_TIME_FMT` +""""""""""""""""""""" + This is the format of the initialization times the user can configure in METplus Wrappers. The value of :term:`INIT_BEG` and :term:`INIT_END` must correspond to this format. Example: INIT_TIME_FMT = %Y%m%d%H. Using this format, the initialization time range values specified must be defined as YYYYMMDDHH, i.e. 2019020112. -:term:`INIT_BEG`: +:term:`INIT_BEG` +"""""""""""""""" + This is the first initialization time that will be processed. The format of this variable is controlled by :term:`INIT_TIME_FMT`. For example, if INIT_TIME_FMT = %Y%m%d, then INIT_BEG must be set to an initialization time matching YYYYMMDD, such as 20190201. -:term:`INIT_END`: +:term:`INIT_END` +"""""""""""""""" + This is the last initialization time that can be processed. The format of this variable is controlled by INIT_TIME_FMT. For example, if INIT_TIME_FMT = %Y%m%d, then INIT_END must be set to an initialization time matching YYYYMMDD, such as 20190202. .. note:: The time specified for this variable will not necessarily be processed. It is used to determine the cutoff of run times that can be processed. For example, if METplus Wrappers is configured to start at 2019-02-01 and end at 2019-02-02 processing data in 48 hour increments, it will process 2019-02-01 then increment the run time to 2019-02-03. This is later than the INIT_END valid, so execution will stop. However, if the increment is set to 24 hours (see INIT_INCREMENT), then METplus Wrappers will process initialization times 2019-02-01 and 2019-02-02 before ending executaion. -:term:`INIT_INCREMENT`: +:term:`INIT_INCREMENT` +"""""""""""""""""""""" + This is the time interval to add to each run time to determine the next run time to process. See :ref:`time-interval-units` for information on time interval formatting. Units of hours are assumed if no units are specified. This value must be greater than or equal to 60 seconds because the METplus wrappers currently do not support processing intervals of less than one minute. The following is a configuration that will process initialization time 2019-02-01 at 00Z until 2019-02-02 at 00Z in 6 hour (21600 second) increments:: @@ -725,6 +755,21 @@ The following is a configuration that will process initialization time 2019-02-0 This will process data initialized on 2019-02-01 at 00Z, 06Z, 12Z, and 18Z as well as 2019-02-02 at 00Z. For each of these initialization times, METplus Wrappers can also loop over a set of forecast leads that are all initialized at the current run time. See :ref:`looping_over_forecast_leads` for more information. +:term:`INIT_LIST` +""""""""""""""""" + +If the intervals between run times are irregular, then an explicit list of +times can be defined. The following example will process the same times +as the previous example:: + + [config] + LOOP_BY = INIT + INIT_TIME_FMT = %Y%m%d%H + INIT_LIST = 2019020100, 2019020106, 2019020112, 2019020118, 2019020200 + +See the glossary entry for :term:`INIT_LIST` for more information. + + .. _looping_over_forecast_leads: Looping over Forecast Leads diff --git a/internal_tests/pytests/met_util/test_met_util.py b/internal_tests/pytests/met_util/test_met_util.py index cb3f04af7d..7ff9bc2b74 100644 --- a/internal_tests/pytests/met_util/test_met_util.py +++ b/internal_tests/pytests/met_util/test_met_util.py @@ -4,7 +4,6 @@ import datetime import os from dateutil.relativedelta import relativedelta -from csv import reader import pprint import pytest @@ -153,31 +152,6 @@ def test_preprocess_file_options(metplus_config, result = util.preprocess_file(filename, data_type, config, allow_dir) assert(result == expected) -def test_getlist(): - l = 'gt2.7, >3.6, eq42' - test_list = util.getlist(l) - assert(test_list == ['gt2.7', '>3.6', 'eq42']) - -def test_getlist_int(): - l = '6, 7, 42' - test_list = util.getlistint(l) - assert(test_list == [6, 7, 42]) - -def test_getlist_float(): - l = '6.2, 7.8, 42.0' - test_list = util.getlistfloat(l) - assert(test_list == [6.2, 7.8, 42.0]) - -def test_getlist_has_commas(): - l = 'gt2.7, >3.6, eq42, "has,commas,in,it"' - test_list = util.getlist(l) - assert(test_list == ['gt2.7', '>3.6', 'eq42', 'has,commas,in,it']) - -def test_getlist_empty(): - l = '' - test_list = util.getlist(l) - assert(test_list == []) - def test_get_lead_sequence_lead(metplus_config): input_dict = {'valid': datetime.datetime(2019, 2, 1, 13)} conf = metplus_config() @@ -263,64 +237,6 @@ def test_get_lead_sequence_groups(metplus_config, config_dict, expected_list): assert(hour_seq == expected_list) -@pytest.mark.parametrize( - 'list_string, output_list', [ - ('begin_end_incr(3,12,3)', - ['3', '6', '9', '12']), - - ('1,2,3,4', - ['1', '2', '3', '4']), - - (' 1,2,3,4', - ['1', '2', '3', '4']), - - ('1,2,3,4 ', - ['1', '2', '3', '4']), - - (' 1,2,3,4 ', - ['1', '2', '3', '4']), - - ('1, 2,3,4', - ['1', '2', '3', '4']), - - ('1,2, 3, 4', - ['1', '2', '3', '4']), - - ('begin_end_incr( 3,12 , 3)', - ['3', '6', '9', '12']), - - ('begin_end_incr(0,10,2)', - ['0', '2', '4', '6', '8', '10']), - - ('begin_end_incr(10,0,-2)', - ['10', '8', '6', '4', '2', '0']), - - ('begin_end_incr(2,2,20)', - ['2']), - - ('begin_end_incr(0,2,1), begin_end_incr(3,9,3)', - ['0','1','2','3','6','9']), - - ('mem_begin_end_incr(0,2,1), mem_begin_end_incr(3,9,3)', - ['mem_0','mem_1','mem_2','mem_3','mem_6','mem_9']), - - ('mem_begin_end_incr(0,2,1,3), mem_begin_end_incr(3,12,3,3)', - ['mem_000', 'mem_001', 'mem_002', 'mem_003', 'mem_006', 'mem_009', 'mem_012']), - - ('begin_end_incr(0,10,2)H, 12', [ '0H', '2H', '4H', '6H', '8H', '10H', '12']), - - ('begin_end_incr(0,10800,3600)S, 4H', [ '0S', '3600S', '7200S', '10800S', '4H']), - - ('data.{init?fmt=%Y%m%d%H?shift=begin_end_incr(0, 3, 3)H}.ext', - ['data.{init?fmt=%Y%m%d%H?shift=0H}.ext', - 'data.{init?fmt=%Y%m%d%H?shift=3H}.ext', - ]), - - ] -) -def test_getlist_begin_end_incr(list_string, output_list): - assert(util.getlist(list_string) == output_list) - @pytest.mark.parametrize( 'current_hour, lead_seq', [ (0, [0, 12, 24, 36]), @@ -367,56 +283,6 @@ def test_get_lead_sequence_init_min_10(metplus_config): lead_seq = [12, 24] assert(test_seq == [relativedelta(hours=lead) for lead in lead_seq]) -@pytest.mark.parametrize( - 'time_from_conf, fmt, is_datetime', [ - ('', '%Y', False), - ('a', '%Y', False), - ('1987', '%Y', True), - ('1987', '%Y%m', False), - ('198702', '%Y%m', True), - ('198702', '%Y%m%d', False), - ('19870201', '%Y%m%d', True), - ('19870201', '%Y%m%d%H', False), - ('{now?fmt=%Y%m%d}', '%Y%m%d', True), - ('{now?fmt=%Y%m%d}', '%Y%m%d%H', True), - ('{now?fmt=%Y%m%d}00', '%Y%m%d%H', True), - ('{today}', '%Y%m%d', True), - ('{today}', '%Y%m%d%H', True), - ] -) -def test_get_time_obj(time_from_conf, fmt, is_datetime): - clock_time = datetime.datetime(2019, 12, 31, 15, 30) - - time_obj = util.get_time_obj(time_from_conf, fmt, clock_time) - - assert(isinstance(time_obj, datetime.datetime) == is_datetime) - -@pytest.mark.parametrize( - 'list_str, expected_fixed_list', [ - ('some,items,here', ['some', - 'items', - 'here']), - ('(*,*)', ['(*,*)']), - ("-type solar_alt -thresh 'ge45' -name solar_altitude_ge_45_mask -input_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;' -mask_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;\'", - ["-type solar_alt -thresh 'ge45' -name solar_altitude_ge_45_mask -input_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;' -mask_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;\'"]), - ("(*,*),'level=\"(0,*,*)\"' -censor_thresh [lt12.3,gt8.8],other", ['(*,*)', - "'level=\"(0,*,*)\"' -censor_thresh [lt12.3,gt8.8]", - 'other']), - ] -) -def test_fix_list(list_str, expected_fixed_list): - item_list = list(reader([list_str]))[0] - fixed_list = util.fix_list(item_list) - print("FIXED LIST:") - for fixed in fixed_list: - print(f"ITEM: {fixed}") - - print("EXPECTED LIST") - for expected in expected_fixed_list: - print(f"ITEM: {expected}") - - assert(fixed_list == expected_fixed_list) - @pytest.mark.parametrize( 'camel, underscore', [ ('ASCII2NCWrapper', 'ascii2nc_wrapper'), diff --git a/internal_tests/pytests/time_util/test_time_util.py b/internal_tests/pytests/time_util/test_time_util.py index 0954df4a71..bec515faff 100644 --- a/internal_tests/pytests/time_util/test_time_util.py +++ b/internal_tests/pytests/time_util/test_time_util.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 import sys import pytest diff --git a/internal_tests/pytests/util/string_manip/test_util_string_manip.py b/internal_tests/pytests/util/string_manip/test_util_string_manip.py new file mode 100644 index 0000000000..9ceab10ed3 --- /dev/null +++ b/internal_tests/pytests/util/string_manip/test_util_string_manip.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +import pytest + +from csv import reader + +from metplus.util.string_manip import * +from metplus.util.string_manip import _fix_list + +def test_getlist(): + string_list = 'gt2.7, >3.6, eq42' + test_list = getlist(string_list) + assert test_list == ['gt2.7', '>3.6', 'eq42'] + +def test_getlist_int(): + string_list = '6, 7, 42' + test_list = getlistint(string_list) + assert test_list == [6, 7, 42] + +def test_getlist_has_commas(): + string_list = 'gt2.7, >3.6, eq42, "has,commas,in,it"' + test_list = getlist(string_list) + assert test_list == ['gt2.7', '>3.6', 'eq42', 'has,commas,in,it'] + +def test_getlist_empty(): + string_list = '' + test_list = getlist(string_list) + assert test_list == [] + +@pytest.mark.parametrize( + 'list_string, output_list', [ + ('begin_end_incr(3,12,3)', + ['3', '6', '9', '12']), + + ('1,2,3,4', + ['1', '2', '3', '4']), + + (' 1,2,3,4', + ['1', '2', '3', '4']), + + ('1,2,3,4 ', + ['1', '2', '3', '4']), + + (' 1,2,3,4 ', + ['1', '2', '3', '4']), + + ('1, 2,3,4', + ['1', '2', '3', '4']), + + ('1,2, 3, 4', + ['1', '2', '3', '4']), + + ('begin_end_incr( 3,12 , 3)', + ['3', '6', '9', '12']), + + ('begin_end_incr(0,10,2)', + ['0', '2', '4', '6', '8', '10']), + + ('begin_end_incr(10,0,-2)', + ['10', '8', '6', '4', '2', '0']), + + ('begin_end_incr(2,2,20)', + ['2']), + + ('begin_end_incr(0,2,1), begin_end_incr(3,9,3)', + ['0','1','2','3','6','9']), + + ('mem_begin_end_incr(0,2,1), mem_begin_end_incr(3,9,3)', + ['mem_0','mem_1','mem_2','mem_3','mem_6','mem_9']), + + ('mem_begin_end_incr(0,2,1,3), mem_begin_end_incr(3,12,3,3)', + ['mem_000', 'mem_001', 'mem_002', 'mem_003', 'mem_006', 'mem_009', 'mem_012']), + + ('begin_end_incr(0,10,2)H, 12', [ '0H', '2H', '4H', '6H', '8H', '10H', '12']), + + ('begin_end_incr(0,10800,3600)S, 4H', [ '0S', '3600S', '7200S', '10800S', '4H']), + + ('data.{init?fmt=%Y%m%d%H?shift=begin_end_incr(0, 3, 3)H}.ext', + ['data.{init?fmt=%Y%m%d%H?shift=0H}.ext', + 'data.{init?fmt=%Y%m%d%H?shift=3H}.ext', + ]), + + ] +) +def test_getlist_begin_end_incr(list_string, output_list): + assert getlist(list_string) == output_list + +@pytest.mark.parametrize( + 'list_str, expected_fixed_list', [ + ('some,items,here', ['some', + 'items', + 'here']), + ('(*,*)', ['(*,*)']), + ("-type solar_alt -thresh 'ge45' -name solar_altitude_ge_45_mask -input_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;' -mask_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;\'", + ["-type solar_alt -thresh 'ge45' -name solar_altitude_ge_45_mask -input_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;' -mask_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;\'"]), + ("(*,*),'level=\"(0,*,*)\"' -censor_thresh [lt12.3,gt8.8],other", ['(*,*)', + "'level=\"(0,*,*)\"' -censor_thresh [lt12.3,gt8.8]", + 'other']), + ] +) +def test_fix_list(list_str, expected_fixed_list): + item_list = list(reader([list_str]))[0] + fixed_list = _fix_list(item_list) + print("FIXED LIST:") + for fixed in fixed_list: + print(f"ITEM: {fixed}") + + print("EXPECTED LIST") + for expected in expected_fixed_list: + print(f"ITEM: {expected}") + + assert(fixed_list == expected_fixed_list) diff --git a/internal_tests/pytests/util/time_looping/test_time_looping.py b/internal_tests/pytests/util/time_looping/test_time_looping.py new file mode 100644 index 0000000000..7326734b48 --- /dev/null +++ b/internal_tests/pytests/util/time_looping/test_time_looping.py @@ -0,0 +1,124 @@ +import pytest + +from metplus.util.time_looping import * + +def test_time_generator_list(metplus_config): + for prefix in ['INIT', 'VALID']: + config = metplus_config() + config.set('config', 'LOOP_BY', prefix) + config.set('config', f'{prefix}_TIME_FMT', '%Y%m%d%H') + config.set('config', f'{prefix}_LIST', '2021020104, 2021103121') + + expected_times = [ + datetime.strptime('2021020104', '%Y%m%d%H'), + datetime.strptime('2021103121', '%Y%m%d%H'), + ] + + generator = time_generator(config) + assert next(generator)[prefix.lower()] == expected_times[0] + assert next(generator)[prefix.lower()] == expected_times[1] + try: + next(generator) + assert False + except StopIteration: + assert True + +def test_time_generator_increment(metplus_config): + for prefix in ['INIT', 'VALID']: + config = metplus_config() + config.set('config', 'LOOP_BY', prefix) + config.set('config', f'{prefix}_TIME_FMT', '%Y%m%d%H') + config.set('config', f'{prefix}_BEG', '2021020104') + config.set('config', f'{prefix}_END', '2021020106') + config.set('config', f'{prefix}_INCREMENT', '1H') + + expected_times = [ + datetime.strptime('2021020104', '%Y%m%d%H'), + datetime.strptime('2021020105', '%Y%m%d%H'), + datetime.strptime('2021020106', '%Y%m%d%H'), + ] + + generator = time_generator(config) + assert next(generator)[prefix.lower()] == expected_times[0] + assert next(generator)[prefix.lower()] == expected_times[1] + assert next(generator)[prefix.lower()] == expected_times[2] + try: + next(generator) + assert False + except StopIteration: + assert True + +def test_time_generator_error_check(metplus_config): + """! Test that None is returned by the time generator when + the time looping config variables are not set properly. Tests: + Missing LOOP_BY, + Missing [INIT/VALID]_TIME_FMT, + Empty [INIT/VALID]_LIST (if set), + List value doesn't match time format, + _BEG or _END value doesn't match format, + _INCREMENT is less than 60 seconds, + _BEG is after _END, + """ + time_fmt = '%Y%m%d%H' + for prefix in ['INIT', 'VALID']: + config = metplus_config() + + # unset LOOP_BY + assert next(time_generator(config)) is None + config.set('config', 'LOOP_BY', prefix) + + # unset _TIME_FMT + assert next(time_generator(config)) is None + config.set('config', f'{prefix}_TIME_FMT', time_fmt) + + # test [INIT/VALID]_LIST configurations + + # empty _LIST + config.set('config', f'{prefix}_LIST', '') + assert next(time_generator(config)) is None + + # list value doesn't match format + config.set('config', f'{prefix}_LIST', '202102010412') + assert next(time_generator(config)) is None + + # 2nd list value doesn't match format + config.set('config', f'{prefix}_LIST', '2021020104, 202102010412') + expected_time = datetime.strptime('2021020104', time_fmt) + generator = time_generator(config) + assert next(generator)[prefix.lower()] == expected_time + assert next(generator) is None + + # good _LIST + config.set('config', f'{prefix}_LIST', '2021020104') + assert next(time_generator(config))[prefix.lower()] == expected_time + + # get a fresh config object to test BEG/END configurations + config = metplus_config() + config.set('config', 'LOOP_BY', prefix) + config.set('config', f'{prefix}_TIME_FMT', time_fmt) + + # _BEG doesn't match time format (too long) + config.set('config', f'{prefix}_BEG', '202110311259') + config.set('config', f'{prefix}_END', '2021112012') + + assert next(time_generator(config)) is None + config.set('config', f'{prefix}_BEG', '2021103112') + + # unset _END uses _BEG value, so it should succeed + assert next(time_generator(config)) is not None + + # _END doesn't match time format (too long) + config.set('config', f'{prefix}_END', '202111201259') + + assert next(time_generator(config)) is None + config.set('config', f'{prefix}_END', '2021112012') + assert next(time_generator(config)) is not None + + # _INCREMENT is less than 60 seconds + config.set('config', f'{prefix}_INCREMENT', '10S') + assert next(time_generator(config)) is None + config.set('config', f'{prefix}_INCREMENT', '1d') + + # _END time comes before _BEG time + config.set('config', f'{prefix}_END', '2020112012') + assert next(time_generator(config)) is None diff --git a/metplus/util/__init__.py b/metplus/util/__init__.py index b124dcb5b8..368f1caae0 100644 --- a/metplus/util/__init__.py +++ b/metplus/util/__init__.py @@ -1,4 +1,5 @@ from .constants import * +from .string_manip import * from .metplus_check import * from .doc_util import * from .config_metplus import * @@ -6,3 +7,4 @@ from .met_util import * from .string_template_substitution import * from .met_config import * +from .time_looping import * diff --git a/metplus/util/config_metplus.py b/metplus/util/config_metplus.py index 892990994e..e2cc3981cd 100644 --- a/metplus/util/config_metplus.py +++ b/metplus/util/config_metplus.py @@ -22,7 +22,8 @@ from . import met_util as util from .string_template_substitution import get_tags, do_string_sub -from .met_util import getlist, is_python_script, format_var_items +from .met_util import is_python_script, format_var_items +from .string_manip import getlist from .doc_util import get_wrapper_name """!Creates the initial METplus directory structure, diff --git a/metplus/util/met_config.py b/metplus/util/met_config.py index 2e7c54ad00..847eb43e21 100644 --- a/metplus/util/met_config.py +++ b/metplus/util/met_config.py @@ -5,7 +5,8 @@ import os -from .met_util import getlist, get_threshold_via_regex, MISSING_DATA_VALUE +from .string_manip import getlist +from .met_util import get_threshold_via_regex, MISSING_DATA_VALUE from .met_util import remove_quotes as util_remove_quotes from .config_metplus import find_indices_in_config_section diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index 5b853807fd..aed1e225b1 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -7,14 +7,15 @@ import bz2 import zipfile import struct -from csv import reader from dateutil.relativedelta import relativedelta from pathlib import Path from importlib import import_module +from .string_manip import getlist, getlistint from .string_template_substitution import do_string_sub from .string_template_substitution import parse_template from . import time_util as time_util +from .time_looping import time_generator from .. import get_metplus_version """!@namespace met_util @@ -370,102 +371,6 @@ def is_loop_by_init(config): return None -def get_time_obj(time_from_conf, fmt, clock_time, logger=None, warn=False): - """!Substitute today or now into [INIT/VALID]_[BEG/END] if used - Args: - @param time_from_conf value from [INIT/VALID]_[BEG/END] that - may include now or today tags - @param fmt format of time_from_conf, i.e. %Y%m%d - @param clock_time datetime object for time when execution started - @param logger log object to write error messages - None if not provided - @returns datetime object if successful, None if not - """ - time_str = do_string_sub(time_from_conf, - now=clock_time, - today=clock_time.strftime('%Y%m%d')) - try: - time_t = datetime.datetime.strptime(time_str, fmt) - except ValueError: - error_message = (f"[INIT/VALID]_TIME_FMT ({fmt}) does not match " - f"[INIT/VALID]_[BEG/END] ({time_str})") - if logger: - if warn: - logger.warning(error_message) - else: - logger.error(error_message) - else: - print(f"ERROR: {error_message}") - - return None - - return time_t - -def get_start_end_interval_times(config, warn=False): - """! Reads the METplusConfig object to determine the start, end, and - increment values based on the configuration. Based on the LOOP_BY value, - it will read the INIT_ or VALID_ variables TIME_FMT, BEG, END, and - INCREMENT and use the time format value to parse the other values. - - @param config METplusConfig object to parse - @parm warn (optional) if True, output warnings instead of errors - @returns tuple of start time (datetime), end time (datetime) and - increment (dateutil.relativedelta) or all None values if time info - could not be parsed properly - """ - # set function to send log messages (warning or error) - if warn: - log_function = config.logger.warning - else: - log_function = config.logger.error - - clock_time_obj = datetime.datetime.strptime(config.getstr('config', - 'CLOCK_TIME'), - '%Y%m%d%H%M%S') - use_init = is_loop_by_init(config) - if use_init is None: - return None, None, None - - if use_init: - time_format = config.getstr('config', 'INIT_TIME_FMT') - start_t = config.getraw('config', 'INIT_BEG') - end_t = config.getraw('config', 'INIT_END', start_t) - time_interval = time_util.get_relativedelta( - config.getstr('config', 'INIT_INCREMENT', '60') - ) - else: - time_format = config.getstr('config', 'VALID_TIME_FMT') - start_t = config.getraw('config', 'VALID_BEG') - end_t = config.getraw('config', 'VALID_END', start_t) - time_interval = time_util.get_relativedelta( - config.getstr('config', 'VALID_INCREMENT', '60') - ) - - start_time = get_time_obj(start_t, time_format, - clock_time_obj, config.logger, - warn=warn) - if not start_time: - log_function("Could not format start time") - return None, None, None - - end_time = get_time_obj(end_t, time_format, - clock_time_obj, config.logger, - warn=warn) - if not end_time: - log_function("Could not format end time") - return None, None, None - - if (start_time + time_interval < - start_time + datetime.timedelta(seconds=60)): - log_function('[INIT/VALID]_INCREMENT must be greater than or ' - 'equal to 60 seconds') - return None, None, None - - if start_time > end_time: - log_function("Start time must come before end time") - return None, None, None - - return start_time, end_time, time_interval - def loop_over_times_and_call(config, processes): """! Loop over all run times and call wrappers listed in config @@ -474,79 +379,55 @@ def loop_over_times_and_call(config, processes): @returns list of tuples with all commands run and the environment variables that were set for each """ - use_init = is_loop_by_init(config) - if use_init is None: - return None - - # get start time, end time, and time interval from config - loop_time, end_time, time_interval = get_start_end_interval_times(config) - if not loop_time: - config.logger.error("Could not get [INIT/VALID] time information from configuration file") - return None - # keep track of commands that were run all_commands = [] - while loop_time <= end_time: - log_runtime_banner(loop_time, config, use_init) + for time_input in time_generator(config): if not isinstance(processes, list): processes = [processes] + for process in processes: - input_dict = set_input_dict(loop_time, - config, - use_init, - instance=process.instance) + # if time could not be read, increment errors for each process + if time_input is None: + process.errors += 1 + continue + + log_runtime_banner(config, time_input, process) + add_to_time_input(time_input, + instance=process.instance) process.clear() - process.run_at_time(input_dict) + process.run_at_time(time_input) if process.all_commands: all_commands.extend(process.all_commands) process.all_commands.clear() - loop_time += time_interval - return all_commands -def log_runtime_banner(loop_time, config, use_init): - run_time = loop_time.strftime("%Y-%m-%d %H:%M") - config.logger.info("****************************************") - config.logger.info("* Running METplus") - if use_init: - config.logger.info("* at init time: " + run_time) - else: - config.logger.info("* at valid time: " + run_time) - config.logger.info("****************************************") +def log_runtime_banner(config, time_input, process): + loop_by = time_input['loop_by'] + run_time = time_input[loop_by].strftime("%Y-%m-%d %H:%M") -def set_input_dict(loop_time, config, use_init, instance=None, custom=None): - """! Create input dictionary, set key 'now' to clock time in - YYYYMMDDHHMMSS, set key 'init' to loop_time value if use_init is True, - set key 'valid' to loop_time value if use_init is False, do not set - either if use_init is None + process_name = process.__class__.__name__ + if process.instance: + process_name = f"{process_name}({process.instance})" - @param loop_time datetime object of current runtime - @param config METplusConfig object used to read CLOCK_TIME - @param use_init True if looping by init, False if looping by valid, - None otherwise - """ - input_dict = {} - clock_time_obj = datetime.datetime.strptime(config.getstr('config', - 'CLOCK_TIME'), - '%Y%m%d%H%M%S') - input_dict['now'] = clock_time_obj + config.logger.info("****************************************") + config.logger.info(f"* Running METplus {process_name}") + config.logger.info(f"* at {loop_by} time: {run_time}") + config.logger.info("****************************************") - if use_init: - input_dict['init'] = loop_time - elif use_init is not None: - input_dict['valid'] = loop_time +def add_to_time_input(time_input, clock_time=None, instance=None, custom=None): + if clock_time: + clock_dt = datetime.datetime.strptime(clock_time, '%Y%m%d%H%M%S') + time_input['now'] = clock_dt # if instance is set, use that value, otherwise use empty string - input_dict['instance'] = instance if instance else '' + time_input['instance'] = instance if instance else '' - # if custom is specified, set it, otherwise leave it unset so it can be - # set within the wrapper + # if custom is specified, set it + # otherwise leave it unset so it can be set within the wrapper if custom: - input_dict['custom'] = custom - - return input_dict + time_input['custom'] = custom def get_lead_sequence(config, input_dict=None, wildcard_if_empty=False): """!Get forecast lead list from LEAD_SEQ or compute it from INIT_SEQ. @@ -895,184 +776,6 @@ def prune_empty(output_dir, logger): "...removing") os.rmdir(full_dir) -def handle_begin_end_incr(list_str): - """!Check for instances of begin_end_incr() in the input string and evaluate as needed - Args: - @param list_str string that contains a comma separated list - @returns string that has list expanded""" - - matches = begin_end_incr_findall(list_str) - - for match in matches: - item_list = begin_end_incr_evaluate(match) - if item_list: - list_str = list_str.replace(match, ','.join(item_list)) - - return list_str - -def begin_end_incr_findall(list_str): - """!Find all instances of begin_end_incr in list string - Args: - @param list_str string that contains a comma separated list - @returns list of strings that have begin_end_incr() characters""" - # remove space around commas (again to make sure) - # this makes the regex slightly easier because we don't have to include - # as many \s* instances in the regex string - list_str = re.sub(r'\s*,\s*', ',', list_str) - - # find begin_end_incr and any text before and after that are not a comma - # [^,\s]* evaluates to any character that is not a comma or space - return re.findall(r"([^,]*begin_end_incr\(\s*-?\d*,-?\d*,-*\d*,?\d*\s*\)[^,]*)", - list_str) - -def begin_end_incr_evaluate(item): - """!Expand begin_end_incr() items into a list of values - Args: - @param item string containing begin_end_incr() tag with - possible text before and after - @returns list of items expanded from begin_end_incr - """ - match = re.match(r"^(.*)begin_end_incr\(\s*(-*\d*),(-*\d*),(-*\d*),?(\d*)\s*\)(.*)$", - item) - if match: - before = match.group(1).strip() - after = match.group(6).strip() - start = int(match.group(2)) - end = int(match.group(3)) - step = int(match.group(4)) - precision = match.group(5).strip() - - if start <= end: - int_list = range(start, end+1, step) - else: - int_list = range(start, end-1, step) - - out_list = [] - for int_values in int_list: - out_str = str(int_values) - - if precision: - out_str = out_str.zfill(int(precision)) - - out_list.append(f"{before}{out_str}{after}") - - return out_list - - return None - -def fix_list(item_list): - item_list = fix_list_helper(item_list, '(') - item_list = fix_list_helper(item_list, '[') - return item_list - -def fix_list_helper(item_list, type): - if type == '(': - close_regex = r"[^(]+\).*" - open_regex = r".*\([^)]*$" - elif type == '[': - close_regex = r"[^\[]+\].*" - open_regex = r".*\[[^\]]*$" - else: - return item_list - - # combine items that had a comma between ()s or []s - fixed_list = [] - incomplete_item = None - found_close = False - for index, item in enumerate(item_list): - # if we have found an item that ends with ( but - if incomplete_item: - # check if item has ) before ( - match = re.match(close_regex, item) - if match: - # add rest of text, add it to output list, then reset incomplete_item - incomplete_item += ',' + item - found_close = True - else: - # if not ) before (, add text and continue - incomplete_item += ',' + item - - match = re.match(open_regex, item) - # if we find ( without ) after it - if match: - # if we are still putting together an item, append comma and new item - if incomplete_item: - if not found_close: - incomplete_item += ',' + item - # if not, start new incomplete item to put together - else: - incomplete_item = item - - found_close = False - # if we don't find ( without ) - else: - # if we are putting together item, we can add to the output list and reset incomplete_item - if incomplete_item: - if found_close: - fixed_list.append(incomplete_item) - incomplete_item = None - # if we are not within brackets and we found no brackets, add item to output list - else: - fixed_list.append(item) - - return fixed_list - -def getlist(list_str, expand_begin_end_incr=True): - """! Returns a list of string elements from a comma - separated string of values. - This function MUST also return an empty list [] if s is '' empty. - This function is meant to handle these possible or similar inputs: - AND return a clean list with no surrounding spaces or trailing - commas in the elements. - '4,4,2,4,2,4,2, ' or '4,4,2,4,2,4,2 ' or - '4, 4, 4, 4, ' or '4, 4, 4, 4 ' - Note: getstr on an empty variable (EMPTY_VAR = ) in - a conf file returns '' an empty string. - - @param list_str the string being converted to a list. - @returns list of strings formatted properly and expanded as needed - """ - if not list_str: - return [] - - # FIRST remove surrounding comma, and spaces, form the string. - list_str = list_str.strip(';[] ').strip().strip(',').strip() - - # remove space around commas - list_str = re.sub(r'\s*,\s*', ',', list_str) - - # option to not evaluate begin_end_incr - if expand_begin_end_incr: - list_str = handle_begin_end_incr(list_str) - - # use csv reader to divide comma list while preserving strings with comma - # convert the csv reader to a list and get first item (which is the whole list) - item_list = list(reader([list_str], escapechar='\\'))[0] - - item_list = fix_list(item_list) - - return item_list - -def getlistfloat(list_str): - """!Get list and convert all values to float - Args: - @param list_str the string being converted to a list. - @returns list of floats - """ - list_str = getlist(list_str) - list_str = [float(i) for i in list_str] - return list_str - -def getlistint(list_str): - """!Get list and convert all values to int - Args: - @param list_str the string being converted to a list. - @returns list of ints - """ - list_str = getlist(list_str) - list_str = [int(i) for i in list_str] - return list_str - def camel_to_underscore(camel): """! Change camel case notation to underscore notation, i.e. GridStatWrapper to grid_stat_wrapper Multiple capital letters are excluded, i.e. PCPCombineWrapper to pcp_combine_wrapper diff --git a/metplus/util/string_manip.py b/metplus/util/string_manip.py new file mode 100644 index 0000000000..f6326d50fb --- /dev/null +++ b/metplus/util/string_manip.py @@ -0,0 +1,189 @@ +""" +Program Name: string_manip.py +Contact(s): George McCabe +Description: METplus utility to handle string manipulation +""" + +import re +from csv import reader + +def getlist(list_str, expand_begin_end_incr=True): + """! Returns a list of string elements from a comma + separated string of values. + This function MUST also return an empty list [] if s is '' empty. + This function is meant to handle these possible or similar inputs: + AND return a clean list with no surrounding spaces or trailing + commas in the elements. + '4,4,2,4,2,4,2, ' or '4,4,2,4,2,4,2 ' or + '4, 4, 4, 4, ' or '4, 4, 4, 4 ' + Note: getstr on an empty variable (EMPTY_VAR = ) in + a conf file returns '' an empty string. + + @param list_str the string being converted to a list. + @returns list of strings formatted properly and expanded as needed + """ + if not list_str: + return [] + + # FIRST remove surrounding comma, and spaces, form the string. + list_str = list_str.strip(';[] ').strip().strip(',').strip() + + # remove space around commas + list_str = re.sub(r'\s*,\s*', ',', list_str) + + # option to not evaluate begin_end_incr + if expand_begin_end_incr: + list_str = _handle_begin_end_incr(list_str) + + # use csv reader to divide comma list while preserving strings with comma + # convert the csv reader to a list and get first item + # (which is the whole list) + item_list = list(reader([list_str], escapechar='\\'))[0] + + item_list = _fix_list(item_list) + + return item_list + +def getlistint(list_str): + """! Get list and convert all values to int + + @param list_str the string being converted to a list. + @returns list of ints + """ + list_str = getlist(list_str) + list_str = [int(i) for i in list_str] + return list_str + + +def _handle_begin_end_incr(list_str): + """! Check for instances of begin_end_incr() in the input string and + evaluate as needed + + @param list_str string that contains a comma separated list + @returns string that has list expanded + """ + + matches = _begin_end_incr_findall(list_str) + + for match in matches: + item_list = _begin_end_incr_evaluate(match) + if item_list: + list_str = list_str.replace(match, ','.join(item_list)) + + return list_str + +def _begin_end_incr_findall(list_str): + """! Find all instances of begin_end_incr in list string + + @param list_str string that contains a comma separated list + @returns list of strings that have begin_end_incr() characters + """ + # remove space around commas (again to make sure) + # this makes the regex slightly easier because we don't have to include + # as many \s* instances in the regex string + list_str = re.sub(r'\s*,\s*', ',', list_str) + + # find begin_end_incr and any text before and after that are not a comma + # [^,\s]* evaluates to any character that is not a comma or space + return re.findall( + r"([^,]*begin_end_incr\(\s*-?\d*,-?\d*,-*\d*,?\d*\s*\)[^,]*)", + list_str + ) + +def _begin_end_incr_evaluate(item): + """! Expand begin_end_incr() items into a list of values + + @param item string containing begin_end_incr() tag with + possible text before and after + @returns list of items expanded from begin_end_incr + """ + match = re.match( + r"^(.*)begin_end_incr\(\s*(-*\d*),(-*\d*),(-*\d*),?(\d*)\s*\)(.*)$", + item + ) + if match: + before = match.group(1).strip() + after = match.group(6).strip() + start = int(match.group(2)) + end = int(match.group(3)) + step = int(match.group(4)) + precision = match.group(5).strip() + + if start <= end: + int_list = range(start, end+1, step) + else: + int_list = range(start, end-1, step) + + out_list = [] + for int_values in int_list: + out_str = str(int_values) + + if precision: + out_str = out_str.zfill(int(precision)) + + out_list.append(f"{before}{out_str}{after}") + + return out_list + + return None + +def _fix_list(item_list): + item_list = _fix_list_helper(item_list, '(') + item_list = _fix_list_helper(item_list, '[') + return item_list + +def _fix_list_helper(item_list, type): + if type == '(': + close_regex = r"[^(]+\).*" + open_regex = r".*\([^)]*$" + elif type == '[': + close_regex = r"[^\[]+\].*" + open_regex = r".*\[[^\]]*$" + else: + return item_list + + # combine items that had a comma between ()s or []s + fixed_list = [] + incomplete_item = None + found_close = False + for index, item in enumerate(item_list): + # if we have found an item that ends with ( but + if incomplete_item: + # check if item has ) before ( + match = re.match(close_regex, item) + if match: + # add rest of text, add it to output list, + # then reset incomplete_item + incomplete_item += ',' + item + found_close = True + else: + # if not ) before (, add text and continue + incomplete_item += ',' + item + + match = re.match(open_regex, item) + # if we find ( without ) after it + if match: + # if we are still putting together an item, + # append comma and new item + if incomplete_item: + if not found_close: + incomplete_item += ',' + item + # if not, start new incomplete item to put together + else: + incomplete_item = item + + found_close = False + # if we don't find ( without ) + else: + # if we are putting together item, we can add to the + # output list and reset incomplete_item + if incomplete_item: + if found_close: + fixed_list.append(incomplete_item) + incomplete_item = None + # if we are not within brackets and we found no brackets, + # add item to output list + else: + fixed_list.append(item) + + return fixed_list diff --git a/metplus/util/time_looping.py b/metplus/util/time_looping.py new file mode 100644 index 0000000000..0632b17639 --- /dev/null +++ b/metplus/util/time_looping.py @@ -0,0 +1,164 @@ +from datetime import datetime, timedelta + +from .string_manip import getlist +from .time_util import get_relativedelta +from .string_template_substitution import do_string_sub + +def time_generator(config): + """! Generator used to read METplusConfig variables for time looping + + @param METplusConfig object to read + @returns None if not enough information is available on config. + Yields the next run time dictionary or None if something went wrong + """ + # determine INIT or VALID prefix + prefix = get_time_prefix(config) + if not prefix: + yield None + return + + # get clock time of when the run started + clock_dt = datetime.strptime( + config.getstr('config', 'CLOCK_TIME'), + '%Y%m%d%H%M%S' + ) + + time_format = config.getraw('config', f'{prefix}_TIME_FMT', '') + if not time_format: + config.logger.error(f'Could not read {prefix}_TIME_FMT') + yield None + return + + # check for [INIT/VALID]_LIST and use that list if set + if config.has_option('config', f'{prefix}_LIST'): + time_list = getlist(config.getraw('config', f'{prefix}_LIST')) + if not time_list: + config.logger.error(f"Could not read {prefix}_LIST") + yield None + return + + for time_string in time_list: + current_dt = _get_current_dt(time_string, + time_format, + clock_dt, + config.logger) + if not current_dt: + yield None + + time_info = _create_time_input_dict(prefix, current_dt, clock_dt) + yield time_info + + return + + # if list is not provided, use _BEG, _END, and _INCREMENT + start_string = config.getraw('config', f'{prefix}_BEG') + end_string = config.getraw('config', f'{prefix}_END', start_string) + time_interval = get_relativedelta( + config.getstr('config', f'{prefix}_INCREMENT', '60') + ) + + start_dt = _get_current_dt(start_string, + time_format, + clock_dt, + config.logger) + + end_dt = _get_current_dt(end_string, + time_format, + clock_dt, + config.logger) + + if not _validate_time_values(start_dt, + end_dt, + time_interval, + prefix, + config.logger): + yield None + return + + current_dt = start_dt + while current_dt <= end_dt: + time_info = _create_time_input_dict(prefix, current_dt, clock_dt) + yield time_info + + current_dt += time_interval + +def _validate_time_values(start_dt, end_dt, time_interval, prefix, logger): + if not start_dt: + logger.error(f"Could not read {prefix}_BEG") + return False + + if not end_dt: + logger.error(f"Could not read {prefix}_END") + return False + + # check that time increment is at least 60 seconds + if (start_dt + time_interval < + start_dt + timedelta(seconds=60)): + logger.error(f'{prefix}_INCREMENT must be greater than or ' + 'equal to 60 seconds') + return False + + if start_dt > end_dt: + logger.error(f"{prefix}_BEG must come after {prefix}_END ") + return False + + return True + +def _create_time_input_dict(prefix, current_dt, clock_dt): + return { + 'loop_by': prefix.lower(), + prefix.lower(): current_dt, + 'now': clock_dt, + } + +def get_time_prefix(config): + """! Read the METplusConfig object and determine the prefix for the time + looping variables. + + @param config METplusConfig object to read + @returns string 'INIT' if looping by init time, 'VALID' if looping by + valid time, or None if not enough information was found in the config + """ + loop_by = config.getstr('config', 'LOOP_BY', '').upper() + if loop_by in ['INIT', 'RETRO']: + return 'INIT' + + if loop_by in ['VALID', 'REALTIME']: + return 'VALID' + + # check for legacy variable LOOP_BY_INIT if LOOP_BY is not set properly + if config.has_option('config', 'LOOP_BY_INIT'): + if config.getbool('config', 'LOOP_BY_INIT'): + return 'INIT' + + return 'VALID' + + # report an error if time prefix could not be determined + config.logger.error('MUST SET LOOP_BY to VALID, INIT, RETRO, or REALTIME') + return None + +def _get_current_dt(time_string, time_format, clock_dt, logger): + """! Use time format to get datetime object from time string, substituting + values for today or now template tags if specified. + + @param time_string string value read from the config that + may include now or today tags + @param time_format format of time_string, i.e. %Y%m%d + @param clock_dt datetime object for time when execution started + @returns datetime object if successful, None if not + """ + subbed_time_string = do_string_sub( + time_string, + now=clock_dt, + today=clock_dt.strftime('%Y%m%d') + ) + try: + current_dt = datetime.strptime(subbed_time_string, time_format) + except ValueError: + logger.error( + f'Could not format time string ({time_string}) using ' + f'time format ({time_format})' + ) + return None + + return current_dt diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index d62707317b..3835d8b130 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -18,6 +18,7 @@ import re from .command_runner import CommandRunner +from ..util import getlist from ..util import met_util as util from ..util import do_string_sub, ti_calculate, get_seconds_from_string from ..util import get_time_from_file @@ -623,7 +624,7 @@ def find_exact_file(self, level, data_type, time_info, mandatory=True, # check if there is a list of files provided in the template # process each template in the list (or single template) - template_list = util.getlist(input_template) + template_list = getlist(input_template) # return None if a list is provided for a wrapper that doesn't allow # multiple files to be processed @@ -1400,31 +1401,6 @@ def run_all_times(self): call METplus wrapper for each time""" return util.loop_over_times_and_call(self.config, self) - def set_time_dict_for_single_runtime(self): - # get clock time from start of execution for input time dictionary - clock_time_obj = datetime.strptime(self.config.getstr('config', - 'CLOCK_TIME'), - '%Y%m%d%H%M%S') - - # get start run time and set INPUT_TIME_DICT - time_info = {'now': clock_time_obj} - start_time, _, _ = util.get_start_end_interval_times(self.config) - if start_time: - # set init or valid based on LOOP_BY - use_init = util.is_loop_by_init(self.config) - if use_init is None: - return None - elif use_init: - time_info['init'] = start_time - else: - time_info['valid'] = start_time - else: - self.config.logger.error("Could not get [INIT/VALID] time " - "information from configuration file") - return None - - return time_info - @staticmethod def format_met_config_dict(c_dict, name, keys=None): """! Return formatted dictionary named with any if they @@ -1600,7 +1576,7 @@ def read_climo_file_name(self, climo_type): # if dir is set and not python embedding, # prepend it to each template in list if input_dir and input_template not in util.PYTHON_EMBEDDING_TYPES: - template_list = util.getlist(input_template) + template_list = getlist(input_template) for index, template in enumerate(template_list): template_list[index] = os.path.join(input_dir, template) @@ -1811,23 +1787,3 @@ def get_config_file(self, default_config_file=None): return get_wrapped_met_config_file(self.config, self.app_name, default_config_file) - - def get_start_time_input_dict(self): - """! Get the first run time specified in config. Used if only running - the wrapper once (LOOP_ORDER = processes). - - @returns dictionary containing time information for first run time - """ - use_init = util.is_loop_by_init(self.config) - if use_init is None: - self.log_error('Could not read time info') - return None - - - start_time, _, _ = util.get_start_end_interval_times(self.config) - if start_time is None: - self.log_error("Could not get start time") - return None - - input_dict = util.set_input_dict(start_time, self.config, use_init) - return input_dict diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index b05e76ded2..e401c9bd88 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -39,6 +39,7 @@ from ..util import met_util as util from ..util import do_string_sub +from ..util import time_generator, add_to_time_input from . import CommandBuilder @@ -65,23 +66,12 @@ def __init__(self, config, instance=None, config_overrides=None): 'CYCLONE_PLOTTER_INIT_DATE') self.init_hr = self.config.getraw('config', 'CYCLONE_PLOTTER_INIT_HR') - init_time_fmt = self.config.getstr('config', 'INIT_TIME_FMT', '') - - if init_time_fmt: - clock_time = datetime.datetime.strptime( - self.config.getstr('config', - 'CLOCK_TIME'), - '%Y%m%d%H%M%S' - ) - - init_beg = self.config.getraw('config', 'INIT_BEG') - if init_beg: - init_beg_dt = util.get_time_obj(init_beg, - init_time_fmt, - clock_time, - logger=self.logger) - self.init_date = do_string_sub(self.init_date, init=init_beg_dt) - self.init_hr = do_string_sub(self.init_hr, init=init_beg_dt) + # attempt to get first runtime from config + # if successful, substitute time values into init date and hour + time_input = next(time_generator(self.config)) + if time_input is not None: + self.init_date = do_string_sub(self.init_date, **time_input) + self.init_hr = do_string_sub(self.init_hr, **time_input) self.model = self.config.getstr('config', 'CYCLONE_PLOTTER_MODEL') self.title = self.config.getstr('config', diff --git a/metplus/wrappers/extract_tiles_wrapper.py b/metplus/wrappers/extract_tiles_wrapper.py index 6dd37a9684..c04d995806 100755 --- a/metplus/wrappers/extract_tiles_wrapper.py +++ b/metplus/wrappers/extract_tiles_wrapper.py @@ -290,35 +290,38 @@ def use_tc_stat_input(self, storm_dict, idx_dict): def use_mtd_input(self, object_dict, idx_dict): """! Find lat/lons in MTD input file and create tiles from locations. - @param input_path path to MTD file to process + @param object_dict dictionary of MTD object data @param idx_dict dictionary with header names as keys and the index of those names as values. """ indices = self.get_object_indices(object_dict.keys()) if not indices: - self.log_error(f"No non-zero OBJECT_CAT found") + self.logger.warning(f"No non-zero OBJECT_CAT found") return # loop over corresponding CF### and CO### lines for index in indices: fcst_data_list = self.get_cluster_data(object_dict[f'CF{index}'], - idx_dict) + idx_dict) obs_data_list = self.get_cluster_data(object_dict[f'CO{index}'], - idx_dict) + idx_dict) track_data = {} - for fcst_data, obs_data in zip(fcst_data_list, obs_data_list): - if fcst_data.get('FCST_VALID') != obs_data.get('FCST_VALID'): - self.log_error("Time mismatch in valid time between " - f"CF{index} and CO{index}: " - f"({fcst_data.get('FCST_VALID')} vs " - f"{obs_data.get('FCST_VALID')}). " - "Wrapper assumes fcst and obs cluster data " - "are in the same order.") - return + # loop through fcst data and find obs data that matches the time + for fcst_data in fcst_data_list: + fcst_lead = fcst_data.get('FCST_LEAD') + fcst_valid = fcst_data.get('FCST_VALID') + + obs_data = [item for item in obs_data_list + if item.get('FCST_LEAD') == fcst_lead and + item.get('FCST_VALID') == fcst_valid] + + # skip if no obs data with the same fcst lead and valid time + if not obs_data: + continue track_data['FCST'] = fcst_data - track_data['OBS'] = obs_data + track_data['OBS'] = obs_data[0] time_info = ( self.set_time_info_from_track_data(track_data['FCST']) @@ -361,17 +364,17 @@ def get_object_indices(object_cats): indices = set() for key in object_cats: match = re.match(r'CF(\d+)', key) - if match: + # only use non-zero (000) objects + if match and int(match.group(1)) != 0: indices.add(match.group(1)) indices = sorted(list(indices)) - # if no indices were found or there is 1 and it is zero, return None - if not indices or (len(indices) == 1 and int(indices[0]) == 0): + # if no indices were found, return None + if not indices: return None return indices - def call_regrid_data_plane(self, time_info, track_data, input_type): # set var list from config using time info var_list = util.sub_var_list(self.c_dict['VAR_LIST_TEMP'], diff --git a/metplus/wrappers/gen_vx_mask_wrapper.py b/metplus/wrappers/gen_vx_mask_wrapper.py index 2c852ed33f..ac5459ec1d 100755 --- a/metplus/wrappers/gen_vx_mask_wrapper.py +++ b/metplus/wrappers/gen_vx_mask_wrapper.py @@ -12,6 +12,7 @@ import os +from ..util import getlist from ..util import met_util as util from ..util import time_util from . import CommandBuilder @@ -53,16 +54,18 @@ def create_c_dict(self): c_dict['MASK_INPUT_DIR'] = self.config.getdir('GEN_VX_MASK_INPUT_MASK_DIR', '') - c_dict['MASK_INPUT_TEMPLATES'] = util.getlist(self.config.getraw('filename_templates', - 'GEN_VX_MASK_INPUT_MASK_TEMPLATE')) + c_dict['MASK_INPUT_TEMPLATES'] = getlist( + self.config.getraw('config', 'GEN_VX_MASK_INPUT_MASK_TEMPLATE') + ) if not c_dict['MASK_INPUT_TEMPLATES']: self.log_error("Must set GEN_VX_MASK_INPUT_MASK_TEMPLATE to run GenVxMask wrapper") self.isOK = False # optional arguments - c_dict['COMMAND_OPTIONS'] = util.getlist(self.config.getraw('config', - 'GEN_VX_MASK_OPTIONS')) + c_dict['COMMAND_OPTIONS'] = getlist( + self.config.getraw('config', 'GEN_VX_MASK_OPTIONS') + ) # if no options were specified, set to a list with an empty string if not c_dict['COMMAND_OPTIONS']: diff --git a/metplus/wrappers/make_plots_wrapper.py b/metplus/wrappers/make_plots_wrapper.py index b73d660028..43d3183633 100755 --- a/metplus/wrappers/make_plots_wrapper.py +++ b/metplus/wrappers/make_plots_wrapper.py @@ -18,6 +18,7 @@ import datetime import itertools +from ..util import getlist from ..util import met_util as util from ..util import parse_var_list from . import CommandBuilder @@ -115,56 +116,56 @@ def create_c_dict(self): c_dict['VALID_END'] = self.config.getstr('config', 'VALID_END', '') c_dict['INIT_BEG'] = self.config.getstr('config', 'INIT_BEG', '') c_dict['INIT_END'] = self.config.getstr('config', 'INIT_END', '') - c_dict['GROUP_LIST_ITEMS'] = util.getlist( + c_dict['GROUP_LIST_ITEMS'] = getlist( self.config.getstr('config', 'GROUP_LIST_ITEMS') ) - c_dict['LOOP_LIST_ITEMS'] = util.getlist( + c_dict['LOOP_LIST_ITEMS'] = getlist( self.config.getstr('config', 'LOOP_LIST_ITEMS') ) c_dict['VAR_LIST'] = parse_var_list(self.config) - c_dict['MODEL_LIST'] = util.getlist( + c_dict['MODEL_LIST'] = getlist( self.config.getstr('config', 'MODEL_LIST', '') ) - c_dict['DESC_LIST'] = util.getlist( + c_dict['DESC_LIST'] = getlist( self.config.getstr('config', 'DESC_LIST', '') ) - c_dict['FCST_LEAD_LIST'] = util.getlist( + c_dict['FCST_LEAD_LIST'] = getlist( self.config.getstr('config', 'FCST_LEAD_LIST', '') ) - c_dict['OBS_LEAD_LIST'] = util.getlist( + c_dict['OBS_LEAD_LIST'] = getlist( self.config.getstr('config', 'OBS_LEAD_LIST', '') ) - c_dict['FCST_VALID_HOUR_LIST'] = util.getlist( + c_dict['FCST_VALID_HOUR_LIST'] = getlist( self.config.getstr('config', 'FCST_VALID_HOUR_LIST', '') ) - c_dict['FCST_INIT_HOUR_LIST'] = util.getlist( + c_dict['FCST_INIT_HOUR_LIST'] = getlist( self.config.getstr('config', 'FCST_INIT_HOUR_LIST', '') ) - c_dict['OBS_VALID_HOUR_LIST'] = util.getlist( + c_dict['OBS_VALID_HOUR_LIST'] = getlist( self.config.getstr('config', 'OBS_VALID_HOUR_LIST', '') ) - c_dict['OBS_INIT_HOUR_LIST'] = util.getlist( + c_dict['OBS_INIT_HOUR_LIST'] = getlist( self.config.getstr('config', 'OBS_INIT_HOUR_LIST', '') ) - c_dict['VX_MASK_LIST'] = util.getlist( + c_dict['VX_MASK_LIST'] = getlist( self.config.getstr('config', 'VX_MASK_LIST', '') ) - c_dict['INTERP_MTHD_LIST'] = util.getlist( + c_dict['INTERP_MTHD_LIST'] = getlist( self.config.getstr('config', 'INTERP_MTHD_LIST', '') ) - c_dict['INTERP_PNTS_LIST'] = util.getlist( + c_dict['INTERP_PNTS_LIST'] = getlist( self.config.getstr('config', 'INTERP_PNTS_LIST', '') ) - c_dict['COV_THRESH_LIST'] = util.getlist( + c_dict['COV_THRESH_LIST'] = getlist( self.config.getstr('config', 'COV_THRESH_LIST', '') ) - c_dict['ALPHA_LIST'] = util.getlist( + c_dict['ALPHA_LIST'] = getlist( self.config.getstr('config', 'ALPHA_LIST', '') ) - c_dict['LINE_TYPE_LIST'] = util.getlist( + c_dict['LINE_TYPE_LIST'] = getlist( self.config.getstr('config', 'LINE_TYPE_LIST', '') ) - c_dict['USER_SCRIPT_LIST'] = util.getlist( + c_dict['USER_SCRIPT_LIST'] = getlist( self.config.getstr('config', 'MAKE_PLOTS_USER_SCRIPT_LIST', '') ) c_dict['VERIF_CASE'] = self.config.getstr('config', @@ -198,7 +199,7 @@ def create_c_dict(self): "MAKE_PLOTS_VERIF_TYPE, or " "MAKE_PLOTS_USER_SCRIPT_LIST") - c_dict['STATS_LIST'] = util.getlist( + c_dict['STATS_LIST'] = getlist( self.config.getstr('config', 'MAKE_PLOTS_STATS_LIST', '') ) c_dict['AVERAGE_METHOD'] = self.config.getstr( diff --git a/metplus/wrappers/pb2nc_wrapper.py b/metplus/wrappers/pb2nc_wrapper.py index 5e8622a2bd..f571934f3c 100755 --- a/metplus/wrappers/pb2nc_wrapper.py +++ b/metplus/wrappers/pb2nc_wrapper.py @@ -13,6 +13,7 @@ import os import re +from ..util import getlistint from ..util import met_util as util from ..util import time_util from ..util import do_string_sub @@ -60,9 +61,9 @@ def create_c_dict(self): 'LOG_PB2NC_VERBOSITY', c_dict['VERBOSITY']) - c_dict['OFFSETS'] = util.getlistint(self.config.getstr('config', - 'PB2NC_OFFSETS', - '0')) + c_dict['OFFSETS'] = getlistint(self.config.getstr('config', + 'PB2NC_OFFSETS', + '0')) # Directories # these are optional because users can specify full file path diff --git a/metplus/wrappers/point_stat_wrapper.py b/metplus/wrappers/point_stat_wrapper.py index 02c97debee..c47673a5a4 100755 --- a/metplus/wrappers/point_stat_wrapper.py +++ b/metplus/wrappers/point_stat_wrapper.py @@ -12,6 +12,7 @@ import os +from ..util import getlistint from ..util import met_util as util from ..util import time_util from ..util import do_string_sub @@ -93,7 +94,7 @@ def create_c_dict(self): c_dict['VERBOSITY']) ) c_dict['ALLOW_MULTIPLE_FILES'] = True - c_dict['OFFSETS'] = util.getlistint( + c_dict['OFFSETS'] = getlistint( self.config.getstr('config', 'POINT_STAT_OFFSETS', '0') diff --git a/metplus/wrappers/runtime_freq_wrapper.py b/metplus/wrappers/runtime_freq_wrapper.py index eccc0604a5..105bc552e9 100755 --- a/metplus/wrappers/runtime_freq_wrapper.py +++ b/metplus/wrappers/runtime_freq_wrapper.py @@ -15,9 +15,10 @@ from ..util import time_util from . import CommandBuilder -from ..util import do_string_sub, get_start_end_interval_times, set_input_dict +from ..util import do_string_sub from ..util import log_runtime_banner, get_lead_sequence, is_loop_by_init from ..util import skip_time, getlist +from ..util import time_generator, add_to_time_input '''!@namespace RuntimeFreqWrapper @brief Parent class for wrappers that run over a grouping of times @@ -57,13 +58,6 @@ def create_c_dict(self): '').upper() ) - # get runtime information to obtain all input files - start, end, interval = get_start_end_interval_times(self.config, - warn=True) - c_dict['START_TIME'] = start - c_dict['END_TIME'] = end - c_dict['TIME_INTERVAL'] = interval - return c_dict def get_input_templates(self, c_dict): @@ -108,15 +102,6 @@ def run_all_times(self): f" {', '.join(self.FREQ_OPTIONS)}") return None - # if looping over init/valid time, - # check that the time config variables can be read correctly - if self.c_dict['RUNTIME_FREQ'] == 'RUN_ONCE_PER_INIT_OR_VALID': - - if not self.c_dict['START_TIME']: - self.log_error("Could not get [INIT/VALID] time information" - "from configuration file") - return None - # if not running once for each runtime and loop order is not set to # 'processes' report an error if self.c_dict['RUNTIME_FREQ'] != 'RUN_ONCE_FOR_EACH': @@ -159,53 +144,44 @@ def run_all_times_custom(self, custom): def run_once(self, custom): self.logger.debug("Running once for all files") - # create input dictionary and get 'now' item - input_dict = set_input_dict(loop_time=None, - config=self.config, - use_init=None, - instance=self.instance, - custom=custom) + # create input dictionary and set clock time, instance, and custom + time_input = {} + add_to_time_input(time_input, + clock_time=self.config.getstr('config', + 'CLOCK_TIME'), + instance=self.instance, + custom=custom) # set other time items to wildcard to find all files - input_dict['init'] = '*' - input_dict['valid'] = '*' - input_dict['lead'] = '*' + time_input['init'] = '*' + time_input['valid'] = '*' + time_input['lead'] = '*' - return self.run_at_time_once(input_dict) + return self.run_at_time_once(time_input) def run_once_per_init_or_valid(self, custom): - use_init = is_loop_by_init(self.config) - if use_init is None: - return False - - # log which time type to loop over - if use_init: - init_or_valid = 'init' - else: - init_or_valid = 'valid' - self.logger.debug(f"Running once for each {init_or_valid} time") + self.logger.debug(f"Running once for each init/valid time") success = True - loop_time = self.c_dict['START_TIME'] - while loop_time <= self.c_dict['END_TIME']: - log_runtime_banner(loop_time, self.config, use_init) - input_dict = set_input_dict(loop_time, - self.config, - use_init, - instance=self.instance, - custom=custom) - - if 'init' in input_dict: - input_dict['valid'] = '*' - elif 'valid' in input_dict: - input_dict['init'] = '*' - - input_dict['lead'] = '*' - - if not self.run_at_time_once(input_dict): + for time_input in time_generator(self.config): + if time_input is None: success = False + continue + + log_runtime_banner(self.config, time_input, self) + add_to_time_input(time_input, + instance=self.instance, + custom=custom) - loop_time += self.c_dict['TIME_INTERVAL'] + if 'init' in time_input: + time_input['valid'] = '*' + elif 'valid' in time_input: + time_input['init'] = '*' + + time_input['lead'] = '*' + + if not self.run_at_time_once(time_input): + success = False return success @@ -218,17 +194,19 @@ def run_once_per_lead(self, custom): # create input dict and only set 'now' item # create a new dictionary each iteration in case the function # that it is passed into modifies it - input_dict = set_input_dict(loop_time=None, - config=self.config, - use_init=None, - instance=self.instance, - custom=custom) + time_input = {} + add_to_time_input(time_input, + clock_time=self.config.getstr('config', + 'CLOCK_TIME'), + instance=self.instance, + custom=custom) + # add forecast lead - input_dict['lead'] = lead - input_dict['init'] = '*' - input_dict['valid'] = '*' + time_input['lead'] = lead + time_input['init'] = '*' + time_input['valid'] = '*' - if not self.run_at_time_once(input_dict): + if not self.run_at_time_once(time_input): success = False return success @@ -237,8 +215,8 @@ def run_at_time(self, input_dict): """! Runs the command for a given run time. This function loops over the list of forecast leads and list of custom loops and runs once for each combination - Args: - @param input_dict dictionary containing time information + + @param input_dict dictionary containing time information """ for custom_string in self.c_dict['CUSTOM_LOOP_LIST']: if custom_string: @@ -283,37 +261,29 @@ def get_all_files(self, custom=None): i.e. fcst or obs, and the value is a list of files that fit in that category """ - use_init = is_loop_by_init(self.config) - if use_init is None: - return False - self.logger.debug("Finding all input files") all_files = [] - # if start time is not set, don't loop - if not self.c_dict.get('START_TIME'): - return False - # loop over all init/valid times - loop_time = self.c_dict['START_TIME'] - while loop_time <= self.c_dict['END_TIME']: - input_dict = set_input_dict(loop_time, - self.config, - use_init, - instance=self.instance, - custom=custom) + for time_input in time_generator(self.config): + if time_input is None: + return False + + add_to_time_input(time_input, + instance=self.instance, + custom=custom) # loop over all forecast leads wildcard_if_empty = self.c_dict.get('WILDCARD_LEAD_IF_EMPTY', False) lead_seq = get_lead_sequence(self.config, - input_dict, + time_input, wildcard_if_empty=wildcard_if_empty) for lead in lead_seq: - input_dict['lead'] = lead + time_input['lead'] = lead # set current lead time config and environment variables - time_info = time_util.ti_calculate(input_dict) + time_info = time_util.ti_calculate(time_input) if skip_time(time_info, self.c_dict.get('SKIP_TIMES')): continue @@ -325,8 +295,6 @@ def get_all_files(self, custom=None): else: all_files.append(file_dict) - loop_time += self.c_dict['TIME_INTERVAL'] - if not all_files: return False diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 429b139c39..8a16f4e2dd 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -22,12 +22,14 @@ WRAPPER_CANNOT_RUN = True EXCEPTION_ERR = err_msg +from ..util import getlist from ..util import met_util as util from ..util import do_string_sub, parse_template -from ..util import get_lead_sequence, get_lead_sequence_groups, set_input_dict +from ..util import get_lead_sequence, get_lead_sequence_groups from ..util import ti_get_hours_from_lead, ti_get_seconds_from_lead from ..util import ti_get_lead_string from ..util import parse_var_list +from ..util import add_to_time_input from .plot_data_plane_wrapper import PlotDataPlaneWrapper from . import RuntimeFreqWrapper @@ -117,7 +119,7 @@ def create_c_dict(self): extra_args={'remove_quotes': True}) # get stat list to loop over - c_dict['STAT_LIST'] = util.getlist( + c_dict['STAT_LIST'] = getlist( self.config.getstr('config', 'SERIES_ANALYSIS_STAT_LIST', '') @@ -349,11 +351,12 @@ def run_once_per_lead(self, custom): # create input dict and only set 'now' item # create a new dictionary each iteration in case the function # that it is passed into modifies it - input_dict = set_input_dict(loop_time=None, - config=self.config, - use_init=None, - instance=self.instance, - custom=custom) + input_dict = {} + add_to_time_input(input_dict, + clock_time=self.config.getstr('config', + 'CLOCK_TIME'), + instance=self.instance, + custom=custom) input_dict['init'] = '*' input_dict['valid'] = '*' @@ -490,11 +493,13 @@ def find_input_files(self, time_info, data_type): """! Loop over list of input templates and find files for each @param time_info time dictionary to use for string substitution + @param data_type type of data to find, i.e. FCST or OBS @returns Input file list if all files were found, None if not. """ input_files = self.find_data(time_info, return_list=True, - data_type=data_type) + data_type=data_type, + mandatory=False) return input_files def subset_input_files(self, time_info): diff --git a/metplus/wrappers/stat_analysis_wrapper.py b/metplus/wrappers/stat_analysis_wrapper.py index 44fabeb66e..d95bb32305 100755 --- a/metplus/wrappers/stat_analysis_wrapper.py +++ b/metplus/wrappers/stat_analysis_wrapper.py @@ -18,6 +18,7 @@ import datetime import itertools +from ..util import getlist from ..util import met_util as util from ..util import do_string_sub, find_indices_in_config_section from ..util import parse_var_list @@ -184,7 +185,7 @@ def create_c_dict(self): conf_list in all_lists_to_read if conf_list not in self.field_lists] for conf_list in non_field_lists: - c_dict[conf_list] = util.getlist( + c_dict[conf_list] = getlist( self.config.getstr('config', conf_list, '') ) @@ -305,7 +306,7 @@ def read_field_lists_from_config(self, field_dict): self.get_level_list(field_list.split('_')[0]) ) else: - field_dict[field_list] = util.getlist( + field_dict[field_list] = getlist( self.config.getstr('config', field_list, '') @@ -1318,7 +1319,7 @@ def get_level_list(self, data_type): """ level_list = [] - level_input = util.getlist( + level_input = getlist( self.config.getstr('config', f'{data_type}_LEVEL_LIST', '') ) @@ -1495,7 +1496,7 @@ def get_c_dict_list(self): False) ) if run_fourier: - fourier_wave_num_pairs = util.getlist( + fourier_wave_num_pairs = getlist( self.config.getstr('config', 'VAR' + var_info['index'] + '_WAVE_NUM_LIST', '') diff --git a/metplus/wrappers/tc_gen_wrapper.py b/metplus/wrappers/tc_gen_wrapper.py index 91168a0e96..3f6393b841 100755 --- a/metplus/wrappers/tc_gen_wrapper.py +++ b/metplus/wrappers/tc_gen_wrapper.py @@ -16,8 +16,9 @@ from ..util import met_util as util from ..util import time_util -from . import CommandBuilder from ..util import do_string_sub +from ..util import time_generator +from . import CommandBuilder '''!@namespace TCGenWrapper @brief Wraps the TC-Gen tool @@ -262,9 +263,8 @@ def create_c_dict(self): ) self.add_met_config_window('genesis_match_window') - # get INPUT_TIME_DICT values since wrapper only runs - # once (doesn't look over time) - c_dict['INPUT_TIME_DICT'] = self.set_time_dict_for_single_runtime() + # get INPUT_TIME_DICT values since wrapper doesn't loop over time + c_dict['INPUT_TIME_DICT'] = next(time_generator(self.config)) if not c_dict['INPUT_TIME_DICT']: self.isOK = False diff --git a/metplus/wrappers/tc_pairs_wrapper.py b/metplus/wrappers/tc_pairs_wrapper.py index f86e814e13..c3e6a67a5b 100755 --- a/metplus/wrappers/tc_pairs_wrapper.py +++ b/metplus/wrappers/tc_pairs_wrapper.py @@ -20,11 +20,13 @@ import datetime import glob +from ..util import getlist from ..util import time_util from ..util import met_util as util from ..util import do_string_sub from ..util import get_tags from ..util.met_config import add_met_config_dict_list +from ..util import time_generator, log_runtime_banner, add_to_time_input from . import CommandBuilder '''!@namespace TCPairsWrapper @@ -167,13 +169,10 @@ def create_c_dict(self): self.handle_consensus() - # if looping by processes, get the init or valid beg time and run once - c_dict['INPUT_DICT'] = self.get_start_time_input_dict() - - c_dict['INIT_INCLUDE'] = util.getlist( + c_dict['INIT_INCLUDE'] = getlist( self.get_wrapper_or_generic_config('INIT_INCLUDE') ) - c_dict['INIT_EXCLUDE'] = util.getlist( + c_dict['INIT_EXCLUDE'] = getlist( self.get_wrapper_or_generic_config('INIT_EXCLUDE') ) c_dict['VALID_BEG'] = self.get_wrapper_or_generic_config('VALID_BEG') @@ -195,7 +194,7 @@ def create_c_dict(self): ) # get list of models to process - c_dict['MODEL_LIST'] = util.getlist( + c_dict['MODEL_LIST'] = getlist( self.config.getraw('config', 'MODEL', '') ) # if no models are requested, set list to contain a single string @@ -205,7 +204,7 @@ def create_c_dict(self): self._read_storm_info(c_dict) - c_dict['STORM_NAME_LIST'] = util.getlist( + c_dict['STORM_NAME_LIST'] = getlist( self.config.getraw('config', 'TC_PAIRS_STORM_NAME') ) c_dict['DLAND_FILE'] = self.config.getraw('config', @@ -287,13 +286,13 @@ def _read_storm_info(self, c_dict): @param c_dict dictionary to populate with values from config @returns None """ - storm_id_list = util.getlist( + storm_id_list = getlist( self.config.getraw('config', 'TC_PAIRS_STORM_ID', '') ) - cyclone_list = util.getlist( + cyclone_list = getlist( self.config.getraw('config', 'TC_PAIRS_CYCLONE', '') ) - basin_list = util.getlist( + basin_list = getlist( self.config.getraw('config', 'TC_PAIRS_BASIN', '') ) @@ -336,7 +335,13 @@ def run_all_times(self): """! Build up the command to invoke the MET tool tc_pairs. """ # use first run time - input_dict = self.c_dict.get('INPUT_DICT') + input_dict = next(time_generator(self.config)) + if not input_dict: + return self.all_commands + + add_to_time_input(input_dict, + instance=self.instance) + log_runtime_banner(self.config, input_dict, self) # if running in READ_ALL_FILES mode, call tc_pairs once and exit if self.c_dict['READ_ALL_FILES']: diff --git a/metplus/wrappers/tcmpr_plotter_wrapper.py b/metplus/wrappers/tcmpr_plotter_wrapper.py index 371b1a8417..3a3b6fb6e7 100755 --- a/metplus/wrappers/tcmpr_plotter_wrapper.py +++ b/metplus/wrappers/tcmpr_plotter_wrapper.py @@ -6,9 +6,11 @@ import os import shutil +from ..util import getlist from ..util import met_util as util from ..util import time_util from ..util import do_string_sub +from ..util import time_generator from . import CommandBuilder class TCMPRPlotterWrapper(CommandBuilder): @@ -107,7 +109,7 @@ def create_c_dict(self): self.log_error("TCMPR_PLOTTER_CONFIG_FILE must be set") # get time information - input_dict = self.set_time_dict_for_single_runtime() + input_dict = next(time_generator(self.config)) if not input_dict: self.isOK = False c_dict['TIME_INFO'] = time_util.ti_calculate(input_dict) @@ -151,9 +153,7 @@ def read_optional_args(self): elif 'bool' in data_type: value = self.config.getbool('config', config_name, '') elif 'list' in data_type: - value = util.getlist(self.config.getraw('config', - config_name, - '')) + value = getlist(self.config.getraw('config', config_name)) else: self.log_error(f"Invalid type for {name}: {data_type}") @@ -186,12 +186,8 @@ def read_loop_info(self): elif name == 'dep': config_name = f'{config_name}_VARS' - values = util.getlist(self.config.getraw('config', - config_name, - '')) - labels = util.getlist(self.config.getraw('config', - label_config_name, - '')) + values = getlist(self.config.getraw('config', config_name)) + labels = getlist(self.config.getraw('config', label_config_name)) # if labels are not set, use values as labels if not labels: diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf b/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf index 08d56d2a5b..670c14b592 100644 --- a/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf +++ b/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf @@ -1,43 +1,12 @@ [config] -# time looping - options are INIT, VALID, RETRO, and REALTIME -# If set to INIT or RETRO: -# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set -# If set to VALID or REALTIME: -# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set -LOOP_BY = REALTIME - -# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. -# see www.strftime.org for more information -# %Y%m%d%H expands to YYYYMMDDHH -VALID_TIME_FMT = %Y%m%d%H - -# BLank for this usecase but the parameter still needs to be there -VALID_BEG = - -# BLank for this usecase but the parameter still needs to be there -VALID_END = - -# BLank for this usecase but the parameter still needs to be there -VALID_INCREMENT = - -# List of forecast leads to process for each run time (init or valid) -# In hours if units are not specified -# If unset, defaults to 0 (don't loop through forecast leads) -LEAD_SEQ = - -# Order of loops to process data - Options are times, processes -# Not relevant if only one item is in the PROCESS_LIST -# times = run all wrappers in the PROCESS_LIST for a single run time, then -# increment the run time and run all wrappers again until all times have -# been evaluated. -# processes = run the first wrapper in the PROCESS_LIST for all times -# specified, then repeat for the next item in the PROCESS_LIST until all -# wrappers have been run -LOOP_ORDER = processes - PROCESS_LIST = UserScript +# Note: time looping is not used in this use case +LOOP_BY = REALTIME +VALID_TIME_FMT = %Y +VALID_BEG = 2020 + USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE USER_SCRIPT_COMMAND = {PARM_BASE}/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot/cross_spectra_plot.py diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_Hovmoeller.conf b/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_Hovmoeller.conf index 1b688230f6..9656a1b0e1 100644 --- a/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_Hovmoeller.conf +++ b/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_Hovmoeller.conf @@ -1,44 +1,11 @@ - [config] -# time looping - options are INIT, VALID, RETRO, and REALTIME -# If set to INIT or RETRO: -# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set -# If set to VALID or REALTIME: -# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set -LOOP_BY = REALTIME - -# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. -# see www.strftime.org for more information -# %Y%m%d%H expands to YYYYMMDDHH -VALID_TIME_FMT = %Y%m%d%H - -# BLank for this usecase but the parameter still needs to be there -VALID_BEG = - -# BLank for this usecase but the parameter still needs to be there -VALID_END = - -# BLank for this usecase but the parameter still needs to be there -VALID_INCREMENT = - -# List of forecast leads to process for each run time (init or valid) -# In hours if units are not specified -# If unset, defaults to 0 (don't loop through forecast leads) -LEAD_SEQ = - -# Order of loops to process data - Options are times, processes -# Not relevant if only one item is in the PROCESS_LIST -# times = run all wrappers in the PROCESS_LIST for a single run time, then -# increment the run time and run all wrappers again until all times have -# been evaluated. -# processes = run the first wrapper in the PROCESS_LIST for all times -# specified, then repeat for the next item in the PROCESS_LIST until all -# wrappers have been run -LOOP_ORDER = processes - PROCESS_LIST = UserScript +LOOP_BY = REALTIME +VALID_TIME_FMT = %Y +VALID_BEG = 2014 + USER_SCRIPT_RUNTIME_FREQ = RUN_ONCE USER_SCRIPT_COMMAND = {PARM_BASE}/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_Hovmoeller/hovmoeller_diagram.py From 46658c6b2436d4200bda837184b8cbfca975699f Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 27 Dec 2021 15:41:47 -0700 Subject: [PATCH 244/770] Feature 896 more met config (#1322) --- docs/Contributors_Guide/basic_components.rst | 8 +- docs/Users_Guide/glossary.rst | 164 +++++++++- docs/Users_Guide/wrappers.rst | 229 ++++++++++++-- .../grid_stat/test_grid_stat_wrapper.py | 16 + .../pytests/mode/test_mode_wrapper.py | 5 +- .../pytests/pb2nc/test_pb2nc_wrapper.py | 4 + .../point_stat/test_point_stat_wrapper.py | 38 +++ .../series_analysis/test_series_analysis.py | 70 ++++- .../pytests/tc_pairs/test_tc_pairs_wrapper.py | 7 + metplus/util/diff_util.py | 2 +- metplus/util/doc_util.py | 280 ++++++++++++------ metplus/util/met_config.py | 5 +- metplus/wrappers/command_builder.py | 21 +- metplus/wrappers/compare_gridded_wrapper.py | 3 +- metplus/wrappers/grid_stat_wrapper.py | 16 + metplus/wrappers/mode_wrapper.py | 9 + metplus/wrappers/pb2nc_wrapper.py | 10 + metplus/wrappers/point_stat_wrapper.py | 23 ++ metplus/wrappers/series_analysis_wrapper.py | 56 +++- metplus/wrappers/tc_pairs_wrapper.py | 10 + parm/met_config/GridStatConfig_wrapped | 12 +- parm/met_config/MODEConfig_wrapped | 7 +- parm/met_config/PB2NCConfig_wrapped | 21 +- parm/met_config/PointStatConfig_wrapped | 22 +- parm/met_config/SeriesAnalysisConfig_wrapped | 16 +- parm/met_config/TCPairsConfig_wrapped | 7 +- .../met_tool_wrapper/GridStat/GridStat.conf | 6 + .../use_cases/met_tool_wrapper/MODE/MODE.conf | 5 +- .../met_tool_wrapper/PB2NC/PB2NC.conf | 85 +----- .../met_tool_wrapper/PointStat/PointStat.conf | 191 ++++-------- .../SeriesAnalysis/SeriesAnalysis.conf | 140 +++++---- .../TCPairs/TCPairs_extra_tropical.conf | 132 +++------ .../TCPairs/TCPairs_tropical.conf | 115 +++---- 33 files changed, 1086 insertions(+), 649 deletions(-) diff --git a/docs/Contributors_Guide/basic_components.rst b/docs/Contributors_Guide/basic_components.rst index 86d3d57323..7de9af11d8 100644 --- a/docs/Contributors_Guide/basic_components.rst +++ b/docs/Contributors_Guide/basic_components.rst @@ -302,12 +302,12 @@ data type, extra info, children, and nicknames. * extra: Additional info as a comma separated string (see extra_args above) * children: Dictionary defining a nested dictionary where the key is the name of the sub-directory and the value is the item info (see items above) -* nicknames: List of METplus variable names (with app name excluded) to also +* nicknames: List of METplus variable names to also search and use if it is set. For example, the GridStat variable mask.poly is set by the METplus config variable GRID_STAT_MASK_POLY. However, in older versions of the METplus wrappers, the variable used was GRID_STAT_VERIFICATION_MASK_TEMPLATE. To preserve support for this name, the - nickname can be set to ['VERIFICATION_MASK_TEMPLATE'] and the old variable + nickname can be set to [f'{self.app_name.upper()}_VERIFICATION_MASK_TEMPLATE'] and the old variable will be checked if GRID_STAT_MASK_POLY is not set. Values must be set to None to preserve the order. @@ -320,7 +320,7 @@ CompareGriddedWrapper and is used by GridStat, PointStat, and EnsembleStat:: def handle_climo_cdf_dict(self): self.add_met_config_dict('climo_cdf', { - 'cdf_bins': ('float', None, None, ['CLIMO_CDF_BINS']), + 'cdf_bins': ('float', None, None, [f'{self.app_name.upper()}_CLIMO_CDF_BINS']), 'center_bins': 'bool', 'write_bins': 'bool', }) @@ -329,7 +329,7 @@ This function handles setting the climo_cdf dictionary. The METplus config variable that fits the format {APP_NAME}_{DICTIONARY_NAME}_{VARIABLE_NAME}, i.e. GRID_STAT_CLIMO_CDF_CDF_BINS for GridStat's climo_cdf.cdf_bins, is quieried first. However, this default name is a little redundant, so adding -the nickname 'CLIMO_CDF_BINS' allows the user to set the variable +the nickname 'GRID_STAT_CLIMO_CDF_BINS' allows the user to set the variable GRID_STAT_CLIMO_CDF_BINS instead. There are many MET config dictionaries that only contain beg and end to define diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index ca701205ce..5183c899be 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -3431,9 +3431,7 @@ METplus Configuration Glossary | *Used by:* PB2NC POINT_STAT_STATION_ID - Specify the ID of a specific station to use with the MET point_stat tool. - - | *Used by:* PointStat + .. warning:: **DEPRECATED:** Please use :term:`POINT_STAT_MASK_SID` instead. POINT_STAT_VERIFICATION_MASK_TEMPLATE Template used to specify the verification mask filename for the MET tool point_stat. Now supports a list of filenames. @@ -3734,17 +3732,13 @@ METplus Configuration Glossary .. warning:: **DEPRECATED:** Please use :term:`MAKE_PLOTS_INPUT_DIR` instead. SERIES_ANALYSIS_STAT_LIST - Specify a list of statistics to be computed by the MET series_analysis tool. Sets the 'cnt' value in the output_stats dictionary in the MET SeriesAnalysis config file - - | *Used by:* SeriesAnalysis + .. warning:: **DEPRECATED:** Please use :term:`SERIES_ANALYSIS_OUTPUT_STATS_CNT` instead. SERIES_ANALYSIS_CTS_LIST - Specify a list of contingency table statistics to be computed by the MET series_analysis tool. Sets the 'cts' value in the output_stats dictionary in the MET SeriesAnalysis config file - - | *Used by:* SeriesAnalysis + .. warning:: **DEPRECATED:** Please use :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTS` instead. STAT_LIST - .. warning:: **DEPRECATED:** Please use :term:`SERIES_ANALYSIS_STAT_LIST` instead. + .. warning:: **DEPRECATED:** Please use :term:`SERIES_ANALYSIS_OUTPUT_STATS_CNT` instead. STORM_ID .. warning:: **DEPRECATED:** Please use :term:`TC_PAIRS_STORM_ID` or :term:`TC_STAT_STORM_ID`. @@ -6066,6 +6060,11 @@ METplus Configuration Glossary | *Used by:* PointStat + POINT_STAT_MASK_LLPNT + Specify the value for 'mask.llpnt' in the MET configuration file for PointStat. + + | *Used by:* PointStat + MODE_GRID_RES Set the grid_res entry in the MODE MET config file. @@ -8530,6 +8529,151 @@ METplus Configuration Glossary | *Used by:* EnsembleStat + GRID_STAT_FOURIER_WAVE_1D_BEG + Specify the value for 'fourier.wave_1d_beg' in the MET configuration file for GridStat. + + | *Used by:* GridStat + + GRID_STAT_FOURIER_WAVE_1D_END + Specify the value for 'fourier.wave_1d_end' in the MET configuration file for GridStat. + + | *Used by:* GridStat + + PB2NC_OBS_BUFR_MAP + Specify the value for 'obs_bufr_map' in the MET configuration file for PB2NC. + + | *Used by:* PB2NC + + PB2NC_OBS_PREPBUFR_MAP + Specify the value for 'obs_prepbufr_map' in the MET configuration file for PB2NC. + + | *Used by:* PB2NC + + POINT_STAT_HIRA_FLAG + Specify the value for 'hira.flag' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_HIRA_WIDTH + Specify the value for 'hira.width' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_HIRA_VLD_THRESH + Specify the value for 'hira.vld_thresh' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_HIRA_COV_THRESH + Specify the value for 'hira.cov_thresh' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_HIRA_SHAPE + Specify the value for 'hira.shape' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_HIRA_PROB_CAT_THRESH + Specify the value for 'hira.prob_cat_thresh' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + POINT_STAT_MESSAGE_TYPE_GROUP_MAP + Specify the value for 'message_type_group_map' in the MET configuration file for PointStat. + + | *Used by:* PointStat + + TC_PAIRS_CHECK_DUP + Specify the value for 'check_dup' in the MET configuration file for TCPairs. + + | *Used by:* TCPairs + + TC_PAIRS_INTERP12 + Specify the value for 'interp12' in the MET configuration file for TCPairs. + + | *Used by:* TCPairs + + SERIES_ANALYSIS_OUTPUT_STATS_FHO + Specify the value for 'output_stats.fho' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_CTC + Specify the value for 'output_stats.ctc' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_CTS + Specify the value for 'output_stats.cts' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_MCTC + Specify the value for 'output_stats.mctc' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_MCTS + Specify the value for 'output_stats.mcts' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_CNT + Specify the value for 'output_stats.cnt' in the MET configuration file for SeriesAnalysis. Also used to generate plots for each value in the list. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_SL1L2 + Specify the value for 'output_stats.sl1l2' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_SAL1L2 + Specify the value for 'output_stats.sal1l2' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_PCT + Specify the value for 'output_stats.pct' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_PSTD + Specify the value for 'output_stats.pstd' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_PJC + Specify the value for 'output_stats.pjc' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_OUTPUT_STATS_PRC + Specify the value for 'output_stats.prc' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + MODE_PS_PLOT_FLAG + Specify the value for 'ps_plot_flag' in the MET configuration file for MODE. + + | *Used by:* MODE + + MODE_CT_STATS_FLAG + Specify the value for 'ct_stats_flag' in the MET configuration file for MODE. + + | *Used by:* MODE + + GRID_STAT_CENSOR_THRESH + Specify the value for 'censor_thresh' in the MET configuration file for GridStat. + + | *Used by:* GridStat + + GRID_STAT_CENSOR_VAL + Specify the value for 'censor_val' in the MET configuration file for GridStat. + + | *Used by:* GridStat + INIT_LIST List of initialization times to process. This variable is used when intervals between run times are irregular. diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 40df66983e..9cd7af499e 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -2800,6 +2800,10 @@ METplus Configuration | :term:`GRID_STAT_DISTANCE_MAP_FOM_ALPHA` | :term:`GRID_STAT_DISTANCE_MAP_ZHU_WEIGHT` | :term:`GRID_STAT_DISTANCE_MAP_BETA_VALUE_N` +| :term:`GRID_STAT_FOURIER_WAVE_1D_BEG` +| :term:`GRID_STAT_FOURIER_WAVE_1D_END` +| :term:`GRID_STAT_CENSOR_THRESH` +| :term:`GRID_STAT_CENSOR_VAL` | :term:`GRID_STAT_MASK_GRID` (optional) | :term:`GRID_STAT_MASK_POLY` (optional) | :term:`GRID_STAT_MET_CONFIG_OVERRIDES` @@ -3266,6 +3270,42 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`GRID_STAT_DISTANCE_MAP_BETA_VALUE_N` - distance_map.beta_value(n) +**${METPLUS_FOURIER_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`GRID_STAT_FOURIER_WAVE_1D_BEG` + - fourier.wave_1d_beg + * - :term:`GRID_STAT_FOURIER_WAVE_1D_END` + - fourier.wave_1d_end + +**${METPLUS_CENSOR_THRESH}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`GRID_STAT_CENSOR_THRESH` + - censor_thresh + +**${METPLUS_CENSOR_VAL}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`GRID_STAT_CENSOR_VAL` + - censor_val + + .. _ioda2nc_wrapper: IODA2NC @@ -3926,6 +3966,8 @@ METplus Configuration | :term:`MODE_INTEREST_FUNCTION_CENTROID_DIST` | :term:`MODE_INTEREST_FUNCTION_BOUNDARY_DIST` | :term:`MODE_INTEREST_FUNCTION_CONVEX_HULL_DIST` +| :term:`MODE_PS_PLOT_FLAG` +| :term:`MODE_CT_STATS_FLAG` | :term:`FCST_MODE_VAR_NAME` (optional) | :term:`FCST_MODE_VAR_LEVELS` (optional) | :term:`FCST_MODE_VAR_THRESH` (optional) @@ -4440,7 +4482,27 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`MODE_TOTAL_INTEREST_THRESH` - total_interest_thresh +**${METPLUS_PS_PLOT_FLAG}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`MODE_PS_PLOT_FLAG` + - ps_plot_flag + +**${METPLUS_CT_STATS_FLAG}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + * - METplus Config(s) + - MET Config File + * - :term:`MODE_CT_STATS_FLAG` + - ct_stats_flag .. _mtd_wrapper: @@ -4750,6 +4812,8 @@ METplus Configuration | :term:`PB2NC_LEVEL_RANGE_END` | :term:`PB2NC_LEVEL_CATEGORY` | :term:`PB2NC_QUALITY_MARK_THRESH` +| :term:`PB2NC_OBS_BUFR_MAP` +| :term:`PB2NC_OBS_PREPBUFR_MAP` .. warning:: **DEPRECATED:** @@ -4938,6 +5002,27 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`PB2NC_QUALITY_MARK_THRESH` - quality_mark_thresh +**${METPLUS_OBS_BUFR_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`PB2NC_OBS_BUFR_MAP` + - obs_bufr_map + +**${METPLUS_OBS_PREPBUFR_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`PB2NC_OBS_PREPBUFR_MAP` + - obs_prepbufr_map .. _pcp_combine_wrapper: @@ -5136,9 +5221,10 @@ Configuration | :term:`POINT_STAT_REGRID_WIDTH` | :term:`POINT_STAT_REGRID_VLD_THRESH` | :term:`POINT_STAT_REGRID_SHAPE` -| :term:`POINT_STAT_GRID` -| :term:`POINT_STAT_POLY` -| :term:`POINT_STAT_STATION_ID` +| :term:`POINT_STAT_MASK_GRID` +| :term:`POINT_STAT_MASK_POLY` +| :term:`POINT_STAT_MASK_SID` +| :term:`POINT_STAT_MASK_LLPNT` | :term:`POINT_STAT_MESSAGE_TYPE` | :term:`POINT_STAT_CUSTOM_LOOP_LIST` | :term:`POINT_STAT_SKIP_IF_OUTPUT_EXISTS` @@ -5194,6 +5280,13 @@ Configuration | :term:`POINT_STAT_CLIMO_STDEV_DAY_INTERVAL` | :term:`POINT_STAT_CLIMO_STDEV_HOUR_INTERVAL` | :term:`POINT_STAT_HSS_EC_VALUE` +| :term:`POINT_STAT_HIRA_FLAG` +| :term:`POINT_STAT_HIRA_WIDTH` +| :term:`POINT_STAT_HIRA_VLD_THRESH` +| :term:`POINT_STAT_HIRA_COV_THRESH` +| :term:`POINT_STAT_HIRA_SHAPE` +| :term:`POINT_STAT_HIRA_PROB_CAT_THRESH` +| :term:`POINT_STAT_MESSAGE_TYPE_GROUP_MAP` | :term:`FCST_POINT_STAT_WINDOW_BEGIN` (optional) | :term:`FCST_POINT_STAT_WINDOW_END` (optional) | :term:`OBS_POINT_STAT_WINDOW_BEGIN` (optional) @@ -5453,6 +5546,18 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`POINT_STAT_MASK_SID` - mask.sid +**${METPLUS_MASK_LLPNT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`POINT_STAT_MASK_LLPNT` + - mask.llpnt + + **${METPLUS_OUTPUT_PREFIX}** .. list-table:: @@ -5589,6 +5694,37 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`POINT_STAT_HSS_EC_VALUE` - hss_ec_value +**${METPLUS_HIRA_DICT}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`POINT_STAT_HIRA_FLAG` + - hira.flag + * - :term:`POINT_STAT_HIRA_WIDTH` + - hira.width + * - :term:`POINT_STAT_HIRA_VLD_THRESH` + - hira.vld_thresh + * - :term:`POINT_STAT_HIRA_COV_THRESH` + - hira.cov_thresh + * - :term:`POINT_STAT_HIRA_SHAPE` + - hira.shape + * - :term:`POINT_STAT_HIRA_PROB_CAT_THRESH` + - hira.prob_cat_thresh + +**${METPLUS_MESSAGE_TYPE_GROUP_MAP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`POINT_STAT_MESSAGE_TYPE_GROUP_MAP` + - message_type_group_map .. _py_embed_ingest_wrapper: @@ -5746,6 +5882,18 @@ METplus Configuration | :term:`SERIES_ANALYSIS_CLIMO_STDEV_DAY_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_HOUR_INTERVAL` | :term:`SERIES_ANALYSIS_HSS_EC_VALUE` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_FHO` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTC` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTS` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_MCTC` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_MCTS` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_CNT` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_SL1L2` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_SAL1L2` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_PCT` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_PSTD` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_PJC` +| :term:`SERIES_ANALYSIS_OUTPUT_STATS_PRC` | .. warning:: **DEPRECATED:** @@ -5981,18 +6129,7 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`SERIES_ANALYSIS_VLD_THRESH` - vld_thresh -**${METPLUS_CTS_LIST}** - -.. list-table:: - :widths: 5 5 - :header-rows: 0 - - * - METplus Config(s) - - MET Config File - * - :term:`SERIES_ANALYSIS_CTS_LIST` - - output_stats.cts - -**${METPLUS_STAT_LIST}** +**${METPLUS_MET_CONFIG_OVERRIDES}** .. list-table:: :widths: 5 5 @@ -6000,10 +6137,10 @@ see :ref:`How METplus controls MET config file settings`. * - METplus Config(s) - MET Config File - * - :term:`SERIES_ANALYSIS_STAT_LIST` - - output_stats.cnt + * - :term:`SERIES_ANALYSIS_MET_CONFIG_OVERRIDES` + - n/a -**${METPLUS_MET_CONFIG_OVERRIDES}** +**${METPLUS_HSS_EC_VALUE}** .. list-table:: :widths: 5 5 @@ -6011,10 +6148,10 @@ see :ref:`How METplus controls MET config file settings`. * - METplus Config(s) - MET Config File - * - :term:`SERIES_ANALYSIS_MET_CONFIG_OVERRIDES` - - n/a + * - :term:`SERIES_ANALYSIS_HSS_EC_VALUE` + - hss_ec_value -**${METPLUS_HSS_EC_VALUE}** +**${METPLUS_OUTPUT_STATS_DICT}** .. list-table:: :widths: 5 5 @@ -6022,8 +6159,30 @@ see :ref:`How METplus controls MET config file settings`. * - METplus Config(s) - MET Config File - * - :term:`SERIES_ANALYSIS_HSS_EC_VALUE` - - hss_ec_value + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_FHO` + - output_stats.fho + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTC` + - output_stats.ctc + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTS` + - output_stats.cts + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_MCTC` + - output_stats.mctc + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_MCTS` + - output_stats.mcts + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_CNT` + - output_stats.cnt + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_SL1L2` + - output_stats.sl1l2 + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_SAL1L2` + - output_stats.sal1l2 + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_PCT` + - output_stats.pct + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_PSTD` + - output_stats.pstd + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_PJC` + - output_stats.pjc + * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_PRC` + - output_stats.prc SeriesByInit @@ -7329,6 +7488,8 @@ METplus Configuration | :term:`TC_PAIRS_CONSENSUS_MIN_REQ` | :term:`TC_PAIRS_SKIP_LEAD_SEQ` | :term:`TC_PAIRS_RUN_ONCE` +| :term:`TC_PAIRS_CHECK_DUP` +| :term:`TC_PAIRS_INTERP12` | .. warning:: **DEPRECATED:** @@ -7578,6 +7739,28 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`TC_PAIRS_CONSENSUS_MIN_REQ` - consensus.min_req +**${METPLUS_CHECK_DUP}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_PAIRS_CHECK_DUP` + - check_dup + +**${METPLUS_INTERP12}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`TC_PAIRS_INTERP12` + - interp12 + .. _tcrmw_wrapper: TCRMW diff --git a/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py b/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py index b38bee453a..80c7393c83 100644 --- a/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py +++ b/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py @@ -567,6 +567,21 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, 'baddeley_max_dist = 2.3;' 'fom_alpha = 4.5;zhu_weight = 0.5;' 'beta_value(n) = n * n / 3.0;}')}), + ({'GRID_STAT_FOURIER_WAVE_1D_BEG': '0,4,10', }, + {'METPLUS_FOURIER_DICT': 'fourier = {wave_1d_beg = [0, 4, 10];}'}), + + ({'GRID_STAT_FOURIER_WAVE_1D_END': '3,9,20', }, + {'METPLUS_FOURIER_DICT': 'fourier = {wave_1d_end = [3, 9, 20];}'}), + + ({'GRID_STAT_FOURIER_WAVE_1D_BEG': '0,4,10', + 'GRID_STAT_FOURIER_WAVE_1D_END': '3,9,20',}, + {'METPLUS_FOURIER_DICT': ('fourier = {wave_1d_beg = [0, 4, 10];' + 'wave_1d_end = [3, 9, 20];}')}), + ({'GRID_STAT_CENSOR_THRESH': '>12000,<5000', }, + {'METPLUS_CENSOR_THRESH': 'censor_thresh = [>12000, <5000];'}), + + ({'GRID_STAT_CENSOR_VAL': '12000, 5000', }, + {'METPLUS_CENSOR_VAL': 'censor_val = [12000, 5000];'}), ] ) @@ -616,6 +631,7 @@ def test_grid_stat_single_field(metplus_config, config_overrides, item.startswith(env_var_key)), None) assert(match is not None) actual_value = match.split('=', 1)[1] + print(f"ENV VAR: {env_var_key}") if env_var_key == 'METPLUS_FCST_FIELD': assert(actual_value == fcst_fmt) elif env_var_key == 'METPLUS_OBS_FIELD': diff --git a/internal_tests/pytests/mode/test_mode_wrapper.py b/internal_tests/pytests/mode/test_mode_wrapper.py index 8c4144fd68..4ee11d41ee 100644 --- a/internal_tests/pytests/mode/test_mode_wrapper.py +++ b/internal_tests/pytests/mode/test_mode_wrapper.py @@ -302,7 +302,10 @@ def set_minimum_config_settings(config): '(0.0, 2.0) ' '200.0/grid_res, 1.0)' ');')}), - + ({'MODE_PS_PLOT_FLAG': 'True', }, + {'METPLUS_PS_PLOT_FLAG': 'ps_plot_flag = TRUE;'}), + ({'MODE_CT_STATS_FLAG': 'True', }, + {'METPLUS_CT_STATS_FLAG': 'ct_stats_flag = TRUE;'}), ] ) def test_mode_single_field(metplus_config, config_overrides, diff --git a/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py b/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py index a1c202c335..97b920b957 100644 --- a/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py +++ b/internal_tests/pytests/pb2nc/test_pb2nc_wrapper.py @@ -279,6 +279,10 @@ def test_find_input_files(metplus_config, offsets, offset_to_find): 'type = ["min", "max", "range"];' 'vld_freq = 1;' 'vld_thresh = 0.1;}')}), + ({'PB2NC_OBS_BUFR_MAP': '{key="POB"; val="PRES"; },{key="QOB"; val="SPFH";}', }, + {'METPLUS_OBS_BUFR_MAP': 'obs_bufr_map = [{key="POB"; val="PRES"; }, {key="QOB"; val="SPFH";}];'}), + ({'PB2NC_OBS_PREPBUFR_MAP': '{key="POB"; val="PRES"; },{key="QOB"; val="SPFH";}', }, + {'METPLUS_OBS_PREPBUFR_MAP': 'obs_prepbufr_map = [{key="POB"; val="PRES"; }, {key="QOB"; val="SPFH";}];'}), ] ) diff --git a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py index f706e702d1..118a6f7dcc 100755 --- a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py +++ b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py @@ -405,6 +405,44 @@ def test_met_dictionary_in_var_options(metplus_config): 'CLIMO_STDEV_FILE': '"/some/climo_stdev/file.txt"'}), ({'POINT_STAT_HSS_EC_VALUE': '0.5', }, {'METPLUS_HSS_EC_VALUE': 'hss_ec_value = 0.5;'}), + ({'POINT_STAT_MASK_LLPNT': ('{ name = "LAT30TO40"; lat_thresh = >=30&&<=40; lon_thresh = NA; },' + '{ name = "BOX"; lat_thresh = >=20&&<=40; lon_thresh = >=-110&&<=-90; }')}, + {'METPLUS_MASK_LLPNT': 'llpnt = [{ name = "LAT30TO40"; lat_thresh = >=30&&<=40; lon_thresh = NA; }, { name = "BOX"; lat_thresh = >=20&&<=40; lon_thresh = >=-110&&<=-90; }];'}), + + ({'POINT_STAT_HIRA_FLAG': 'False', }, + {'METPLUS_HIRA_DICT': 'hira = {flag = FALSE;}'}), + + ({'POINT_STAT_HIRA_WIDTH': '2,3,4,5', }, + {'METPLUS_HIRA_DICT': 'hira = {width = [2, 3, 4, 5];}'}), + + ({'POINT_STAT_HIRA_VLD_THRESH': '1.0', }, + {'METPLUS_HIRA_DICT': 'hira = {vld_thresh = 1.0;}'}), + + ({'POINT_STAT_HIRA_COV_THRESH': '==0.25, ==0.5', }, + {'METPLUS_HIRA_DICT': 'hira = {cov_thresh = [==0.25, ==0.5];}'}), + + ({'POINT_STAT_HIRA_SHAPE': 'square', }, + {'METPLUS_HIRA_DICT': 'hira = {shape = SQUARE;}'}), + + ({'POINT_STAT_HIRA_PROB_CAT_THRESH': '>1,<=2', }, + {'METPLUS_HIRA_DICT': 'hira = {prob_cat_thresh = [>1, <=2];}'}), + + ({ + 'POINT_STAT_HIRA_FLAG': 'False', + 'POINT_STAT_HIRA_WIDTH': '2,3,4,5', + 'POINT_STAT_HIRA_VLD_THRESH': '1.0', + 'POINT_STAT_HIRA_COV_THRESH': '==0.25, ==0.5', + 'POINT_STAT_HIRA_SHAPE': 'square', + 'POINT_STAT_HIRA_PROB_CAT_THRESH': '>1,<=2', + }, + { + 'METPLUS_HIRA_DICT': ('hira = {flag = FALSE;width = [2, 3, 4, 5];' + 'vld_thresh = 1.0;' + 'cov_thresh = [==0.25, ==0.5];' + 'shape = SQUARE;' + 'prob_cat_thresh = [>1, <=2];}')}), + ({'POINT_STAT_MESSAGE_TYPE_GROUP_MAP': '{ key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET";},{ key = "ANYAIR"; val = "AIRCAR,AIRCFT";}', }, + {'METPLUS_MESSAGE_TYPE_GROUP_MAP': 'message_type_group_map = [{ key = "SURFACE"; val = "ADPSFC, SFCSHP, MSONET";}, { key = "ANYAIR"; val = "AIRCAR, AIRCFT";}];'}), ] ) diff --git a/internal_tests/pytests/series_analysis/test_series_analysis.py b/internal_tests/pytests/series_analysis/test_series_analysis.py index 1e99c93e63..5d21eb0b39 100644 --- a/internal_tests/pytests/series_analysis/test_series_analysis.py +++ b/internal_tests/pytests/series_analysis/test_series_analysis.py @@ -25,7 +25,7 @@ run_times = ['2005080700',] stat_list = 'TOTAL,RMSE,FBAR,OBAR' stat_list_quotes = '", "'.join(stat_list.split(',')) -stat_list_fmt = f'cnt = ["{stat_list_quotes}"];' +stat_list_fmt = f'output_stats = {{cnt = ["{stat_list_quotes}"];}}' def get_input_dirs(config): fake_data_dir = os.path.join(config.getdir('METPLUS_BASE'), @@ -206,6 +206,71 @@ def set_minimum_config_settings(config): 'CLIMO_STDEV_FILE': '"/some/climo_stdev/file.txt"'}), ({'SERIES_ANALYSIS_HSS_EC_VALUE': '0.5', }, {'METPLUS_HSS_EC_VALUE': 'hss_ec_value = 0.5;'}), + # output_stats + ({'SERIES_ANALYSIS_OUTPUT_STATS_FHO': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {fho = ["RMSE", "FBAR", "OBAR"];cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_CTC': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {ctc = ["RMSE", "FBAR", "OBAR"];cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_CTS': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cts = ["RMSE", "FBAR", "OBAR"];cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_MCTC': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {mctc = ["RMSE", "FBAR", "OBAR"];cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_MCTS': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {mcts = ["RMSE", "FBAR", "OBAR"];cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_CNT': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_SL1L2': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];sl1l2 = ["RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_SAL1L2': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];sal1l2 = ["RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_PCT': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];pct = ["RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_PSTD': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];pstd = ["RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_PJC': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];pjc = ["RMSE", "FBAR", "OBAR"];}'}), + + ({'SERIES_ANALYSIS_OUTPUT_STATS_PRC': 'RMSE,FBAR,OBAR', }, + {'METPLUS_OUTPUT_STATS_DICT': 'output_stats = {cnt = ["TOTAL", "RMSE", "FBAR", "OBAR"];prc = ["RMSE", "FBAR", "OBAR"];}'}), + + ({ + 'SERIES_ANALYSIS_OUTPUT_STATS_FHO': 'RMSE1,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_CTC': 'RMSE2,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_CTS': 'RMSE3,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_MCTC': 'RMSE4,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_MCTS': 'RMSE5,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_CNT': 'RMSE6,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_SL1L2': 'RMSE7,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_SAL1L2': 'RMSE8,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_PCT': 'RMSE9,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_PSTD': 'RMSE10,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_PJC': 'RMSE11,FBAR,OBAR', + 'SERIES_ANALYSIS_OUTPUT_STATS_PRC': 'RMSE12,FBAR,OBAR', + }, + {'METPLUS_OUTPUT_STATS_DICT': ('output_stats = {' + 'fho = ["RMSE1", "FBAR", "OBAR"];' + 'ctc = ["RMSE2", "FBAR", "OBAR"];' + 'cts = ["RMSE3", "FBAR", "OBAR"];' + 'mctc = ["RMSE4", "FBAR", "OBAR"];' + 'mcts = ["RMSE5", "FBAR", "OBAR"];' + 'cnt = ["RMSE6", "FBAR", "OBAR"];' + 'sl1l2 = ["RMSE7", "FBAR", "OBAR"];' + 'sal1l2 = ["RMSE8", "FBAR", "OBAR"];' + 'pct = ["RMSE9", "FBAR", "OBAR"];' + 'pstd = ["RMSE10", "FBAR", "OBAR"];' + 'pjc = ["RMSE11", "FBAR", "OBAR"];' + 'prc = ["RMSE12", "FBAR", "OBAR"];}')}), + ] ) def test_series_analysis_single_field(metplus_config, config_overrides, @@ -248,11 +313,12 @@ def test_series_analysis_single_field(metplus_config, config_overrides, item.startswith(env_var_key)), None) assert(match is not None) actual_value = match.split('=', 1)[1] + print(f"ENV VAR: {env_var_key}") if env_var_key == 'METPLUS_FCST_FIELD': assert(actual_value == fcst_fmt) elif env_var_key == 'METPLUS_OBS_FIELD': assert (actual_value == obs_fmt) - elif env_var_key == 'METPLUS_STAT_LIST': + elif env_var_key == 'METPLUS_OUTPUT_STATS_DICT' and 'METPLUS_OUTPUT_STATS_DICT' not in env_var_values: assert (actual_value == stat_list_fmt) else: assert(env_var_values.get(env_var_key, '') == actual_value) diff --git a/internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py b/internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py index 5696553914..a0845be227 100644 --- a/internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py +++ b/internal_tests/pytests/tc_pairs/test_tc_pairs_wrapper.py @@ -361,6 +361,13 @@ def test_tc_pairs_storm_id_lists(metplus_config, config_overrides, # 17: write_valid ({'TC_PAIRS_WRITE_VALID': '20141031_14'}, {'METPLUS_WRITE_VALID': 'write_valid = ["20141031_14"];'}), + # 18: check_dup + ({'TC_PAIRS_CHECK_DUP': 'False', }, + {'METPLUS_CHECK_DUP': 'check_dup = FALSE;'}), + # 19: interp12 + ({'TC_PAIRS_INTERP12': 'replace', }, + {'METPLUS_INTERP12': 'interp12 = REPLACE;'}), + ] ) def test_tc_pairs_loop_order_processes(metplus_config, config_overrides, diff --git a/metplus/util/diff_util.py b/metplus/util/diff_util.py index e894bbda04..8c59539052 100644 --- a/metplus/util/diff_util.py +++ b/metplus/util/diff_util.py @@ -445,7 +445,7 @@ def nc_is_equal(file_a, file_b, fields=None, debug=False): if any(var_a[:].flatten() != var_b[:].flatten()): print(f"ERROR: Field ({field}) values (non-numeric) " "differ\n" - f" File_A: {var_a}\n File_B: {var_b}") + f" File_A: {var_a[:]}\n File_B: {var_b[:]}") is_equal = False except: print("ERROR: Couldn't diff NetCDF files, need to update diff method") diff --git a/metplus/util/doc_util.py b/metplus/util/doc_util.py index 5bf834d86f..9338a69369 100755 --- a/metplus/util/doc_util.py +++ b/metplus/util/doc_util.py @@ -60,7 +60,7 @@ def get_wrapper_name(process_name): return None -def print_doc_text(tool_name, met_var, dict_items): +def print_doc_text(tool_name, input_dict): """! Format documentation for adding support for a new MET config variable through METplus wrappers. @@ -70,91 +70,181 @@ def print_doc_text(tool_name, met_var, dict_items): is a dictionary """ wrapper_caps = tool_name.upper() - met_var_caps = met_var.upper() - env_var_name = f'METPLUS_{met_var_caps}' - wrapper_camel = get_wrapper_name(wrapper_caps) - metplus_var = f'{wrapper_caps}_{met_var_caps}' - - metplus_config_names = [] - met_config_values = [] - if not dict_items: - metplus_config_names.append(metplus_var) - met_config_values.append(met_var) - else: - env_var_name = f'{env_var_name}_DICT' - for item_name in dict_items: - item_name_caps = item_name.upper() - metplus_config_name = f'{metplus_var}_{item_name_caps}' - - metplus_config_names.append(metplus_config_name) - met_config_values.append(f"{met_var}.{item_name}") - - print('WARNING: Guidance output from this script may differ slightly ' + # get info for each variable and store it in a dictionary + met_vars = [] + for var_name, dict_list in input_dict.items(): + metplus_var = f'{wrapper_caps}_{var_name.upper()}' + env_var_name = f'METPLUS_{var_name.upper()}' + met_var = {'name': var_name, 'dict_items': dict_list, + 'metplus_config_names': [], 'met_config_names': []} + if not dict_list: + met_var['env_var_name'] = env_var_name + met_var['metplus_config_names'].append(metplus_var) + met_var['met_config_names'].append(var_name) + else: + met_var['env_var_name'] = f'{env_var_name}_DICT' + for item_name in dict_list: + metplus_config = f'{metplus_var}_{item_name.upper()}' + met_config = f"{var_name}.{item_name}" + met_var['metplus_config_names'].append(metplus_config) + met_var['met_config_names'].append(met_config) + + met_vars.append(met_var) + + print(f"\nWrapper: {wrapper_camel}\n") + for index, var in enumerate(met_vars, 1): + print(f"MET Variable {index}: {var['name']}") + if var['dict_items']: + print(f" Dictionary Items: {', '.join(var['dict_items'])}") + print() + + print('\nWARNING: Guidance output from this script may differ slightly ' 'from the actual steps to take. It is intended to assist the process.' ' The text that is generated should be reviewed for accuracy before ' 'adding to codebase.') - print(f"\nWrapper: {wrapper_camel}") - print(f"MET Variable: {met_var}") - if dict_items: - print(f"Dictionary Items:") - for item in dict_items: - print(f' {item}') - + print("\nNOTE: Text between lines that contain all dashes (-) should be " + "added or replaced in the files. Do not include the dash lines.") print('\n==================================================\n') - print(f'\n\nIn the {tool_name}_wrapper.py file, in the {wrapper_camel}Wrapper ' + print(f'In metplus/wrappers/{tool_name}_wrapper.py\n\n' + f'In the {wrapper_camel}Wrapper ' f'class, add the following to the WRAPPER_ENV_VAR_KEYS class ' - f"variable list:\n\n\n '{env_var_name}',\n\n") + f"variable list:\n" + "\n---------------------------------------------") + for var in met_vars: + print(f" '{var['env_var_name']}',") + print(f"---------------------------------------------\n") print('\n==================================================\n') + print(f'In metplus/wrappers/{tool_name}_wrapper.py\n\n') print(f'In the create_c_dict function for {wrapper_camel}Wrapper, add a ' 'function call to read the new METplus config variables and save ' - 'the value to be added to the wrapped MET config file.\n\n') - if not dict_items: - print(f" self.add_met_config(name='{met_var}',\n" - " data_type='',\n" - f" metplus_configs=['{metplus_var}'])" - "\n\n\n" - "where can be string, list, int, float, bool, " - "or thresh.\n\n") - else: - print("Typically a function is written to handle MET config dictionary" - " items. Search for functions that start with handle_ in " - "CommandBuilder or other parent class wrappers to see if a " - "function already exists for the item you are adding or to use " - "as an example to write a new one.\n\n") + 'the value to be added to the wrapped MET config file.\n') + print("\n---------------------------------------------") + for var in met_vars: + print_add_met_config(var) + print("---------------------------------------------\n" + "\nwhere DATA_TYPE can be string, list, int, float, bool, " + "or thresh. Refer to the METplus Contributor's Guide " + "Basic Components section to see how to add additional info.\n") + print("Sometimes a function is written to handle MET config dictionary" + " items that are complex and common to many wrappers." + " Search for functions that start with handle_ in " + "CommandBuilder or other parent class wrappers to see if a " + "function already exists for the item you are adding or to use " + "as an example to write a new one.\n\n") print('\n==================================================\n') print('Add the new variables to the basic use case example for the tool,\n' f'i.e. parm/use_cases/met_tool_wrapper/{wrapper_camel}/' - f'{wrapper_camel}.conf:\n\n') - for mp_config in metplus_config_names: - print(f'#{mp_config} =') + f'{wrapper_camel}.conf:\n' + "\n---------------------------------------------") + for var in met_vars: + for mp_config in var['metplus_config_names']: + print(f'#{mp_config} =') + + print("---------------------------------------------\n") print('\n\n==================================================\n') - print(f"In the parm/met_config/{wrapper_camel}Config_wrapped file, " - f"compare the default values set for {met_var} to the version" + + var_names = '/'.join([var['name'] for var in met_vars]) + print(f"In parm/met_config/{wrapper_camel}Config_wrapped\n\n" + "IMPORTANT: Compare the default values set for " + f"{var_names} " + "to the version" f" in share/met/config/{wrapper_camel}Config_default. If " "they do differ, make sure to add variables to the use case " "config files so that they produce the same output.\n\n") - print(f"In the parm/met_config/{wrapper_camel}Config_wrapped file, " - "replace:\n\n") - print(f"{met_var} = ...\n\n with:\n\n//{met_var} =" - f"{' {' if dict_items else ''}\n${{{env_var_name}}}\n\n") + + for var in met_vars: + print("REPLACE:\n" + "\n---------------------------------------------") + print(f"{var['name']} = ..." + "\n---------------------------------------------\n" + "\nwith:\n" + "\n---------------------------------------------\n" + f"//{var['name']} =" + f"{' {' if var['dict_items'] else ''}\n${{{var['env_var_name']}}}" + "\n---------------------------------------------\n") print('\n==================================================\n') - print(f"\n\nIn docs/Users_Guide/wrappers.rst under {wrapper_camel} => " - "METplus Configuration section, add:\n\n") - for metplus_config_name in metplus_config_names: - print(f'| :term:`{metplus_config_name}`') + print(f"\nIn docs/Users_Guide/wrappers.rst\n\n" + f"Under {wrapper_camel} => " + "METplus Configuration section, add:\n" + "\n---------------------------------------------") + for var in met_vars: + for metplus_config_name in var['metplus_config_names']: + print(f'| :term:`{metplus_config_name}`') + + print("---------------------------------------------\n") print('\n==================================================\n') - print(f"\n\nIn docs/Users_Guide/wrappers.rst under {wrapper_camel} => " - "MET Configuration section, add:\n\n") - var_header = (f"**${{{env_var_name}}}**") + print(f"\n\nIn docs/Users_Guide/wrappers.rst\n\n" + f"Under {wrapper_camel} => " + "MET Configuration section, add:\n" + "\n---------------------------------------------\n") + + for var in met_vars: + print_met_config_table(var) + + print("---------------------------------------------") + print('\n==================================================\n') + print(f"In docs/Users_Guide/glossary.rst" + "\n\nAdd the following anywhere in the file:\n") + print("---------------------------------------------\n") + + for var in met_vars: + print_glossary_entry(var, wrapper_camel) + + print("---------------------------------------------") + print('\n==================================================\n') + print(f"In internal_tests/pytests/{tool_name}/" + f"test_{tool_name}_wrapper.py" + "\n\nAdd the following items to " + "the tests to ensure the new items are set properly. Note: " + "if the tool does not have unit tests to check the handling of " + "MET config variables, you will need to add those tests. See " + "grid_stat/test_grid_stat_wrapper.py for an example. Change " + "VALUE to an appropriate value for the variable.\n\n") + + print("---------------------------------------------") + for var in met_vars: + print_unit_test(var) + print("---------------------------------------------") + # add note to test setting a valid value in the basic use case config file + # to ensure that it is formatted properly when read by the MET tool + print('\n==================================================\n') + print(f"In parm/use_cases/met_tool_wrapper/{wrapper_camel}" + "\n\nVerify that the new METplus configuration variable(s) " + "will be formatted properly when read by the MET tool by " + "setting the variable(s) in the basic use case config files " + "to a valid value " + "and run the use case to ensure that it still succeeds. " + "Be sure to remove the value and comment out the variable " + "after you have confirmed this step.") + print('\n==================================================\n') + +def print_add_met_config(var): + met_var = var['name'] + dict_items = var['dict_items'] + if not dict_items: + print(f" self.add_met_config(name='{met_var}',\n" + " data_type='DATA_TYPE')") + else: + print(f" self.add_met_config_dict('{met_var}', {{") + for item in dict_items: + print(f" '{item}': 'DATA_TYPE',") + print(" })") + print() + +def print_met_config_table(var): + env_var_name = var['env_var_name'] + metplus_names = var['metplus_config_names'] + met_names = var['met_config_names'] + var_header = (f"**${{{env_var_name}}}**") list_table_text = (f"{var_header}\n\n" ".. list-table::\n" " :widths: 5 5\n" @@ -163,33 +253,33 @@ def print_doc_text(tool_name, met_var, dict_items): " - MET Config File\n" ) - for metplus_config_name, met_config_name in zip(metplus_config_names, met_config_values): + for metplus_config_name, met_config_name in zip(metplus_names, met_names): list_table_text += (f" * - :term:`{metplus_config_name}`\n" f" - {met_config_name}\n" ) print(list_table_text) - print('\n==================================================\n') - print(f"In docs/Users_Guide/glossary.rst, add:\n\n") - for metplus_config_name, met_config_name in zip(metplus_config_names, met_config_values): - glossary_entry = (f" {metplus_config_name}\n" - f" Specify the value for '{met_config_name}' " - f"in the MET configuration file for {wrapper_camel}.\n\n" - f" | *Used by:* {wrapper_camel}") +def print_glossary_entry(var, wrapper_camel): + metplus_names = var['metplus_config_names'] + met_names = var['met_config_names'] + for metplus_config_name, met_config_name in zip(metplus_names, met_names): + glossary_entry = ( + f" {metplus_config_name}\n" + f" Specify the value for '{met_config_name}' " + f"in the MET configuration file for {wrapper_camel}.\n\n" + f" | *Used by:* {wrapper_camel}" + ) print(f'{glossary_entry}\n') - print('\n==================================================\n') - print(f"In internal_tests/pytests/{tool_name}/" - f"test_{tool_name}_wrapper.py, add the following items to " - "the tests to ensure the new items are set properly. Note: " - "if the tool does not have unit tests to check the handling of " - "MET config variables, you will need to add those tests. See " - "grid_stat/test_grid_stat_wrapper.py for an example. Change " - "VALUE to an appropriate value for the variable.\n\n") - +def print_unit_test(var): input_dict_items = [] output_items = [] - for metplus_config_name, met_config_name in zip(metplus_config_names, met_config_values): + metplus_names = var['metplus_config_names'] + met_names = var['met_config_names'] + dict_items = var['dict_items'] + env_var_name = var['env_var_name'] + var_name = var['name'] + for metplus_config_name, met_config_name in zip(metplus_names, met_names): if dict_items: item_name = met_config_name.split('.')[1] output_item = f"{item_name} = VALUE;" @@ -203,9 +293,8 @@ def print_doc_text(tool_name, met_var, dict_items): else: output_fmt = output_item - test_text = (f" ({{{mp_config_dict_item} }},\n" - f" {{'{env_var_name}': '{met_var} = " + f" {{'{env_var_name}': '{var_name} = " f"{output_fmt}'}}),\n") print(test_text) @@ -214,7 +303,7 @@ def print_doc_text(tool_name, met_var, dict_items): for input_dict_item in input_dict_items: all_items_text += f" {input_dict_item}\n" all_items_text += (" },\n" - f" {{'{env_var_name}': '{met_var} = {{") + f" {{'{env_var_name}': '{var_name} = {{") all_items_text += ''.join(output_items) all_items_text += "}'})," print(all_items_text) @@ -222,24 +311,29 @@ def print_doc_text(tool_name, met_var, dict_items): def doc_util_usage(): """! Print usage statement for script """ - print(f"{__file__} [ " []" ' + '" []"\n' + f"\nExample: {__file__} grid_stat output_prefix " + "\n (simple variable named output_prefix)\n" + f'\nExample: {__file__} grid_stat "output_flag fho ctc mctc" ' + '\n (dictionary named output_flag containing fho, ctc, and mctc)\n' + f'\nExample: {__file__} grid_stat "output_flag fho ctc mctc" ' + 'output_prefix \n (both of the variables from the previous ' + 'examples)\n') if __name__ == "__main__": # sys.argv[1] is MET tool name, i.e. grid_stat - # sys.argv[2] is MET variable name, i.e. output_flag - # sys.argv[3] is optional list of MET dictionary var items: fho ctc cts + # sys.argv[2+] is MET variable name, i.e. output_flag or a MET variable + # name followed by a list of MET dictionary var items separated by spaces if len(sys.argv) < 3: doc_util_usage() sys.exit(1) tool_name = sys.argv[1] - met_var = sys.argv[2] - dict_items = None - - if len(sys.argv) > 3: - items = ','.join(sys.argv[3:]).split(',') - dict_items = [item.strip() for item in items] + input_dict = {} + for arg in sys.argv[2:]: + var_name, *dict_items = arg.split() + input_dict[var_name] = dict_items - print_doc_text(tool_name, met_var, dict_items) + print_doc_text(tool_name, input_dict) diff --git a/metplus/util/met_config.py b/metplus/util/met_config.py index 847eb43e21..e8ef186e94 100644 --- a/metplus/util/met_config.py +++ b/metplus/util/met_config.py @@ -173,11 +173,10 @@ def add_met_config_dict(config, app_name, output_dict, dict_name, items): metplus_configs.append(f'{metplus_name}IN') metplus_configs.append(metplus_name) + # add other variable names to search if expected name is unset if nicknames: for nickname in nicknames: - metplus_configs.append( - f'{app_name}_{nickname}'.upper() - ) + metplus_configs.append(nickname) # if dictionary, read get children from MET config else: diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 3835d8b130..9b4ea34922 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -1685,6 +1685,7 @@ def handle_time_summary_dict(self): METPLUS_TIME_SUMMARY_DICT that is referenced in the wrapped MET config files. """ + app_upper = self.app_name.upper() self.add_met_config_dict('time_summary', { 'flag': 'bool', 'raw_data': 'bool', @@ -1693,12 +1694,15 @@ def handle_time_summary_dict(self): 'step': 'int', 'width': ('string', 'remove_quotes'), 'grib_code': ('list', 'remove_quotes,allow_empty', None, - ['TIME_SUMMARY_GRIB_CODES']), + [f'{app_upper}_TIME_SUMMARY_GRIB_CODES']), 'obs_var': ('list', 'allow_empty', None, - ['TIME_SUMMARY_VAR_NAMES']), - 'type': ('list', 'allow_empty', None, ['TIME_SUMMARY_TYPES']), - 'vld_freq': ('int', None, None, ['TIME_SUMMARY_VALID_FREQ']), - 'vld_thresh': ('float', None, None, ['TIME_SUMMARY_VALID_THRESH']), + [f'{app_upper}_TIME_SUMMARY_VAR_NAMES']), + 'type': ('list', 'allow_empty', None, + [f'{app_upper}_TIME_SUMMARY_TYPES']), + 'vld_freq': ('int', None, None, + [f'{app_upper}_TIME_SUMMARY_VALID_FREQ']), + 'vld_thresh': ('float', None, None, + [f'{app_upper}_TIME_SUMMARY_VALID_THRESH']), }) def handle_mask(self, single_value=False, get_flags=False): @@ -1709,12 +1713,13 @@ def handle_mask(self, single_value=False, get_flags=False): @param get_flags if True, read grid_flag and poly_flag values """ data_type = 'string' if single_value else 'list' - + app_upper = self.app_name.upper() items = { 'grid': (data_type, 'allow_empty', None, - ['GRID']), + [f'{app_upper}_GRID']), 'poly': (data_type, 'allow_empty', None, - ['VERIFICATION_MASK_TEMPLATE', 'POLY']), + [f'{app_upper}_VERIFICATION_MASK_TEMPLATE', + f'{app_upper}_POLY']), } if get_flags: diff --git a/metplus/wrappers/compare_gridded_wrapper.py b/metplus/wrappers/compare_gridded_wrapper.py index f90e7c0567..e9c4e2bafe 100755 --- a/metplus/wrappers/compare_gridded_wrapper.py +++ b/metplus/wrappers/compare_gridded_wrapper.py @@ -404,7 +404,8 @@ def get_command(self): def handle_climo_cdf_dict(self): self.add_met_config_dict('climo_cdf', { - 'cdf_bins': ('float', None, None, ['CLIMO_CDF_BINS']), + 'cdf_bins': ('float', None, None, + [f'{self.app_name.upper()}_CLIMO_CDF_BINS']), 'center_bins': 'bool', 'write_bins': 'bool', }) diff --git a/metplus/wrappers/grid_stat_wrapper.py b/metplus/wrappers/grid_stat_wrapper.py index 2ad1d011a2..35451c9ff2 100755 --- a/metplus/wrappers/grid_stat_wrapper.py +++ b/metplus/wrappers/grid_stat_wrapper.py @@ -49,6 +49,9 @@ class GridStatWrapper(CompareGriddedWrapper): 'METPLUS_OBS_FILE_TYPE', 'METPLUS_HSS_EC_VALUE', 'METPLUS_DISTANCE_MAP_DICT', + 'METPLUS_FOURIER_DICT', + 'METPLUS_CENSOR_THRESH', + 'METPLUS_CENSOR_VAL', ] # handle deprecated env vars used pre v4.0.0 @@ -246,6 +249,19 @@ def create_c_dict(self): 'beta_value(n)': ('string', 'remove_quotes'), }) + self.add_met_config_dict('fourier', { + 'wave_1d_beg': ('list', 'remove_quotes'), + 'wave_1d_end': ('list', 'remove_quotes'), + }) + + self.add_met_config(name='censor_thresh', + data_type='list', + extra_args={'remove_quotes': True}) + + self.add_met_config(name='censor_val', + data_type='list', + extra_args={'remove_quotes': True}) + return c_dict def set_environment_variables(self, time_info): diff --git a/metplus/wrappers/mode_wrapper.py b/metplus/wrappers/mode_wrapper.py index af4237c4d7..23f958ac10 100755 --- a/metplus/wrappers/mode_wrapper.py +++ b/metplus/wrappers/mode_wrapper.py @@ -56,6 +56,8 @@ class MODEWrapper(CompareGriddedWrapper): 'METPLUS_INTEREST_FUNCTION_CENTROID_DIST', 'METPLUS_INTEREST_FUNCTION_BOUNDARY_DIST', 'METPLUS_INTEREST_FUNCTION_CONVEX_HULL_DIST', + 'METPLUS_PS_PLOT_FLAG', + 'METPLUS_CT_STATS_FLAG', ] WEIGHTS = { @@ -347,6 +349,13 @@ def create_c_dict(self): } ) + self.add_met_config(name='ps_plot_flag', + data_type='bool') + + self.add_met_config(name='ct_stats_flag', + data_type='bool') + + c_dict['ALLOW_MULTIPLE_FILES'] = False c_dict['MERGE_CONFIG_FILE'] = ( diff --git a/metplus/wrappers/pb2nc_wrapper.py b/metplus/wrappers/pb2nc_wrapper.py index f571934f3c..31828f145b 100755 --- a/metplus/wrappers/pb2nc_wrapper.py +++ b/metplus/wrappers/pb2nc_wrapper.py @@ -35,6 +35,8 @@ class PB2NCWrapper(CommandBuilder): 'METPLUS_LEVEL_RANGE_DICT', 'METPLUS_LEVEL_CATEGORY', 'METPLUS_QUALITY_MARK_THRESH', + 'METPLUS_OBS_BUFR_MAP', + 'METPLUS_OBS_PREPBUFR_MAP', ] def __init__(self, config, instance=None, config_overrides=None): @@ -187,6 +189,14 @@ def create_c_dict(self): data_type='int', metplus_configs=['PB2NC_QUALITY_MARK_THRESH']) + self.add_met_config(name='obs_bufr_map', + data_type='list', + extra_args={'remove_quotes': True}) + + self.add_met_config(name='obs_prepbufr_map', + data_type='list', + extra_args={'remove_quotes': True}) + return c_dict def set_environment_variables(self, time_info): diff --git a/metplus/wrappers/point_stat_wrapper.py b/metplus/wrappers/point_stat_wrapper.py index c47673a5a4..669ab8252c 100755 --- a/metplus/wrappers/point_stat_wrapper.py +++ b/metplus/wrappers/point_stat_wrapper.py @@ -32,6 +32,7 @@ class PointStatWrapper(CompareGriddedWrapper): 'METPLUS_MASK_GRID', 'METPLUS_MASK_POLY', 'METPLUS_MASK_SID', + 'METPLUS_MASK_LLPNT', 'METPLUS_OUTPUT_PREFIX', 'METPLUS_CLIMO_CDF_DICT', 'METPLUS_OBS_QUALITY_INC', @@ -41,6 +42,8 @@ class PointStatWrapper(CompareGriddedWrapper): 'METPLUS_CLIMO_MEAN_DICT', 'METPLUS_CLIMO_STDEV_DICT', 'METPLUS_HSS_EC_VALUE', + 'METPLUS_HIRA_DICT', + 'METPLUS_MESSAGE_TYPE_GROUP_MAP', ] # handle deprecated env vars used pre v4.0.0 @@ -165,6 +168,13 @@ def create_c_dict(self): 'POINT_STAT_STATION_ID'], extra_args={'allow_empty': True}) + self.add_met_config(name='llpnt', + data_type='list', + env_var_name='METPLUS_MASK_LLPNT', + metplus_configs=['POINT_STAT_MASK_LLPNT'], + extra_args={'allow_empty': True, + 'remove_quotes': True}) + self.add_met_config(name='message_type', data_type='list') @@ -231,6 +241,19 @@ def create_c_dict(self): data_type='float', metplus_configs=['POINT_STAT_HSS_EC_VALUE']) + self.add_met_config_dict('hira', { + 'flag': 'bool', + 'width': ('list', 'remove_quotes'), + 'vld_thresh': 'float', + 'cov_thresh': ('list', 'remove_quotes'), + 'shape': ('string', 'remove_quotes, uppercase'), + 'prob_cat_thresh': ('list', 'remove_quotes'), + }) + + self.add_met_config(name='message_type_group_map', + data_type='list', + extra_args={'remove_quotes': True}) + if not c_dict['FCST_INPUT_TEMPLATE']: self.log_error('Must set FCST_POINT_STAT_INPUT_TEMPLATE ' 'in config file') diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 8a16f4e2dd..8fdd625079 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -51,8 +51,7 @@ class SeriesAnalysisWrapper(RuntimeFreqWrapper): 'METPLUS_CLIMO_STDEV_DICT', 'METPLUS_BLOCK_SIZE', 'METPLUS_VLD_THRESH', - 'METPLUS_CTS_LIST', - 'METPLUS_STAT_LIST', + 'METPLUS_OUTPUT_STATS_DICT', 'METPLUS_HSS_EC_VALUE', ] @@ -60,6 +59,24 @@ class SeriesAnalysisWrapper(RuntimeFreqWrapper): DEPRECATED_WRAPPER_ENV_VAR_KEYS = [ 'CLIMO_MEAN_FILE', 'CLIMO_STDEV_FILE', + 'METPLUS_CTS_LIST', + 'METPLUS_STAT_LIST', + ] + + # variable names of output_stats dictionary + OUTPUT_STATS = [ + 'fho', + 'ctc', + 'cts', + 'mctc', + 'mcts', + 'cnt', + 'sl1l2', + 'sal1l2', + 'pct', + 'pstd', + 'pjc', + 'prc', ] def __init__(self, config, instance=None, config_overrides=None): @@ -118,23 +135,42 @@ def create_c_dict(self): data_type='string', extra_args={'remove_quotes': True}) - # get stat list to loop over - c_dict['STAT_LIST'] = getlist( - self.config.getstr('config', - 'SERIES_ANALYSIS_STAT_LIST', - '') - ) + # handle all output_stats dictionary values + output_stats_dict = {} + for key in self.OUTPUT_STATS: + nicknames = [ + f'SERIES_ANALYSIS_OUTPUT_STATS_{key.upper()}', + f'SERIES_ANALYSIS_{key.upper()}_LIST', + f'SERIES_ANALYSIS_{key.upper()}' + ] + # add legacy support for STAT_LIST for cnt + if key == 'cnt': + nicknames.append('SERIES_ANALYSIS_STAT_LIST') + # read cnt stat list to get stats to loop over for plotting + self.add_met_config(name='cnt', + data_type='list', + env_var_name='STAT_LIST', + metplus_configs=nicknames) + c_dict['STAT_LIST'] = getlist( + self.get_env_var_value('METPLUS_STAT_LIST') + ) + + value = ('list', None, None, nicknames) + output_stats_dict[key] = value + self.add_met_config_dict('output_stats', output_stats_dict) + if not c_dict['STAT_LIST']: self.log_error("Must set SERIES_ANALYSIS_STAT_LIST to run.") - # set stat list to set output_stats.cnt in MET config file + + # set legacy stat list to set output_stats.cnt in MET config file self.add_met_config(name='cnt', data_type='list', env_var_name='METPLUS_STAT_LIST', metplus_configs=['SERIES_ANALYSIS_STAT_LIST', 'SERIES_ANALYSIS_CNT']) - # set cts list to set output_stats.cts in MET config file + # set legacy cts list to set output_stats.cts in MET config file self.add_met_config(name='cts', data_type='list', env_var_name='METPLUS_CTS_LIST', diff --git a/metplus/wrappers/tc_pairs_wrapper.py b/metplus/wrappers/tc_pairs_wrapper.py index c3e6a67a5b..369779ed43 100755 --- a/metplus/wrappers/tc_pairs_wrapper.py +++ b/metplus/wrappers/tc_pairs_wrapper.py @@ -61,6 +61,8 @@ class TCPairsWrapper(CommandBuilder): 'METPLUS_WRITE_VALID', 'METPLUS_VALID_INC', 'METPLUS_VALID_EXC', + 'METPLUS_CHECK_DUP', + 'METPLUS_INTERP12', ] WILDCARDS = { @@ -169,6 +171,14 @@ def create_c_dict(self): self.handle_consensus() + self.add_met_config(name='check_dup', + data_type='bool') + + self.add_met_config(name='interp12', + data_type='string', + extra_args={'remove_quotes': True, + 'uppercase': True}) + c_dict['INIT_INCLUDE'] = getlist( self.get_wrapper_or_generic_config('INIT_INCLUDE') ) diff --git a/parm/met_config/GridStatConfig_wrapped b/parm/met_config/GridStatConfig_wrapped index fdaf8cf209..29abe3a6d2 100644 --- a/parm/met_config/GridStatConfig_wrapped +++ b/parm/met_config/GridStatConfig_wrapped @@ -35,8 +35,10 @@ ${METPLUS_REGRID_DICT} //////////////////////////////////////////////////////////////////////////////// -censor_thresh = []; -censor_val = []; +//censor_thresh = +${METPLUS_CENSOR_THRESH} +//censor_val = +${METPLUS_CENSOR_VAL} cat_thresh = []; cnt_thresh = [ NA ]; cnt_logic = UNION; @@ -134,10 +136,8 @@ nbrhd = { // Fourier decomposition // May be set separately in each "obs.field" entry // -fourier = { - wave_1d_beg = []; - wave_1d_end = []; -} +//fourier = { +${METPLUS_FOURIER_DICT} //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/MODEConfig_wrapped b/parm/met_config/MODEConfig_wrapped index 5f0442addc..a5ae8e4d4b 100644 --- a/parm/met_config/MODEConfig_wrapped +++ b/parm/met_config/MODEConfig_wrapped @@ -210,12 +210,15 @@ plot_gcarc_flag = FALSE; // // NetCDF matched pairs, PostScript, and contingency table output files // -ps_plot_flag = TRUE; +//ps_plot_flag = +${METPLUS_PS_PLOT_FLAG} //nc_pairs_flag = { ${METPLUS_NC_PAIRS_FLAG_DICT} -ct_stats_flag = TRUE; +//ct_stats_flag = +${METPLUS_CT_STATS_FLAG} + //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/PB2NCConfig_wrapped b/parm/met_config/PB2NCConfig_wrapped index 25cab0375b..4701ccf25c 100644 --- a/parm/met_config/PB2NCConfig_wrapped +++ b/parm/met_config/PB2NCConfig_wrapped @@ -94,26 +94,13 @@ ${METPLUS_OBS_BUFR_VAR} // Mapping of BUFR variable name to GRIB name. The default map is defined at // obs_prepbufr_map. This replaces/expends the default map. // -obs_bufr_map = []; +//obs_bufr_map = +${METPLUS_OBS_BUFR_MAP} // This map is for PREPBUFR. It will be added into obs_bufr_map. // Please do not override this map. -obs_prefbufr_map = [ - { key = "POB"; val = "PRES"; }, - { key = "QOB"; val = "SPFH"; }, - { key = "TOB"; val = "TMP"; }, - { key = "ZOB"; val = "HGT"; }, - { key = "UOB"; val = "UGRD"; }, - { key = "VOB"; val = "VGRD"; }, - { key = "D_DPT"; val = "DPT"; }, - { key = "D_WDIR"; val = "WDIR"; }, - { key = "D_WIND"; val = "WIND"; }, - { key = "D_RH"; val = "RH"; }, - { key = "D_MIXR"; val = "MIXR"; }, - { key = "D_PRMSL"; val = "PRMSL"; }, - { key = "D_PBL"; val = "PBL"; }, - { key = "D_CAPE"; val = "CAPE"; } -]; +//obs_prepbufr_map = +${METPLUS_OBS_PREPBUFR_MAP} //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/PointStatConfig_wrapped b/parm/met_config/PointStatConfig_wrapped index 824aed7145..9ab367e182 100644 --- a/parm/met_config/PointStatConfig_wrapped +++ b/parm/met_config/PointStatConfig_wrapped @@ -77,14 +77,8 @@ obs_perc_value = 50; // // Mapping of message type group name to comma-separated list of values. // -message_type_group_map = [ - { key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET"; }, - { key = "ANYAIR"; val = "AIRCAR,AIRCFT"; }, - { key = "ANYSFC"; val = "ADPSFC,SFCSHP,ADPUPA,PROFLR,MSONET"; }, - { key = "ONLYSF"; val = "ADPSFC,SFCSHP"; }, - { key = "LANDSF"; val = "ADPSFC,MSONET"; }, - { key = "WATERSF"; val = "SFCSHP"; } -]; +//message_type_group_map = +${METPLUS_MESSAGE_TYPE_GROUP_MAP} //////////////////////////////////////////////////////////////////////////////// @@ -121,7 +115,8 @@ mask = { ${METPLUS_MASK_GRID} ${METPLUS_MASK_POLY} ${METPLUS_MASK_SID} - llpnt = []; + //llpnt = + ${METPLUS_MASK_LLPNT} } //////////////////////////////////////////////////////////////////////////////// @@ -152,13 +147,8 @@ ${METPLUS_INTERP_DICT} // // HiRA verification method // -hira = { - flag = FALSE; - width = [ 2, 3, 4, 5 ]; - vld_thresh = 1.0; - cov_thresh = [ ==0.25 ]; - shape = SQUARE; -} +//hira = { +${METPLUS_HIRA_DICT} //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/SeriesAnalysisConfig_wrapped b/parm/met_config/SeriesAnalysisConfig_wrapped index 94fd1b2629..a8cf1af226 100644 --- a/parm/met_config/SeriesAnalysisConfig_wrapped +++ b/parm/met_config/SeriesAnalysisConfig_wrapped @@ -102,20 +102,8 @@ ${METPLUS_VLD_THRESH} // // Statistical output types // -output_stats = { - fho = []; - ctc = []; - ${METPLUS_CTS_LIST} - mctc = []; - mcts = []; - ${METPLUS_STAT_LIST} - sl1l2 = []; - sal1l2 = []; - pct = []; - pstd = []; - pjc = []; - prc = []; -} +//output_stats = { +${METPLUS_OUTPUT_STATS_DICT} //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/met_config/TCPairsConfig_wrapped b/parm/met_config/TCPairsConfig_wrapped index ce13c1db82..67246863de 100644 --- a/parm/met_config/TCPairsConfig_wrapped +++ b/parm/met_config/TCPairsConfig_wrapped @@ -82,13 +82,16 @@ valid_mask = ""; // // Specify if the code should check for duplicate ATCF lines // -check_dup = FALSE; +//check_dup = +${METPLUS_CHECK_DUP} + // // Specify special processing to be performed for interpolated models. // Set to NONE, FILL, or REPLACE. // -interp12 = REPLACE; +//interp12 = +${METPLUS_INTERP12} // // Specify how consensus forecasts should be defined diff --git a/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf b/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf index d9d6d475ef..e4316e8641 100644 --- a/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf +++ b/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf @@ -174,3 +174,9 @@ GRID_STAT_NC_PAIRS_FLAG_APPLY_MASK = FALSE #GRID_STAT_DISTANCE_MAP_FOM_ALPHA = #GRID_STAT_DISTANCE_MAP_ZHU_WEIGHT = #GRID_STAT_DISTANCE_MAP_BETA_VALUE_N = + +#GRID_STAT_FOURIER_WAVE_1D_BEG = +#GRID_STAT_FOURIER_WAVE_1D_END = + +#GRID_STAT_CENSOR_THRESH = +#GRID_STAT_CENSOR_VAL = diff --git a/parm/use_cases/met_tool_wrapper/MODE/MODE.conf b/parm/use_cases/met_tool_wrapper/MODE/MODE.conf index 35bc4c2f6d..6cbc269527 100644 --- a/parm/use_cases/met_tool_wrapper/MODE/MODE.conf +++ b/parm/use_cases/met_tool_wrapper/MODE/MODE.conf @@ -1,7 +1,5 @@ [config] -# MODE METplus Configuration - PROCESS_LIST = MODE LOOP_ORDER = times @@ -119,3 +117,6 @@ MODE_GRID_RES = 40 #MODE_NC_PAIRS_FLAG_POLYLINES = MODE_QUILT = True + +#MODE_PS_PLOT_FLAG = +#MODE_CT_STATS_FLAG = diff --git a/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf b/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf index 14cc9b405f..a919fc00b9 100644 --- a/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf +++ b/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf @@ -1,78 +1,34 @@ -# PrepBufr to NetCDF Configurations - -# section heading for [config] variables - all items below this line and -# before the next section heading correspond to the [config] section [config] -# List of applications to run - only PB2NC for this case PROCESS_LIST = PB2NC -# time looping - options are INIT, VALID, RETRO, and REALTIME -# If set to INIT or RETRO: -# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set -# If set to VALID or REALTIME: -# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set LOOP_BY = VALID - -# Format of VALID_BEG and VALID_END using % items -# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. -# see www.strftime.org for more information -# %Y%m%d%H expands to YYYYMMDDHH VALID_TIME_FMT = %Y%m%d%H - -# Start time for METplus run - must match VALID_TIME_FMT VALID_BEG = 2007033112 - -# End time for METplus run - must match VALID_TIME_FMT VALID_END = 2007033112 - -# Increment between METplus runs (in seconds if no units are specified) -# Must be >= 60 seconds VALID_INCREMENT = 1M -# List of forecast leads to process for each run time (init or valid) -# In hours if units are not specified -# If unset, defaults to 0 (don't loop through forecast leads) LEAD_SEQ = 0 -# list of offsets in the prepBUFR input filenames to allow. List is in order of preference -# i.e. if 12, 6 is listed, it will try to use a 12 offset file and then try to use a 6 offset -# if the 12 does not exist PB2NC_OFFSETS = 12 -# Order of loops to process data - Options are times, processes -# Not relevant if only one item is in the PROCESS_LIST -# times = run all wrappers in the PROCESS_LIST for a single run time, then -# increment the run time and run all wrappers again until all times have -# been evaluated. -# processes = run the first wrapper in the PROCESS_LIST for all times -# specified, then repeat for the next item in the PROCESS_LIST until all -# wrappers have been run -LOOP_ORDER = processes - -# Location of MET config file to pass to PB2NC -# References CONFIG_DIR from the [dir] section -PB2NC_CONFIG_FILE = {CONFIG_DIR}/PB2NCConfig_wrapped - -# If set to True, skip run if the output file determined by the output directory and -# filename template already exists PB2NC_SKIP_IF_OUTPUT_EXISTS = True -# Time relative to each input file's valid time (in seconds if no units are specified) for data within the file to be -# considered valid. Values are set in the 'obs_window' dictionary in the PB2NC config file +PB2NC_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_obs/prepbufr +PB2NC_INPUT_TEMPLATE = ndas.t{da_init?fmt=%2H}z.prepbufr.tm{offset?fmt=%2H}.{da_init?fmt=%Y%m%d}.nr + +PB2NC_OUTPUT_DIR = {OUTPUT_BASE}/pb2nc +PB2NC_OUTPUT_TEMPLATE = sample_pb.nc + + +PB2NC_CONFIG_FILE = {PARM_BASE}/met_config/PB2NCConfig_wrapped + PB2NC_WINDOW_BEGIN = -1800 PB2NC_WINDOW_END = 1800 -# controls the window of time around the current run time to be considered to be valid for all -# input files, not just relative to each input files valid time -# if set, these values are substituted with the times from the current run time and passed to -# PB2NC on the command line with -valid_beg and -valid_end arguments. -# Not used if unset or set to an empty string PB2NC_VALID_BEGIN = {valid?fmt=%Y%m%d_%H} PB2NC_VALID_END = {valid?fmt=%Y%m%d_%H?shift=1d} -# Values to pass to pb2nc config file using environment variables of the same name. -# See MET User's Guide for more information PB2NC_GRID = G212 PB2NC_POLY = PB2NC_STATION_ID = @@ -90,8 +46,6 @@ PB2NC_QUALITY_MARK_THRESH = 3 # Leave empty to process all PB2NC_OBS_BUFR_VAR_LIST = QOB, TOB, ZOB, UOB, VOB, D_DPT, D_WIND, D_RH, D_MIXR -# For defining the time periods for summarization - PB2NC_TIME_SUMMARY_FLAG = False PB2NC_TIME_SUMMARY_BEG = 000000 PB2NC_TIME_SUMMARY_END = 235959 @@ -105,22 +59,5 @@ PB2NC_TIME_SUMMARY_GRIB_CODES = PB2NC_TIME_SUMMARY_VALID_FREQ = 0 PB2NC_TIME_SUMMARY_VALID_THRESH = 0.0 -# End of [config] section and start of [dir] section -[dir] -# location of configuration files used by MET applications -CONFIG_DIR = {PARM_BASE}/met_config - -# directory containing input to PB2NC -PB2NC_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_obs/prepbufr - -# directory to write output from PB2NC -PB2NC_OUTPUT_DIR = {OUTPUT_BASE}/pb2nc - - -# End of [dir] section and start of [filename_templates] section -[filename_templates] -# Template to look for forecast input to PB2NC relative to PB2NC_INPUT_DIR -PB2NC_INPUT_TEMPLATE = ndas.t{da_init?fmt=%2H}z.prepbufr.tm{offset?fmt=%2H}.{da_init?fmt=%Y%m%d}.nr - -# Template to use to write output from PB2NC -PB2NC_OUTPUT_TEMPLATE = sample_pb.nc +#PB2NC_OBS_BUFR_MAP = +#PB2NC_OBS_PREPBUFR_MAP = diff --git a/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf b/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf index 36875e0a79..cc43470edb 100644 --- a/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf +++ b/parm/use_cases/met_tool_wrapper/PointStat/PointStat.conf @@ -1,56 +1,73 @@ [config] -# List of applications to run - only PointStat for this case PROCESS_LIST = PointStat -# time looping - options are INIT, VALID, RETRO, and REALTIME -# If set to INIT or RETRO: -# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set -# If set to VALID or REALTIME: -# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set -LOOP_BY = INIT +### +# Time Info +### -# Format of INIT_BEG and INIT_END using % items -# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. -# see www.strftime.org for more information -# %Y%m%d%H expands to YYYYMMDDHH +LOOP_BY = INIT INIT_TIME_FMT = %Y%m%d - -# Start time for METplus run - must match INIT_TIME_FMT INIT_BEG = 20070330 - -# End time for METplus run - must match INIT_TIME_FMT INIT_END = 20070330 - -# Increment between METplus runs (in seconds if no units are specified) -# Must be >= 60 seconds INIT_INCREMENT = 1M -# List of forecast leads to process for each run time (init or valid) -# In hours if units are not specified -# If unset, defaults to 0 (don't loop through forecast leads) LEAD_SEQ = 36 -# Order of loops to process data - Options are times, processes -# Not relevant if only one item is in the PROCESS_LIST -# times = run all wrappers in the PROCESS_LIST for a single run time, then -# increment the run time and run all wrappers again until all times have -# been evaluated. -# processes = run the first wrapper in the PROCESS_LIST for all times -# specified, then repeat for the next item in the PROCESS_LIST until all -# wrappers have been run -LOOP_ORDER = processes - -# Verbosity of MET output - overrides LOG_VERBOSITY for PointStat only +### +# File I/O +### + +FCST_POINT_STAT_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst +FCST_POINT_STAT_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/nam.t00z.awip1236.tm00.{init?fmt=%Y%m%d}.grb + +OBS_POINT_STAT_INPUT_DIR = {INPUT_BASE}/met_test/out/pb2nc +OBS_POINT_STAT_INPUT_TEMPLATE = sample_pb.nc + +POINT_STAT_OUTPUT_DIR = {OUTPUT_BASE}/point_stat + +POINT_STAT_CLIMO_MEAN_INPUT_DIR = +POINT_STAT_CLIMO_MEAN_INPUT_TEMPLATE = + +POINT_STAT_CLIMO_STDEV_INPUT_DIR = +POINT_STAT_CLIMO_STDEV_INPUT_TEMPLATE = + + +### +# Field Info +### + +POINT_STAT_ONCE_PER_FIELD = False + +FCST_VAR1_NAME = TMP +FCST_VAR1_LEVELS = P750-900 +FCST_VAR1_THRESH = <=273, >273 +OBS_VAR1_NAME = TMP +OBS_VAR1_LEVELS = P750-900 +OBS_VAR1_THRESH = <=273, >273 + +FCST_VAR2_NAME = UGRD +FCST_VAR2_LEVELS = Z10 +FCST_VAR2_THRESH = >=5 +OBS_VAR2_NAME = UGRD +OBS_VAR2_LEVELS = Z10 +OBS_VAR2_THRESH = >=5 + +FCST_VAR3_NAME = VGRD +FCST_VAR3_LEVELS = Z10 +FCST_VAR3_THRESH = >=5 +OBS_VAR3_NAME = VGRD +OBS_VAR3_LEVELS = Z10 +OBS_VAR3_THRESH = >=5 + +### +# PointStat +### + #LOG_POINT_STAT_VERBOSITY = 2 -# Location of MET config file to pass to GridStat -# References PARM_BASE which is the location of the parm directory corresponding -# to the ush directory of the run_metplus.py script that is called -# or the value of the environment variable METPLUS_PARM_BASE if set POINT_STAT_CONFIG_FILE ={PARM_BASE}/met_config/PointStatConfig_wrapped - #POINT_STAT_OBS_QUALITY_INC = 1, 2, 3 #POINT_STAT_OBS_QUALITY_EXC = @@ -89,115 +106,37 @@ POINT_STAT_OUTPUT_FLAG_VL1L2 = STAT #POINT_STAT_HSS_EC_VALUE = -# Time relative to each input file's valid time (in seconds if no units are specified) for data within the file to be -# considered valid. Values are set in the 'obs_window' dictionary in the PointStat config file OBS_POINT_STAT_WINDOW_BEGIN = -5400 OBS_POINT_STAT_WINDOW_END = 5400 -# Optional list of offsets to look for point observation data POINT_STAT_OFFSETS = 0 -# Model/fcst and obs name, e.g. GFS, NAM, GDAS, etc. MODEL = WRF POINT_STAT_DESC = NA OBTYPE = -# Regrid to specified grid. Indicate NONE if no regridding, or the grid id -# (e.g. G212) POINT_STAT_REGRID_TO_GRID = NONE POINT_STAT_REGRID_METHOD = BILIN POINT_STAT_REGRID_WIDTH = 2 POINT_STAT_OUTPUT_PREFIX = -# sets the -obs_valid_beg command line argument (optional) -# not used for this example #POINT_STAT_OBS_VALID_BEG = {valid?fmt=%Y%m%d_%H} - -# sets the -obs_valid_end command line argument (optional) -# not used for this example #POINT_STAT_OBS_VALID_END = {valid?fmt=%Y%m%d_%H} -# Verification Masking regions -# Indicate which grid and polygon masking region, if applicable -POINT_STAT_GRID = DTC165, DTC166 - -# List of full path to poly masking files. NOTE: Only short lists of poly -# files work (those that fit on one line), a long list will result in an -# environment variable that is too long, resulting in an error. For long -# lists of poly masking files (i.e. all the mask files in the NCEP_mask -# directory), define these in the MET point_stat configuration file. -POINT_STAT_POLY = MET_BASE/poly/LMV.poly -POINT_STAT_STATION_ID = +POINT_STAT_MASK_GRID = DTC165, DTC166 +POINT_STAT_MASK_POLY = MET_BASE/poly/LMV.poly +POINT_STAT_MASK_SID = +#POINT_STAT_MASK_LLPNT = -# Message types, if all message types are to be returned, leave this empty, -# otherwise indicate the message types of interest. POINT_STAT_MESSAGE_TYPE = ADPUPA, ADPSFC -# Variables and levels as specified in the field dictionary of the MET -# point_stat configuration file. Specify as FCST_VARn_NAME, FCST_VARn_LEVELS, -# (optional) FCST_VARn_OPTION - -# set to True to run PointStat once for each name/level combination -# set to False to run PointStat once per run time including all fields -POINT_STAT_ONCE_PER_FIELD = False - -# fields to compare -# Note: If FCST_VAR_* is set, then a corresponding OBS_VAR_* variable must be set -# To use one variables for both forecast and observation data, set BOTH_VAR_* instead -FCST_VAR1_NAME = TMP -FCST_VAR1_LEVELS = P750-900 -FCST_VAR1_THRESH = <=273, >273 -OBS_VAR1_NAME = TMP -OBS_VAR1_LEVELS = P750-900 -OBS_VAR1_THRESH = <=273, >273 - -FCST_VAR2_NAME = UGRD -FCST_VAR2_LEVELS = Z10 -FCST_VAR2_THRESH = >=5 -OBS_VAR2_NAME = UGRD -OBS_VAR2_LEVELS = Z10 -OBS_VAR2_THRESH = >=5 - -FCST_VAR3_NAME = VGRD -FCST_VAR3_LEVELS = Z10 -FCST_VAR3_THRESH = >=5 -OBS_VAR3_NAME = VGRD -OBS_VAR3_LEVELS = Z10 -OBS_VAR3_THRESH = >=5 - - -# End of [config] section and start of [dir] section -[dir] -FCST_POINT_STAT_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst -OBS_POINT_STAT_INPUT_DIR = {INPUT_BASE}/met_test/out/pb2nc - -# directory containing climatology mean input to PointStat -# Not used in this example -POINT_STAT_CLIMO_MEAN_INPUT_DIR = - -# directory containing climatology mean input to PointStat -# Not used in this example -POINT_STAT_CLIMO_STDEV_INPUT_DIR = - -POINT_STAT_OUTPUT_DIR = {OUTPUT_BASE}/point_stat - - -# End of [dir] section and start of [filename_templates] section -[filename_templates] - -# Template to look for forecast input to PointStat relative to FCST_POINT_STAT_INPUT_DIR -FCST_POINT_STAT_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/nam.t00z.awip1236.tm00.{init?fmt=%Y%m%d}.grb - -# Template to look for observation input to PointStat relative to OBS_POINT_STAT_INPUT_DIR -OBS_POINT_STAT_INPUT_TEMPLATE = sample_pb.nc - -# Template to look for climatology input to PointStat relative to POINT_STAT_CLIMO_MEAN_INPUT_DIR -# Not used in this example -POINT_STAT_CLIMO_MEAN_INPUT_TEMPLATE = - -# Template to look for climatology input to PointStat relative to POINT_STAT_CLIMO_STDEV_INPUT_DIR -# Not used in this example -POINT_STAT_CLIMO_STDEV_INPUT_TEMPLATE = +#POINT_STAT_HIRA_FLAG = +#POINT_STAT_HIRA_WIDTH = +#POINT_STAT_HIRA_VLD_THRESH = +#POINT_STAT_HIRA_COV_THRESH = +#POINT_STAT_HIRA_SHAPE = +#POINT_STAT_HIRA_PROB_CAT_THRESH = +#POINT_STAT_MESSAGE_TYPE_GROUP_MAP = diff --git a/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf b/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf index 8f450802a1..230c081f1d 100644 --- a/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf +++ b/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf @@ -1,54 +1,86 @@ -# SeriesAnalysis METplus Configuration - [config] PROCESS_LIST = SeriesAnalysis -LOOP_BY = INIT +### +# Time Info +### +LOOP_BY = INIT INIT_TIME_FMT = %Y%m%d%H - INIT_BEG=2005080700 - INIT_END=2005080700 - INIT_INCREMENT = 12H LEAD_SEQ = 12, 9, 6 -SERIES_ANALYSIS_CUSTOM_LOOP_LIST = +SERIES_ANALYSIS_RUNTIME_FREQ = RUN_ONCE_PER_INIT_OR_VALID -LOOP_ORDER = processes +SERIES_ANALYSIS_RUN_ONCE_PER_STORM_ID = False -#LOG_SERIES_ANALYSIS_VERBOSITY = 2 +SERIES_ANALYSIS_CUSTOM_LOOP_LIST = -SERIES_ANALYSIS_IS_PAIRED = False +### +# File I/O +### -SERIES_ANALYSIS_GENERATE_PLOTS = no +FCST_SERIES_ANALYSIS_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst +FCST_SERIES_ANALYSIS_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/wrfprs_ruc13_{lead?fmt=%2H}.tm00_G212 -PLOT_DATA_PLANE_TITLE = +OBS_SERIES_ANALYSIS_INPUT_DIR = {INPUT_BASE}/met_test/new +OBS_SERIES_ANALYSIS_INPUT_TEMPLATE = ST2ml{valid?fmt=%Y%m%d%H}_A03h.nc -SERIES_ANALYSIS_GENERATE_ANIMATIONS = no +SERIES_ANALYSIS_TC_STAT_INPUT_DIR = +SERIES_ANALYSIS_TC_STAT_INPUT_TEMPLATE = + +SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/met_tool_wrapper/SeriesAnalysis +SERIES_ANALYSIS_OUTPUT_TEMPLATE = {init?fmt=%Y%m%d%H}_sa.nc + +SERIES_ANALYSIS_CLIMO_MEAN_INPUT_DIR = +SERIES_ANALYSIS_CLIMO_MEAN_INPUT_TEMPLATE = + +SERIES_ANALYSIS_CLIMO_STDEV_INPUT_DIR = +SERIES_ANALYSIS_CLIMO_STDEV_INPUT_TEMPLATE = + + +### +# Field Info +### + +MODEL = WRF +OBTYPE = MC_PCP + +FCST_VAR1_NAME = APCP +FCST_VAR1_LEVELS = A03 + +OBS_VAR1_NAME = APCP_03 +OBS_VAR1_LEVELS = "(*,*)" + +BOTH_VAR1_THRESH = gt12.7, gt25.4, gt50.8, gt76.2 -SERIES_ANALYSIS_CONFIG_FILE = {CONFIG_DIR}/SeriesAnalysisConfig_wrapped +### +# SeriesAnalysis +### -SERIES_ANALYSIS_STAT_LIST = TOTAL, RMSE, FBAR, OBAR +#LOG_SERIES_ANALYSIS_VERBOSITY = 2 + +SERIES_ANALYSIS_CONFIG_FILE = {PARM_BASE}/met_config/SeriesAnalysisConfig_wrapped -SERIES_ANALYSIS_DESC = +SERIES_ANALYSIS_IS_PAIRED = False -SERIES_ANALYSIS_CAT_THRESH = +#SERIES_ANALYSIS_DESC = -SERIES_ANALYSIS_VLD_THRESH = +#SERIES_ANALYSIS_CAT_THRESH = -SERIES_ANALYSIS_BLOCK_SIZE = +#SERIES_ANALYSIS_VLD_THRESH = -SERIES_ANALYSIS_CTS_LIST = +#SERIES_ANALYSIS_BLOCK_SIZE = -SERIES_ANALYSIS_REGRID_TO_GRID = -SERIES_ANALYSIS_REGRID_METHOD = -SERIES_ANALYSIS_REGRID_WIDTH = -SERIES_ANALYSIS_REGRID_VLD_THRESH = -SERIES_ANALYSIS_REGRID_SHAPE = +#SERIES_ANALYSIS_REGRID_TO_GRID = +#SERIES_ANALYSIS_REGRID_METHOD = +#SERIES_ANALYSIS_REGRID_WIDTH = +#SERIES_ANALYSIS_REGRID_VLD_THRESH = +#SERIES_ANALYSIS_REGRID_SHAPE = #SERIES_ANALYSIS_CLIMO_MEAN_FILE_NAME = #SERIES_ANALYSIS_CLIMO_MEAN_FIELD = @@ -74,51 +106,31 @@ SERIES_ANALYSIS_REGRID_SHAPE = #SERIES_ANALYSIS_HSS_EC_VALUE = -SERIES_ANALYSIS_RUNTIME_FREQ = RUN_ONCE_PER_INIT_OR_VALID +#FCST_SERIES_ANALYSIS_PROB_THRESH = -SERIES_ANALYSIS_RUN_ONCE_PER_STORM_ID = False +#SERIES_ANALYSIS_OUTPUT_STATS_FHO = +#SERIES_ANALYSIS_OUTPUT_STATS_CTC = +#SERIES_ANALYSIS_OUTPUT_STATS_CTS = +#SERIES_ANALYSIS_OUTPUT_STATS_MCTC = +#SERIES_ANALYSIS_OUTPUT_STATS_MCTS = -MODEL = WRF +SERIES_ANALYSIS_OUTPUT_STATS_CNT = TOTAL, RMSE, FBAR, OBAR -OBTYPE = MC_PCP - -#FCST_SERIES_ANALYSIS_PROB_THRESH = +#SERIES_ANALYSIS_OUTPUT_STATS_SL1L2 = +#SERIES_ANALYSIS_OUTPUT_STATS_SAL1L2 = +#SERIES_ANALYSIS_OUTPUT_STATS_PCT = +#SERIES_ANALYSIS_OUTPUT_STATS_PSTD = +#SERIES_ANALYSIS_OUTPUT_STATS_PJC = +#SERIES_ANALYSIS_OUTPUT_STATS_PRC = -FCST_VAR1_NAME = APCP - -FCST_VAR1_LEVELS = A03 -OBS_VAR1_NAME = APCP_03 - -OBS_VAR1_LEVELS = "(*,*)" - -BOTH_VAR1_THRESH = gt12.7, gt25.4, gt50.8, gt76.2 - -[dir] -CONFIG_DIR={PARM_BASE}/met_config - -FCST_SERIES_ANALYSIS_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst - -OBS_SERIES_ANALYSIS_INPUT_DIR = {INPUT_BASE}/met_test/new +### +# Plotting +### -SERIES_ANALYSIS_CLIMO_MEAN_INPUT_DIR = - -SERIES_ANALYSIS_CLIMO_STDEV_INPUT_DIR = - -SERIES_ANALYSIS_TC_STAT_INPUT_DIR = - -SERIES_ANALYSIS_OUTPUT_DIR = {OUTPUT_BASE}/met_tool_wrapper/SeriesAnalysis - -[filename_templates] - -FCST_SERIES_ANALYSIS_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/wrfprs_ruc13_{lead?fmt=%2H}.tm00_G212 - -OBS_SERIES_ANALYSIS_INPUT_TEMPLATE = ST2ml{valid?fmt=%Y%m%d%H}_A03h.nc - -SERIES_ANALYSIS_OUTPUT_TEMPLATE = {init?fmt=%Y%m%d%H}_sa.nc +SERIES_ANALYSIS_GENERATE_PLOTS = no -SERIES_ANALYSIS_CLIMO_MEAN_INPUT_TEMPLATE = +PLOT_DATA_PLANE_TITLE = -SERIES_ANALYSIS_CLIMO_STDEV_INPUT_TEMPLATE = +SERIES_ANALYSIS_GENERATE_ANIMATIONS = no -SERIES_ANALYSIS_TC_STAT_INPUT_TEMPLATE = diff --git a/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_extra_tropical.conf b/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_extra_tropical.conf index 62640e4ba8..255d41c784 100644 --- a/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_extra_tropical.conf +++ b/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_extra_tropical.conf @@ -1,37 +1,55 @@ -# -# CONFIGURATION -# [config] -# Looping by processes: run each 'task' in the PROCESS_LIST for all -# defined times then steps to the next 'task'. -LOOP_ORDER = processes - -# Configuration files -TC_PAIRS_CONFIG_FILE = {CONFIG_DIR}/TCPairsConfig_wrapped - PROCESS_LIST = TCPairs -# The init time begin and end times, increment +### +# Time Info +### + LOOP_BY = INIT INIT_TIME_FMT = %Y%m%d%H INIT_BEG = 2014121318 INIT_END = 2014121318 - -# This is the step-size. Increment in seconds from the begin time to the end -# time INIT_INCREMENT = 21600 ;; set to every 6 hours=21600 seconds TC_PAIRS_RUN_ONCE = True -# A list of times to include, in format YYYYMMDD_hh -TC_PAIRS_INIT_INCLUDE = -# A list of times to exclude, in format YYYYMMDD_hh +### +# File I/O +### + +TC_PAIRS_ADECK_INPUT_DIR = {INPUT_BASE}/met_test/new/track_data +TC_PAIRS_ADECK_TEMPLATE = {date?fmt=%Y%m}/a{basin?fmt=%s}q{date?fmt=%Y%m}*.gfso.{cyclone?fmt=%s} + +TC_PAIRS_BDECK_INPUT_DIR = {TC_PAIRS_ADECK_INPUT_DIR} +TC_PAIRS_BDECK_TEMPLATE = {date?fmt=%Y%m}/b{basin?fmt=%s}q{date?fmt=%Y%m}*.gfso.{cyclone?fmt=%s} + +TC_PAIRS_REFORMAT_DIR = {OUTPUT_BASE}/track_data_atcf + +TC_PAIRS_OUTPUT_DIR = {OUTPUT_BASE}/tc_pairs +TC_PAIRS_OUTPUT_TEMPLATE = {date?fmt=%Y%m}/{basin?fmt=%s}q{date?fmt=%Y%m%d%H}.gfso.{cyclone?fmt=%s} + + +TC_PAIRS_SKIP_IF_OUTPUT_EXISTS = yes +TC_PAIRS_SKIP_IF_REFORMAT_EXISTS = yes + +TC_PAIRS_READ_ALL_FILES = no +#TC_PAIRS_SKIP_LEAD_SEQ = False + +TC_PAIRS_REFORMAT_DECK = yes +TC_PAIRS_REFORMAT_TYPE = SBU + + +### +# TCPairs +### + +TC_PAIRS_CONFIG_FILE = {PARM_BASE}/met_config/TCPairsConfig_wrapped + +TC_PAIRS_INIT_INCLUDE = TC_PAIRS_INIT_EXCLUDE = -# Specify model init time window in format YYYYMM[DD[_hh]] -# Only tracks that fall within the initialization time window will be used TC_PAIRS_INIT_BEG = 2014121318 TC_PAIRS_INIT_END = 2014121418 @@ -40,55 +58,20 @@ TC_PAIRS_INIT_END = 2014121418 #TC_PAIRS_WRITE_VALID = -# Specify model valid time window in format YYYYMM[DD[_hh]] -# Only tracks that fall within the valid time window will be used TC_PAIRS_VALID_BEG = TC_PAIRS_VALID_END = -# Skip looping over forecast leads if a list is provided -#TC_PAIRS_SKIP_LEAD_SEQ = False - - -## -# -# MET TC-Pairs -# -## - -# -# Run MET tc_pairs by indicating the top-level directories for the A-deck -# and B-deck files. Set to 'yes' to run using top-level directories, 'no' -# if you want to run tc_pairs on files paired by the wrapper. -TC_PAIRS_READ_ALL_FILES = no - -# List of models to be used (white space or comma separated) eg: DSHP, LGEM, HWRF -# If no models are listed, then process all models in the input file(s). MODEL = #TC_PAIRS_DESC = -# List of storm ids of interest (space or comma separated) e.g.: AL112012, AL122012 -# If no storm ids are listed, then process all storm ids in the input file(s). TC_PAIRS_STORM_ID = - -# Basins (of origin/region). Indicate with space or comma-separated list of regions, eg. AL: for North Atlantic, -# WP: Western North Pacific, CP: Central North Pacific, SH: Southern Hemisphere, IO: North Indian Ocean, LS: Southern -# Hemisphere TC_PAIRS_BASIN = - -# Cyclone, a space or comma-separated list of cyclone numbers. If left empty, all cyclones will be used. TC_PAIRS_CYCLONE = - -# Storm name, a space or comma-separated list of storm names to evaluate. If left empty, all storms will be used. TC_PAIRS_STORM_NAME = -# DLAND file, the full path of the file that contains the gridded representation of the -# minimum distance from land. TC_PAIRS_DLAND_FILE = {MET_INSTALL_DIR}/share/met/tc_data/dland_global_tenth_degree.nc -TC_PAIRS_REFORMAT_DECK = yes -TC_PAIRS_REFORMAT_TYPE = SBU - TC_PAIRS_MISSING_VAL_TO_REPLACE = -99 TC_PAIRS_MISSING_VAL = -9999 @@ -97,43 +80,6 @@ TC_PAIRS_MISSING_VAL = -9999 #TC_PAIRS_CONSENSUS1_REQUIRED = #TC_PAIRS_CONSENSUS1_MIN_REQ = -# OVERWRITE OPTIONS -# Don't overwrite filter files if they already exist. -# Set to no if you do NOT want to override existing files -# Set to yes if you do want to override existing files - -# overwrite modified track data (non-ATCF to ATCF format) -TC_PAIRS_SKIP_IF_REFORMAT_EXISTS = yes - -# overwrite tc_pairs output -TC_PAIRS_SKIP_IF_OUTPUT_EXISTS = yes - -# FILENAME TEMPLATES -# -[filename_templates] -# Define the format of the filenames -TC_PAIRS_ADECK_TEMPLATE = {date?fmt=%Y%m}/a{basin?fmt=%s}q{date?fmt=%Y%m}*.gfso.{cyclone?fmt=%s} -TC_PAIRS_BDECK_TEMPLATE = {date?fmt=%Y%m}/b{basin?fmt=%s}q{date?fmt=%Y%m}*.gfso.{cyclone?fmt=%s} -TC_PAIRS_OUTPUT_TEMPLATE = {date?fmt=%Y%m}/{basin?fmt=%s}q{date?fmt=%Y%m%d%H}.gfso.{cyclone?fmt=%s} - -# -# DIRECTORIES -# -[dir] - -# MET config directory, location of configuration files used by MET applications -# CONFIG_DIR and the value it expands to is set as an environment variable -# and is used in the MET configuration file. -CONFIG_DIR={PARM_BASE}/met_config - -# track data, set to your data source -TC_PAIRS_ADECK_INPUT_DIR = {INPUT_BASE}/met_test/new/track_data -TC_PAIRS_BDECK_INPUT_DIR = {TC_PAIRS_ADECK_INPUT_DIR} -TC_PAIRS_REFORMAT_DIR = {OUTPUT_BASE}/track_data_atcf -TC_PAIRS_OUTPUT_DIR = {OUTPUT_BASE}/tc_pairs - - -# REGEX PATTERNS -# -[regex_pattern] +#TC_PAIRS_CHECK_DUP = +#TC_PAIRS_INTERP12 = diff --git a/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.conf b/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.conf index 81f9ed99c0..2812a0cc16 100644 --- a/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.conf +++ b/parm/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.conf @@ -1,39 +1,54 @@ -# -# CONFIGURATION -# [config] -# Looping by times: steps through each 'task' in the PROCESS_LIST for each -# defined time, and repeats until all times have been evaluated. -LOOP_ORDER = times - -# Configuration files -TC_PAIRS_CONFIG_FILE = {PARM_BASE}/met_config/TCPairsConfig_wrapped - -# 'Tasks' to be run PROCESS_LIST = TCPairs -LOOP_BY = INIT +### +# Time Info +### -# The init time begin and end times, increment, and last init hour. +LOOP_BY = INIT INIT_TIME_FMT = %Y%m%d%H INIT_BEG = 2018083006 INIT_END = 2018083018 - -# This is the step-size. Increment in seconds from the begin time to the end time -# set to 6 hours = 21600 seconds INIT_INCREMENT = 21600 +#TC_PAIRS_SKIP_LEAD_SEQ = False + +LOOP_ORDER = times + TC_PAIRS_RUN_ONCE = False -# A list of times to include, in format YYYYMMDD_hh -TC_PAIRS_INIT_INCLUDE = -# A list of times to exclude, in format YYYYMMDD_hh +### +# File I/O +### + +TC_PAIRS_ADECK_INPUT_DIR = {INPUT_BASE}/met_test/new/hwrf/adeck +TC_PAIRS_ADECK_TEMPLATE = {model?fmt=%s}/*{cyclone?fmt=%s}l.{date?fmt=%Y%m%d%H}.trak.hwrf.atcfunix + +TC_PAIRS_BDECK_INPUT_DIR = {INPUT_BASE}/met_test/new/hwrf/bdeck +TC_PAIRS_BDECK_TEMPLATE = b{basin?fmt=%s}{cyclone?fmt=%s}{date?fmt=%Y}.dat + +TC_PAIRS_EDECK_INPUT_DIR = +TC_PAIRS_EDECK_TEMPLATE = + +TC_PAIRS_OUTPUT_DIR = {OUTPUT_BASE}/tc_pairs +TC_PAIRS_OUTPUT_TEMPLATE = tc_pairs_{basin?fmt=%s}{date?fmt=%Y%m%d%H}.dat + +TC_PAIRS_SKIP_IF_OUTPUT_EXISTS = no +TC_PAIRS_READ_ALL_FILES = no +TC_PAIRS_REFORMAT_DECK = no + + +### +# TCPairs +### + +TC_PAIRS_CONFIG_FILE = {PARM_BASE}/met_config/TCPairsConfig_wrapped + +TC_PAIRS_INIT_INCLUDE = TC_PAIRS_INIT_EXCLUDE = -# Specify model init time window in format YYYYMM[DD[_hh]] -# Only tracks that fall within the initialization time window will be used TC_PAIRS_INIT_BEG = TC_PAIRS_INIT_END = @@ -42,58 +57,18 @@ TC_PAIRS_INIT_END = #TC_PAIRS_WRITE_VALID = -# Specify model valid time window in format YYYYMM[DD[_hh]] -# Only tracks that fall within the valid time window will be used TC_PAIRS_VALID_BEG = TC_PAIRS_VALID_END = -# -# Run MET tc_pairs by indicating the top-level directories for the A-deck and B-deck files. Set to 'yes' to -# run using top-level directories, 'no' if you want to run tc_pairs on files paired by the wrapper. -TC_PAIRS_READ_ALL_FILES = no - -# set to true or yes to reformat track data into ATCF format expected by tc_pairs -TC_PAIRS_REFORMAT_DECK = no - -# OVERWRITE OPTIONS -# Don't overwrite filter files if they already exist. -# Set to yes if you do NOT want to override existing files -# Set to no if you do want to override existing files -TC_PAIRS_SKIP_IF_OUTPUT_EXISTS = no - -# Skip looping over forecast leads if a list is provided -#TC_PAIRS_SKIP_LEAD_SEQ = False - - -# -# MET TC-Pairs -# -# List of models to be used (white space or comma separated) eg: DSHP, LGEM, HWRF -# If no models are listed, then process all models in the input file(s). MODEL = MYNN, H19C, H19M, CTRL, MYGF #TC_PAIRS_DESC = -# List of storm ids of interest (space or comma separated) e.g.: AL112012, AL122012 -# If no storm ids are listed, then process all storm ids in the input file(s). -#TC_PAIRS_STORM_ID = ML2092014 #TC_PAIRS_STORM_ID = al062018, al092018, al132018, al142018 - -# Basins (of origin/region). Indicate with space or comma-separated list of regions, eg. AL: for North Atlantic, -# WP: Western North Pacific, CP: Central North Pacific, SH: Southern Hemisphere, IO: North Indian Ocean, LS: Southern -# Hemisphere #TC_PAIRS_BASIN = AL -TC_PAIRS_BASIN = - -# Cyclone, a space or comma-separated list of cyclone numbers. If left empty, all cyclones will be used. TC_PAIRS_CYCLONE = 06 -#TC_PAIRS_CYCLONE = - -# Storm name, a space or comma-separated list of storm names to evaluate. If left empty, all storms will be used. TC_PAIRS_STORM_NAME = -# DLAND file, the full path of the file that contains the gridded representation of the -# minimum distance from land. TC_PAIRS_DLAND_FILE = MET_BASE/tc_data/dland_global_tenth_degree.nc #TC_PAIRS_CONSENSUS1_NAME = @@ -101,20 +76,6 @@ TC_PAIRS_DLAND_FILE = MET_BASE/tc_data/dland_global_tenth_degree.nc #TC_PAIRS_CONSENSUS1_REQUIRED = #TC_PAIRS_CONSENSUS1_MIN_REQ = -# -# DIRECTORIES -# -[dir] -# Location of input track data directory -# for ADECK and BDECK data -TC_PAIRS_ADECK_INPUT_DIR = {INPUT_BASE}/met_test/new/hwrf/adeck -TC_PAIRS_EDECK_INPUT_DIR = -TC_PAIRS_BDECK_INPUT_DIR = {INPUT_BASE}/met_test/new/hwrf/bdeck +#TC_PAIRS_CHECK_DUP = -TC_PAIRS_OUTPUT_DIR = {OUTPUT_BASE}/tc_pairs - -[filename_templates] -TC_PAIRS_ADECK_TEMPLATE = {model?fmt=%s}/*{cyclone?fmt=%s}l.{date?fmt=%Y%m%d%H}.trak.hwrf.atcfunix -TC_PAIRS_EDECK_TEMPLATE = -TC_PAIRS_BDECK_TEMPLATE = b{basin?fmt=%s}{cyclone?fmt=%s}{date?fmt=%Y}.dat -TC_PAIRS_OUTPUT_TEMPLATE = tc_pairs_{basin?fmt=%s}{date?fmt=%Y%m%d%H}.dat +#TC_PAIRS_INTERP12 = From 7e95915f142ce658333cf268119e43e1082c594d Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 30 Dec 2021 10:16:41 -0700 Subject: [PATCH 245/770] removed incorrect search keyword --- .../Point2Grid_obsLSR_ObsOnly_PracticallyPerfect.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect.py b/docs/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect.py index 52cf171f73..cb1c964e5b 100644 --- a/docs/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect.py +++ b/docs/use_cases/model_applications/convection_allowing_models/Point2Grid_obsLSR_ObsOnly_PracticallyPerfect.py @@ -145,7 +145,6 @@ # * ASCII2NCToolUseCase # * Point2GridUseCase # * RegridDataPlaneToolUseCase -# * PyEmbedIngestToolUseCase # * RegriddingInToolUseCase # * NetCDFFileUseCase # * PythonEmbeddingFileUseCase From e5f53b13fe7b1d148eae5bf32138ca6cd4fc98fe Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 30 Dec 2021 15:38:12 -0700 Subject: [PATCH 246/770] added workflow_dispatch event so workflow can be triggered by an external repository such as MET to test to ensure that changes from that repo will break anything in METplus --- .github/jobs/set_job_controls.sh | 9 +++++++++ .github/workflows/testing.yml | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/.github/jobs/set_job_controls.sh b/.github/jobs/set_job_controls.sh index 0f175711d1..b8e05e27da 100755 --- a/.github/jobs/set_job_controls.sh +++ b/.github/jobs/set_job_controls.sh @@ -13,6 +13,7 @@ run_use_cases=true run_save_truth_data=false run_all_use_cases=false run_diff=false +external_trigger=false # run all use cases and diff logic for pull request if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then @@ -25,6 +26,12 @@ if [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then run_all_use_cases=true run_diff=true fi +# run all use cases and diff logic for external workflow trigger +elif [ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]; then + run_use_cases=true + run_all_use_cases=true + run_diff=true + external_trigger=true # run all use cases and save truth data if -ref branch and not PR elif [ "${GITHUB_REF: -4}" == -ref ]; then run_use_cases=true @@ -98,6 +105,7 @@ echo run_use_cases=${run_use_cases} >> job_control_status echo run_save_truth_data=${run_save_truth_data} >> job_control_status echo run_all_use_cases=${run_all_use_cases} >> job_control_status echo run_diff=${run_diff} >> job_control_status +echo external_trigger=${external_trigger} >> job_control_status echo Job Control Settings: cat job_control_status @@ -105,6 +113,7 @@ echo ::set-output name=run_get_image::$run_get_image echo ::set-output name=run_get_input_data::$run_get_input_data echo ::set-output name=run_diff::$run_diff echo ::set-output name=run_save_truth_data::$run_save_truth_data +echo ::set-output name=external_trigger::$external_trigger # get use cases to run .github/jobs/get_use_cases_to_run.sh $run_use_cases $run_all_use_cases $run_unit_tests diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 23b87420a8..5cc434d064 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -13,6 +13,13 @@ on: types: [opened, reopened, synchronize] paths-ignore: - docs/** + workflow_dispatch: + inputs: + repo_name: + description: 'Repository that triggered workflow' + required: true + docker_tag: + description: 'DockerHub tag to use (for MET)' jobs: job_control: @@ -25,6 +32,7 @@ jobs: run_get_input_data: ${{ steps.job_status.outputs.run_get_input_data }} run_diff: ${{ steps.job_status.outputs.run_diff }} run_save_truth_data: ${{ steps.job_status.outputs.run_save_truth_data }} + external_trigger: ${{ steps.job_status.outputs.external_trigger }} steps: - uses: actions/checkout@v2 - name: Print GitHub values for reference From 5673771634ad6a7f9b50ac0e2619326df27f803f Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 5 Jan 2022 13:39:51 -0700 Subject: [PATCH 247/770] added another input argument for workflow_dispatch event --- .github/workflows/testing.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 5cc434d064..4cb9d20dc7 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -20,6 +20,9 @@ on: required: true docker_tag: description: 'DockerHub tag to use (for MET)' + actor: + description: 'User that triggered the event' + required: true jobs: job_control: From b25d72881d8fe891a356ae2c34b9c40d5d26b83b Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 5 Jan 2022 15:04:23 -0700 Subject: [PATCH 248/770] added job with name that shows the event name or the repository name if triggered by an external repository such as MET --- .github/workflows/testing.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4cb9d20dc7..ece34146a6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -25,6 +25,15 @@ on: required: true jobs: + event_info: + name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repo_name }}" + runs-on: ubuntu-latest + steps: + - name: Print GitHub values for reference + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" + job_control: name: Determine which jobs to run runs-on: ubuntu-latest @@ -38,10 +47,6 @@ jobs: external_trigger: ${{ steps.job_status.outputs.external_trigger }} steps: - uses: actions/checkout@v2 - - name: Print GitHub values for reference - env: - GITHUB_CONTEXT: ${{ toJson(github) }} - run: echo "$GITHUB_CONTEXT" - name: Set job controls id: job_status run: .github/jobs/set_job_controls.sh From ab508d17bb34abd9d7808afa17621dfda64c1a26 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 5 Jan 2022 15:20:13 -0700 Subject: [PATCH 249/770] GHA: add username that triggered external event to event info job name --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index ece34146a6..3552028e43 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -26,7 +26,7 @@ on: jobs: event_info: - name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repo_name }}" + name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repo_name }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.actor }}" runs-on: ubuntu-latest steps: - name: Print GitHub values for reference From 35f20039c3aebce358da8c6006ea45ff2ebd5a35 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 5 Jan 2022 15:46:44 -0700 Subject: [PATCH 250/770] added required input argument for external trigger that contains the commit hash of the push event that triggered in the other repo --- .github/workflows/testing.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 3552028e43..b96f5d3ead 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -23,6 +23,9 @@ on: actor: description: 'User that triggered the event' required: true + commit: + description: 'Commit hash that triggered the event' + required: true jobs: event_info: From 1f9778521fb71ad07f9febc6f61cd8a8c41a4dc2 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 5 Jan 2022 16:45:40 -0700 Subject: [PATCH 251/770] change event info to show commit hash instead of username that merged the PR --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index b96f5d3ead..44e25ad2d8 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -29,7 +29,7 @@ on: jobs: event_info: - name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repo_name }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.actor }}" + name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repo_name }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.commit }}" runs-on: ubuntu-latest steps: - name: Print GitHub values for reference From e4c2f3bb11368182b4f79aee7ed0b1e1b710e102 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 5 Jan 2022 16:54:05 -0700 Subject: [PATCH 252/770] changed input names to match names of event in repository that triggered workflow --- .github/workflows/testing.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 44e25ad2d8..c6b7f7c5f9 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -15,21 +15,20 @@ on: - docs/** workflow_dispatch: inputs: - repo_name: + repository: description: 'Repository that triggered workflow' required: true - docker_tag: - description: 'DockerHub tag to use (for MET)' - actor: - description: 'User that triggered the event' - required: true - commit: + sha: description: 'Commit hash that triggered the event' required: true + ref: + description: 'Branch that triggered event' + actor: + description: 'User that triggered the event' jobs: event_info: - name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repo_name }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.commit }}" + name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repository }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.sha }}" runs-on: ubuntu-latest steps: - name: Print GitHub values for reference From e2a44bccb70a2af3c7029b887d8b82fcc1cdd7ee Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 11 Jan 2022 10:21:35 -0700 Subject: [PATCH 253/770] feature 1320 OMP_NUM_THREADS (#1338) --- docs/Users_Guide/glossary.rst | 10 ++++++ docs/Users_Guide/systemconfiguration.rst | 7 ++++ metplus/util/met_util.py | 43 +++++++++++++++++------- metplus/wrappers/command_builder.py | 6 ++++ parm/metplus_config/defaults.conf | 5 +++ 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 5183c899be..5b24d0b973 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8693,3 +8693,13 @@ METplus Configuration Glossary :term:`VALID_TIME_FMT` or they will be skipped. | *Used by:* All + + OMP_NUM_THREADS + Sets environment variable of the same name that determines the number + of threads to use in the MET executables. Defaults to 1 thread. + If the environment variable of the same name is already set in the + user's environment, then that value will be used instead of the value + set in the METplus configuration. A warning will be output if this is the + case and the values differ between them. + + | *Used by:* All diff --git a/docs/Users_Guide/systemconfiguration.rst b/docs/Users_Guide/systemconfiguration.rst index 1bcb65ad82..3f97b5be84 100644 --- a/docs/Users_Guide/systemconfiguration.rst +++ b/docs/Users_Guide/systemconfiguration.rst @@ -205,6 +205,13 @@ By default this is a directory called **stage** inside the This value is rarely changed, but it can be if desired. +OMP_NUM_THREADS +^^^^^^^^^^^^^^^ + +If the MET executables were installed with threading support, then the number +of threads used by the tools can be configured with this variable. See +the glossary entry for :term:`OMP_NUM_THREADS` for more information. + CONVERT ^^^^^^^ diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index aed1e225b1..0949726a9e 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -88,6 +88,11 @@ def pre_run_setup(config_inputs): # handle dir to write temporary files handle_tmp_dir(config) + # handle OMP_NUM_THREADS environment variable + handle_env_var_config(config, + env_var_name='OMP_NUM_THREADS', + config_name='OMP_NUM_THREADS') + config.env = os.environ.copy() return config @@ -230,19 +235,7 @@ def handle_tmp_dir(config): get config temp dir using getdir_nocheck to bypass check for /path/to this is done so the user can set env MET_TMP_DIR instead of config TMP_DIR and config TMP_DIR will be set automatically""" - met_tmp_dir = os.environ.get('MET_TMP_DIR', '') - conf_tmp_dir = config.getdir_nocheck('TMP_DIR', '') - - # if env MET_TMP_DIR is set - if met_tmp_dir: - # override config TMP_DIR to env MET_TMP_DIR value - config.set('config', 'TMP_DIR', met_tmp_dir) - - # if config TMP_DIR differed from env MET_TMP_DIR, warn - if conf_tmp_dir != met_tmp_dir: - msg = 'TMP_DIR in config will be overridden by the ' +\ - 'environment variable MET_TMP_DIR ({})'.format(met_tmp_dir) - config.logger.warning(msg) + handle_env_var_config(config, 'MET_TMP_DIR', 'TMP_DIR') # create temp dir if it doesn't exist already # this will fail if TMP_DIR is not set correctly and @@ -251,6 +244,30 @@ def handle_tmp_dir(config): if not os.path.exists(tmp_dir): os.makedirs(tmp_dir) +def handle_env_var_config(config, env_var_name, config_name): + """! If environment variable is set, use that value + for the config variable and warn if the previous config value differs + + @param config METplusConfig object to read + @param env_var_name name of environment variable to read + @param config_name name of METplus config variable to check + """ + env_var_value = os.environ.get(env_var_name, '') + config_value = config.getdir_nocheck(config_name, '') + + # do nothing if environment variable is not set + if not env_var_value: + return + + # override config config variable to environment variable value + config.set('config', config_name, env_var_value) + + # if config config value differed from environment variable value, warn + if config_value != env_var_value: + config.logger.warning(f'Config variable {config_name} ({config_value}) ' + 'will be overridden by the environment variable ' + f'{env_var_name} ({env_var_value})') + def get_skip_times(config, wrapper_name=None): """! Read SKIP_TIMES config variable and populate dictionary of times that should be skipped. SKIP_TIMES should be in the format: "%m:begin_end_incr(3,11,1)", "%d:30,31", "%Y%m%d:20201031" diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 9b4ea34922..237f9d79d6 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -70,6 +70,7 @@ def __init__(self, config, instance=None, config_overrides=None): # list of environment variables to set before running command self.env_var_keys = [ 'MET_TMP_DIR', + 'OMP_NUM_THREADS', ] if hasattr(self, 'WRAPPER_ENV_VAR_KEYS'): self.env_var_keys.extend(self.WRAPPER_ENV_VAR_KEYS) @@ -117,6 +118,11 @@ def __init__(self, config, instance=None, config_overrides=None): # where the MET tools write temporary files self.env_var_dict['MET_TMP_DIR'] = self.config.getdir('TMP_DIR') + # set OMP_NUM_THREADS environment variable + self.env_var_dict['OMP_NUM_THREADS'] = ( + self.config.getstr('config', 'OMP_NUM_THREADS') + ) + self.check_for_externals() self.cmdrunner = CommandRunner( diff --git a/parm/metplus_config/defaults.conf b/parm/metplus_config/defaults.conf index 5ae3cc9196..e4636ba47a 100644 --- a/parm/metplus_config/defaults.conf +++ b/parm/metplus_config/defaults.conf @@ -57,6 +57,10 @@ GFDL_TRACKER_EXEC = /path/to/standalone_gfdl-vortextracker_v3.9a/trk_exec ############################################################################### # Runtime Configuration # +# * OMP_NUM_THREADS sets an environment variable of the same name that # +# determines the number of threads to use in the MET executables. If the # +# environment variable is already set in the user's environment, then # +# that value will be used instead of the value set in this file. # ############################################################################### @@ -64,6 +68,7 @@ LOOP_ORDER = processes PROCESS_LIST = Usage +OMP_NUM_THREADS = 1 ############################################################################### # Log File Information (Where to write logs files) # From e65949be4cb06ed2ebbcc48cfe10cd74e82a91ef Mon Sep 17 00:00:00 2001 From: j-opatz <59586397+j-opatz@users.noreply.github.com> Date: Tue, 11 Jan 2022 11:57:03 -0700 Subject: [PATCH 254/770] Feature 1183 memory documentation (#1340) Co-authored-by: George McCabe <23407799+georgemccabe@users.noreply.github.com> --- docs/Contributors_Guide/add_use_case.rst | 66 +++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/Contributors_Guide/add_use_case.rst b/docs/Contributors_Guide/add_use_case.rst index a074fa32f2..eb4e07ad2c 100644 --- a/docs/Contributors_Guide/add_use_case.rst +++ b/docs/Contributors_Guide/add_use_case.rst @@ -144,8 +144,20 @@ Use Case Rules - The use case should be run by someone other than the author to ensure that it runs smoothly outside of the development environment set up by the author. -.. _use_case_documentation: +.. _memory-intense-use-cases: + +Use Cases That Exceed Github Actions Memory Limit +------------------------------------------------- + +Below is a list of use cases in the repository that cannot be run in Github Actions +due to their excessive memory usage. They have been tested and cleared by reviewers +of any other issues and can be used by METplus users in the same manner as all +other use cases. +- model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst + +.. _use_case_documentation: + Document New Use Case --------------------- @@ -1024,6 +1036,24 @@ with "Use Case Tests." Click on the job and search for the use case config filename in the log output by using the search box on the top right of the log output. +If the use case fails in GitHub Actions but runs successfully in the user's environment, +potential reasons include: + +- Errors providing input data (see :ref:`use_case_input_data`) +- Using hard-coded paths from the user's machine +- Referencing variables set in the user's configuration file or local environment +- Memory usuage of the use case exceeds the available memory in hte Github Actions environment + +Github Actions has `limited memory `_ +available and will cause the use case to fail when exceeded. A failure caused by exceeding +the memory allocation in a Python Embedding script may result in an unclear error message. +If you suspect that this is the case, consider utilizing a Python memory profiler to check the +Python script's memory usage. If your use case exceeds the limit, try to pare +down the data held in memory and use less memory intensive Python routines. + +If memory mitigation cannot move the use case’s memory usage below the Github Actions limit, +see :ref:`exceeded-Github-Actions` for next steps. + Verify that the use case ran in a reasonable amount of time ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1039,6 +1069,40 @@ run the set of use cases is now above 20 minutes or so, consider creating a new job for the new use case. See the :ref:`subset_category` section and the multiple medium_range jobs for an example. + +.. _exceeded-Github-Actions: + +Use Cases That Exceed Memory Allocations of Github Actions +---------------------------------------------------------- + +If a use case utilizing Python embedding does not run successfully in +Github Actions due to exceeding the memory limit and memory mitigation +steps were unsuccessful in lowering memory usage, please take the following steps. + +- Document the Github Actions failure in the Github use case issue. + Utilize a Python memory profiler to identify as specifically as possible + where the script exceeds the memory limit. +- Add the use case to the :ref:`memory-intense-use-cases` list. +- In the internal_tests/use_cases/all_use_cases.txt file, ensure that the + use case is listed as the lowest-listed use case in its respective category. + Change the number in front of the new use case to an 'X', preceeded + by the ‘#’ character:: + + #X::GridStat_fcstRTOFS_obsGHRSST_climWOA_sst::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst.conf, model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst/ci_overrides.conf:: icecover_env, py_embed + +- In the **.github/parm/use_case_groups.json** file, remove the entry that + was added during the :ref:`add_new_category_to_test_runs` + for the new use case. This will stop the use case from running on a pull request. +- Push these two updated files to your branch in Github and confirm that it + now compiles successfully. +- During the :ref:`create-a-pull-request` creation, inform the reviewer of + the Github Actions failure. The reviewer should confirm the use case is + successful when run manually, that the memory profiler output confirms that + the Python embedding script exceeds the Github Actions limit, and that + there are no other Github Actions compiling errors. + +.. _create-a-pull-request: + Create a Pull Request ===================== From 8f8b9f94a3e9ee73a54b18770072e9880a76662c Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 12 Jan 2022 11:55:40 -0700 Subject: [PATCH 255/770] add email address of user who triggered push event to job name --- .github/workflows/testing.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c6b7f7c5f9..79c0b51e32 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -25,10 +25,12 @@ on: description: 'Branch that triggered event' actor: description: 'User that triggered the event' + pusher_email: + description: 'Email address of user who triggered push event' jobs: event_info: - name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repository }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.sha }}" + name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repository }} ${{ github.event_name != 'workflow_dispatch' && 'local' || github.event.inputs.pusher_email }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.sha }}" runs-on: ubuntu-latest steps: - name: Print GitHub values for reference From a13835586e83a254b04c2026c1e7a4c5db1f7166 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 13 Jan 2022 10:24:40 -0700 Subject: [PATCH 256/770] Feature 1166 series analysis field info (#1353) --- docs/Users_Guide/glossary.rst | 20 ++ docs/Users_Guide/wrappers.rst | 30 +++ .../series_analysis/test_series_analysis.py | 27 ++- metplus/util/string_template_substitution.py | 4 +- metplus/wrappers/command_builder.py | 6 +- metplus/wrappers/series_analysis_wrapper.py | 193 ++++++++++++------ parm/met_config/SeriesAnalysisConfig_wrapped | 9 + .../SeriesAnalysis/SeriesAnalysis.conf | 5 + 8 files changed, 213 insertions(+), 81 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 5b24d0b973..f3e3f1ec54 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8694,6 +8694,26 @@ METplus Configuration Glossary | *Used by:* All + FCST_SERIES_ANALYSIS_CAT_THRESH + Specify the value for 'fcst.cat_thresh' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + OBS_SERIES_ANALYSIS_CAT_THRESH + Specify the value for 'obs.cat_thresh' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_CLIMO_MEAN_FILE_TYPE + Specify the value for 'climo_mean.file_type' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_CLIMO_STDEV_FILE_TYPE + Specify the value for 'climo_stdev.file_type' in the MET configuration file for SeriesAnalysis. + + | *Used by:* SeriesAnalysis + OMP_NUM_THREADS Sets environment variable of the same name that determines the number of threads to use in the MET executables. Defaults to 1 thread. diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 9cd7af499e..e06bee76fa 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -5871,6 +5871,7 @@ METplus Configuration | :term:`SERIES_ANALYSIS_CLIMO_MEAN_MATCH_MONTH` | :term:`SERIES_ANALYSIS_CLIMO_MEAN_DAY_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_MEAN_HOUR_INTERVAL` +| :term:`SERIES_ANALYSIS_CLIMO_MEAN_FILE_TYPE` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_FILE_NAME` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_FIELD` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_REGRID_METHOD` @@ -5881,6 +5882,7 @@ METplus Configuration | :term:`SERIES_ANALYSIS_CLIMO_STDEV_MATCH_MONTH` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_DAY_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_HOUR_INTERVAL` +| :term:`SERIES_ANALYSIS_CLIMO_STDEV_FILE_TYPE` | :term:`SERIES_ANALYSIS_HSS_EC_VALUE` | :term:`SERIES_ANALYSIS_OUTPUT_STATS_FHO` | :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTC` @@ -5894,6 +5896,8 @@ METplus Configuration | :term:`SERIES_ANALYSIS_OUTPUT_STATS_PSTD` | :term:`SERIES_ANALYSIS_OUTPUT_STATS_PJC` | :term:`SERIES_ANALYSIS_OUTPUT_STATS_PRC` +| :term:`FCST_SERIES_ANALYSIS_CAT_THRESH` +| :term:`OBS_SERIES_ANALYSIS_CAT_THRESH` | .. warning:: **DEPRECATED:** @@ -6076,6 +6080,8 @@ see :ref:`How METplus controls MET config file settings`. - climo_mean.day_interval * - :term:`SERIES_ANALYSIS_CLIMO_MEAN_HOUR_INTERVAL` - climo_mean.hour_interval + * - :term:`SERIES_ANALYSIS_CLIMO_MEAN_FILE_TYPE` + - climo_mean.file_type **${METPLUS_CLIMO_STDEV_DICT}** @@ -6105,6 +6111,8 @@ see :ref:`How METplus controls MET config file settings`. - climo_stdev.day_interval * - :term:`SERIES_ANALYSIS_CLIMO_STDEV_HOUR_INTERVAL` - climo_stdev.hour_interval + * - :term:`SERIES_ANALYSIS_CLIMO_STDEV_FILE_TYPE` + - climo_stdev.file_type **${METPLUS_BLOCK_SIZE}** @@ -6184,6 +6192,28 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`SERIES_ANALYSIS_OUTPUT_STATS_PRC` - output_stats.prc +**${METPLUS_FCST_CAT_THRESH}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`FCST_SERIES_ANALYSIS_CAT_THRESH` + - fcst.cat_thresh + +**${METPLUS_OBS_CAT_THRESH}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`OBS_SERIES_ANALYSIS_CAT_THRESH` + - obs.cat_thresh + SeriesByInit ============ diff --git a/internal_tests/pytests/series_analysis/test_series_analysis.py b/internal_tests/pytests/series_analysis/test_series_analysis.py index 5d21eb0b39..f8ec19e35f 100644 --- a/internal_tests/pytests/series_analysis/test_series_analysis.py +++ b/internal_tests/pytests/series_analysis/test_series_analysis.py @@ -270,6 +270,11 @@ def set_minimum_config_settings(config): 'pstd = ["RMSE10", "FBAR", "OBAR"];' 'pjc = ["RMSE11", "FBAR", "OBAR"];' 'prc = ["RMSE12", "FBAR", "OBAR"];}')}), + ({'SERIES_ANALYSIS_FCST_CAT_THRESH': '>=0.0, >=0.3, >=1.0', }, + {'METPLUS_FCST_CAT_THRESH': 'cat_thresh = [>=0.0, >=0.3, >=1.0];'}), + + ({'SERIES_ANALYSIS_OBS_CAT_THRESH': '<=CDP33', }, + {'METPLUS_OBS_CAT_THRESH': 'cat_thresh = [<=CDP33];'}), ] ) @@ -738,29 +743,23 @@ def test_create_ascii_storm_files_list(metplus_config, config_overrides, leads = lead_group[1] else: leads = None - fcst_list_file = wrapper.get_ascii_filename('FCST', storm_id, leads) + fcst_list_file = wrapper._get_ascii_filename('FCST', storm_id, leads) fcst_file_path = os.path.join(output_dir, output_prefix, fcst_list_file) if os.path.exists(fcst_file_path): os.remove(fcst_file_path) - obs_list_file = wrapper.get_ascii_filename('OBS', storm_id, leads) + obs_list_file = wrapper._get_ascii_filename('OBS', storm_id, leads) obs_file_path = os.path.join(output_dir, output_prefix, obs_list_file) if os.path.exists(obs_file_path): os.remove(obs_file_path) - # perform string substitution on var list - wrapper.c_dict['VAR_LIST'] = ( - sub_var_list(wrapper.c_dict['VAR_LIST_TEMP'], - time_info) - ) - - fcst_path, obs_path = wrapper.create_ascii_storm_files_list(time_info, - storm_id, - lead_group) + fcst_path, obs_path = wrapper._create_ascii_storm_files_list(time_info, + storm_id, + lead_group) assert(fcst_path == fcst_file_path and obs_path == obs_file_path) with open(fcst_file_path, 'r') as file_handle: @@ -813,7 +812,7 @@ def test_get_ascii_filename(metplus_config, storm_id, leads, expected_result): wrapper = series_analysis_wrapper(metplus_config) for data_type in ['FCST', 'OBS']: - actual_result = wrapper.get_ascii_filename(data_type, + actual_result = wrapper._get_ascii_filename(data_type, storm_id, leads) assert(actual_result == f"{data_type}{expected_result}") @@ -822,7 +821,7 @@ def test_get_ascii_filename(metplus_config, storm_id, leads, return lead_seconds = [ti_get_seconds_from_lead(item) for item in leads] - actual_result = wrapper.get_ascii_filename(data_type, + actual_result = wrapper._get_ascii_filename(data_type, storm_id, lead_seconds) assert(actual_result == f"{data_type}{expected_result}") @@ -865,7 +864,7 @@ def test_get_netcdf_min_max(metplus_config): 'tc_data', 'basin_global_tenth_degree.nc') variable_name = 'basin' - min, max = wrapper.get_netcdf_min_max(filepath, variable_name) + min, max = wrapper._get_netcdf_min_max(filepath, variable_name) assert(min == expected_min) assert(max == expected_max) diff --git a/metplus/util/string_template_substitution.py b/metplus/util/string_template_substitution.py index e4085ebc50..7aeb709942 100644 --- a/metplus/util/string_template_substitution.py +++ b/metplus/util/string_template_substitution.py @@ -773,8 +773,8 @@ def add_date_matches_to_output_dict(match_dict, output_dict, time_type, valid_sh time_values = { 'Y': -1, 'y': -1, - 'm': -1, - 'd': -1, + 'm': 1, + 'd': 1, 'j': -1, 'H': 0, 'M': 0, diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 237f9d79d6..4eccbb92d3 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -993,12 +993,15 @@ def find_and_check_output_file(self, time_info=None, output_path = do_string_sub(output_path, **time_info) + # replace wildcard character * with all + output_path.replace('*', 'all') + skip_if_output_exists = self.c_dict.get('SKIP_IF_OUTPUT_EXISTS', False) # get directory that the output file will exist if is_directory: parent_dir = output_path - if time_info: + if time_info and time_info['valid'] != '*': valid_format = time_info['valid'].strftime('%Y%m%d_%H%M%S') else: valid_format = '' @@ -1523,6 +1526,7 @@ def handle_climo_dict(self): 'match_month': ('bool', 'uppercase'), 'day_interval': 'int', 'hour_interval': 'int', + 'file_type': ('string', 'remove_quotes'), } for climo_type in self.climo_types: dict_name = f'climo_{climo_type.lower()}' diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 8fdd625079..578dad2736 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -24,7 +24,7 @@ from ..util import getlist from ..util import met_util as util -from ..util import do_string_sub, parse_template +from ..util import do_string_sub, parse_template, get_tags from ..util import get_lead_sequence, get_lead_sequence_groups from ..util import ti_get_hours_from_lead, ti_get_seconds_from_lead from ..util import ti_get_lead_string @@ -53,6 +53,8 @@ class SeriesAnalysisWrapper(RuntimeFreqWrapper): 'METPLUS_VLD_THRESH', 'METPLUS_OUTPUT_STATS_DICT', 'METPLUS_HSS_EC_VALUE', + 'METPLUS_FCST_CAT_THRESH', + 'METPLUS_OBS_CAT_THRESH', ] # handle deprecated env vars used pre v4.0.0 @@ -89,7 +91,7 @@ def __init__(self, config, instance=None, config_overrides=None): config_overrides=config_overrides) if self.c_dict['GENERATE_PLOTS']: - self.plot_data_plane = self.plot_data_plane_init() + self.plot_data_plane = self._plot_data_plane_init() if WRAPPER_CANNOT_RUN: self.log_error("There was a problem importing modules: " @@ -159,10 +161,6 @@ def create_c_dict(self): output_stats_dict[key] = value self.add_met_config_dict('output_stats', output_stats_dict) - if not c_dict['STAT_LIST']: - self.log_error("Must set SERIES_ANALYSIS_STAT_LIST to run.") - - # set legacy stat list to set output_stats.cnt in MET config file self.add_met_config(name='cnt', data_type='list', @@ -201,6 +199,42 @@ def create_c_dict(self): # initialize list path to None for each type c_dict[f'{data_type}_LIST_PATH'] = None + # read and set file type env var for FCST and OBS + if data_type == 'BOTH': + continue + + self.add_met_config( + name='file_type', + data_type='string', + env_var_name=f'{data_type}_FILE_TYPE', + metplus_configs=[f'{data_type}_SERIES_ANALYSIS_FILE_TYPE', + f'SERIES_ANALYSIS_{data_type}_FILE_TYPE', + f'{data_type}_FILE_TYPE', + f'{data_type}_SERIES_ANALYSIS_INPUT_DATATYPE', + 'SERIES_ANALYSIS_FILE_TYPE'], + extra_args={'remove_quotes': True, + 'uppercase': True}) + + self.add_met_config( + name='cat_thresh', + data_type='list', + env_var_name=f'METPLUS_{data_type}_CAT_THRESH', + metplus_configs=[f'{data_type}_SERIES_ANALYSIS_CAT_THRESH', + f'SERIES_ANALYSIS_{data_type}_CAT_THRESH', + f'{data_type}_CAT_THRESH'], + extra_args={'remove_quotes': True} + ) + + c_dict[f'{data_type}_IS_PROB'] = ( + self.config.getbool('config', f'{data_type}_IS_PROB', False) + ) + if c_dict[f'{data_type}_IS_PROB']: + c_dict[f'{data_type}_PROB_IN_GRIB_PDS'] = ( + self.config.getbool('config', + f'{data_type}_PROB_IN_GRIB_PDS', + False) + ) + # if BOTH is set, neither FCST or OBS can be set c_dict['USING_BOTH'] = False if c_dict['BOTH_INPUT_TEMPLATE']: @@ -269,9 +303,9 @@ def create_c_dict(self): False) ) - c_dict['VAR_LIST_TEMP'] = parse_var_list(self.config, - met_tool=self.app_name) - if not c_dict['VAR_LIST_TEMP']: + c_dict['VAR_LIST'] = parse_var_list(self.config, + met_tool=self.app_name) + if not c_dict['VAR_LIST']: self.log_error("No fields specified. Please set " "[FCST/OBS]_VAR_[NAME/LEVELS]") @@ -320,7 +354,7 @@ def create_c_dict(self): return c_dict - def plot_data_plane_init(self): + def _plot_data_plane_init(self): """! Set values to allow successful initialization of PlotDataPlane wrapper @@ -421,20 +455,14 @@ def run_at_time_once(self, time_info, lead_group=None): if not storm_list: return False - # perform string substitution on var list - self.c_dict['VAR_LIST'] = ( - util.sub_var_list(self.c_dict['VAR_LIST_TEMP'], - time_info) - ) - # loop over storm list and process for each # this loop will execute once if not filtering by storm ID for storm_id in storm_list: # Create FCST and OBS ASCII files fcst_path, obs_path = ( - self.create_ascii_storm_files_list(time_info, - storm_id, - lead_group) + self._create_ascii_storm_files_list(time_info, + storm_id, + lead_group) ) if not fcst_path or not obs_path: self.log_error('No ASCII file lists were created. Skipping.') @@ -447,9 +475,9 @@ def run_at_time_once(self, time_info, lead_group=None): continue if self.c_dict['GENERATE_PLOTS']: - self.generate_plots(fcst_path, - time_info, - storm_id) + self._generate_plots(fcst_path, + time_info, + storm_id) else: self.logger.debug("Skip plotting output. Change " "SERIES_ANALYSIS_GENERATE_PLOTS to True to " @@ -579,7 +607,7 @@ def compare_time_info(self, runtime, filetime): return bool(filetime['storm_id'] == runtime['storm_id']) - def create_ascii_storm_files_list(self, time_info, storm_id, lead_group): + def _create_ascii_storm_files_list(self, time_info, storm_id, lead_group): """! Creates the list of ASCII files that contain the storm id and init times. The list is used to create an ASCII file which will be used as the option to the -obs or -fcst flag to the MET @@ -619,7 +647,7 @@ def create_ascii_storm_files_list(self, time_info, storm_id, lead_group): output_dir = self.get_output_dir(time_info, storm_id, label) - if not self.check_python_embedding(): + if not self._check_python_embedding(): return None, None # create forecast (or both) file list @@ -627,9 +655,9 @@ def create_ascii_storm_files_list(self, time_info, storm_id, lead_group): data_type = 'BOTH' else: data_type = 'FCST' - fcst_ascii_filename = self.get_ascii_filename(data_type, - storm_id, - leads) + fcst_ascii_filename = self._get_ascii_filename(data_type, + storm_id, + leads) self.write_list_file(fcst_ascii_filename, all_fcst_files, output_dir=output_dir) @@ -640,7 +668,7 @@ def create_ascii_storm_files_list(self, time_info, storm_id, lead_group): return fcst_path, fcst_path # create analysis file list - obs_ascii_filename = self.get_ascii_filename('OBS', + obs_ascii_filename = self._get_ascii_filename('OBS', storm_id, leads) self.write_list_file(obs_ascii_filename, @@ -651,7 +679,7 @@ def create_ascii_storm_files_list(self, time_info, storm_id, lead_group): return fcst_path, obs_path - def check_python_embedding(self): + def _check_python_embedding(self): """! Check if any of the field names contain a Python embedding script. See CommandBuilder.check_for_python_embedding for more info. @@ -670,7 +698,7 @@ def check_python_embedding(self): return True @staticmethod - def get_ascii_filename(data_type, storm_id, leads=None): + def _get_ascii_filename(data_type, storm_id, leads=None): """! Build filename for ASCII file list file @param data_type FCST, OBS, or BOTH @@ -769,10 +797,13 @@ def build_and_run_series_request(self, time_info, fcst_path, obs_path): else: self.c_dict['FCST_LIST_PATH'] = fcst_path self.c_dict['OBS_LIST_PATH'] = obs_path + self.add_field_info_to_time_info(time_info, var_info) # get formatted field dictionary to pass into the MET config file - fcst_field, obs_field = self.get_formatted_fields(var_info) + fcst_field, obs_field = self.get_formatted_fields(var_info, + fcst_path, + obs_path) self.format_field('FCST', fcst_field) self.format_field('OBS', obs_field) @@ -800,9 +831,7 @@ def set_environment_variables(self, time_info): """ self.logger.info('Setting env variables from config file...') - # Set all the environment variables that are needed by the - # MET config file. - # Set up the environment variable to be used in the Series Analysis + # Set all the environment variables referenced in the MET config file self.add_env_var("FCST_FILE_TYPE", self.c_dict.get('FCST_FILE_TYPE', '')) self.add_env_var("OBS_FILE_TYPE", self.c_dict.get('OBS_FILE_TYPE', @@ -868,9 +897,10 @@ def get_command(self): cmd += ' -v ' + self.c_dict['VERBOSITY'] return cmd - def generate_plots(self, fcst_path, time_info, storm_id): + def _generate_plots(self, fcst_path, time_info, storm_id): """! Generate the plots from the series_analysis output. + @param fcst_path path to forecast file list file @param time_info dictionary containing time information @param storm_id storm ID to process """ @@ -903,8 +933,8 @@ def generate_plots(self, fcst_path, time_info, storm_id): self.logger.debug(f"Skipping plot for {storm_id}") continue - _, nseries = self.get_netcdf_min_max(plot_input, - 'series_cnt_TOTAL') + _, nseries = self._get_netcdf_min_max(plot_input, + 'series_cnt_TOTAL') nseries_str = '' if nseries is None else f" (N = {nseries})" time_info['nseries'] = nseries_str @@ -915,8 +945,8 @@ def generate_plots(self, fcst_path, time_info, storm_id): self.c_dict['PNG_FILES'][key] = [] min_value, max_value = ( - self.get_netcdf_min_max(plot_input, - f'series_cnt_{cur_stat}') + self._get_netcdf_min_max(plot_input, + f'series_cnt_{cur_stat}') ) range_min_max = f"{min_value} {max_value}" @@ -994,14 +1024,9 @@ def get_fcst_file_info(self, fcst_path): files_of_interest = files_of_interest[1:] num = str(len(files_of_interest)) - if self.c_dict['USING_BOTH']: - input_dir = self.c_dict['BOTH_INPUT_DIR'] - input_template = self.c_dict['BOTH_INPUT_TEMPLATE'] - else: - input_dir = self.c_dict['FCST_INPUT_DIR'] - input_template = self.c_dict['FCST_INPUT_TEMPLATE'] - - full_template = os.path.join(input_dir, input_template) + data_type = 'BOTH' if self.c_dict['USING_BOTH'] else 'FCST' + template = os.path.join(self.c_dict[f'{data_type}_INPUT_DIR'], + self.c_dict[f'{data_type}_INPUT_TEMPLATE']) smallest_fcst = 99999999 largest_fcst = -99999999 @@ -1009,7 +1034,7 @@ def get_fcst_file_info(self, fcst_path): end = None for filepath in files_of_interest: filepath = filepath.strip() - file_time_info = parse_template(full_template, + file_time_info = parse_template(template, filepath, self.logger) if not file_time_info: @@ -1029,7 +1054,7 @@ def get_fcst_file_info(self, fcst_path): return num, beg, end @staticmethod - def get_netcdf_min_max(filepath, variable_name): + def _get_netcdf_min_max(filepath, variable_name): """! Determine the min and max for all lead times for each statistic and variable pairing. @@ -1046,7 +1071,7 @@ def get_netcdf_min_max(filepath, variable_name): except (FileNotFoundError, KeyError): return None, None - def get_formatted_fields(self, var_info): + def get_formatted_fields(self, var_info, fcst_path, obs_path): """! Get forecast and observation field information for var_info and format it so it can be passed into the MET config file @@ -1054,23 +1079,63 @@ def get_formatted_fields(self, var_info): @returns tuple containing strings of the formatted forecast and observation information or None, None if something went wrong """ - # get field info field a single field to pass to the MET config file - fcst_field_list = self.get_field_info(v_level=var_info['fcst_level'], - v_thresh=var_info['fcst_thresh'], - v_name=var_info['fcst_name'], - v_extra=var_info['fcst_extra'], - d_type='FCST') - - obs_field_list = self.get_field_info(v_level=var_info['obs_level'], - v_thresh=var_info['obs_thresh'], - v_name=var_info['obs_name'], - v_extra=var_info['obs_extra'], - d_type='OBS') - - if fcst_field_list is None or obs_field_list is None: + fcst_field_list = self._get_field_list('fcst', var_info, obs_path) + obs_field_list = self._get_field_list('obs', var_info, fcst_path) + + if not fcst_field_list or not obs_field_list: return None, None fcst_fields = ','.join(fcst_field_list) obs_fields = ','.join(obs_field_list) return fcst_fields, obs_fields + + def _get_field_list(self, data_type, var_info, file_list_path): + other = 'OBS' if data_type == 'fcst' else 'FCST' + # check if time filename template tags are used in field level + if not self._has_time_tag(var_info[f'{data_type}_level']): + # get field info for a single field to pass to the MET config file + return self.get_field_info( + v_level=var_info[f'{data_type}_level'], + v_thresh=var_info[f'{data_type}_thresh'], + v_name=var_info[f'{data_type}_name'], + v_extra=var_info[f'{data_type}_extra'], + d_type=data_type.upper() + ) + + field_list = [] + # loop through fcst and obs files to extract time info + template = os.path.join(self.c_dict[f'{other}_INPUT_DIR'], + self.c_dict[f'{other}_INPUT_TEMPLATE']) + # for each file apply time info to field info and add to list + for file_time_info in self._get_times_from_file_list(file_list_path, + template): + level = do_string_sub(var_info[f'{data_type}_level'], + **file_time_info) + field = self.get_field_info( + v_level=level, + v_thresh=var_info[f'{data_type}_thresh'], + v_name=var_info[f'{data_type}_name'], + v_extra=var_info[f'{data_type}_extra'], + d_type=data_type.upper() + ) + if field: + field_list.extend(field) + + return field_list + + @staticmethod + def _has_time_tag(level): + return any(item in ['init', 'valid', 'lead'] + for item in get_tags(level)) + + @staticmethod + def _get_times_from_file_list(file_path, template): + with open(file_path, 'r') as file_handle: + file_list = file_handle.read().splitlines()[1:] + + for file_name in file_list: + file_time_info = parse_template(template, file_name) + if not file_time_info: + continue + yield file_time_info diff --git a/parm/met_config/SeriesAnalysisConfig_wrapped b/parm/met_config/SeriesAnalysisConfig_wrapped index a8cf1af226..864153e9eb 100644 --- a/parm/met_config/SeriesAnalysisConfig_wrapped +++ b/parm/met_config/SeriesAnalysisConfig_wrapped @@ -9,16 +9,19 @@ // // Output model name to be written // +//model = ${METPLUS_MODEL} // // Output description to be written // +//desc = ${METPLUS_DESC} // // Output observation type to be written // +//obtype = ${METPLUS_OBTYPE} //////////////////////////////////////////////////////////////////////////////// @@ -27,12 +30,14 @@ ${METPLUS_OBTYPE} // Verification grid // May be set separately in each "field" entry // +//regrid = { ${METPLUS_REGRID_DICT} //////////////////////////////////////////////////////////////////////////////// censor_thresh = []; censor_val = []; +//cat_thresh = ${METPLUS_CAT_THRESH} cnt_thresh = [ NA ]; cnt_logic = UNION; @@ -42,10 +47,12 @@ cnt_logic = UNION; // fcst = { ${METPLUS_FCST_FILE_TYPE} + ${METPLUS_FCST_CAT_THRESH} ${METPLUS_FCST_FIELD} } obs = { ${METPLUS_OBS_FILE_TYPE} + ${METPLUS_OBS_CAT_THRESH} ${METPLUS_OBS_FIELD} } @@ -90,11 +97,13 @@ mask = { // Number of grid points to be processed concurrently. Set smaller to use // less memory but increase the number of passes through the data. // +//block_size = ${METPLUS_BLOCK_SIZE} // // Ratio of valid matched pairs to compute statistics for a grid point // +//vld_thresh = ${METPLUS_VLD_THRESH} //////////////////////////////////////////////////////////////////////////////// diff --git a/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf b/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf index 230c081f1d..3c8f768fcb 100644 --- a/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf +++ b/parm/use_cases/met_tool_wrapper/SeriesAnalysis/SeriesAnalysis.conf @@ -50,6 +50,9 @@ SERIES_ANALYSIS_CLIMO_STDEV_INPUT_TEMPLATE = MODEL = WRF OBTYPE = MC_PCP +#FCST_CAT_THRESH = +#OBS_CAT_THRESH = + FCST_VAR1_NAME = APCP FCST_VAR1_LEVELS = A03 @@ -92,6 +95,7 @@ SERIES_ANALYSIS_IS_PAIRED = False #SERIES_ANALYSIS_CLIMO_MEAN_MATCH_MONTH = #SERIES_ANALYSIS_CLIMO_MEAN_DAY_INTERVAL = #SERIES_ANALYSIS_CLIMO_MEAN_HOUR_INTERVAL = +#SERIES_ANALYSIS_CLIMO_MEAN_FILE_TYPE = #SERIES_ANALYSIS_CLIMO_STDEV_FILE_NAME = #SERIES_ANALYSIS_CLIMO_STDEV_FIELD = @@ -103,6 +107,7 @@ SERIES_ANALYSIS_IS_PAIRED = False #SERIES_ANALYSIS_CLIMO_STDEV_MATCH_MONTH = #SERIES_ANALYSIS_CLIMO_STDEV_DAY_INTERVAL = #SERIES_ANALYSIS_CLIMO_STDEV_HOUR_INTERVAL = +#SERIES_ANALYSIS_CLIMO_STDEV_FILE_TYPE = #SERIES_ANALYSIS_HSS_EC_VALUE = From c77d5d508152b301ea6dac81bcc034a12defbd8d Mon Sep 17 00:00:00 2001 From: j-opatz <59586397+j-opatz@users.noreply.github.com> Date: Thu, 13 Jan 2022 10:25:43 -0700 Subject: [PATCH 257/770] Feature 1116 usecase smos (#1348) Co-authored-by: Mrinal Biswas --- .github/parm/use_case_groups.json | 5 + ...GridStat_fcstRTOFS_obsSMOS_climWOA_sss.png | Bin 0 -> 189578 bytes .../GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py | 170 +++++++++ internal_tests/use_cases/all_use_cases.txt | 1 + ...ridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf | 267 ++++++++++++++ .../read_rtofs_smos_woa.py | 346 ++++++++++++++++++ 6 files changed, 789 insertions(+) create mode 100644 docs/_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMOS_climWOA_sss.png create mode 100644 docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py create mode 100644 parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf create mode 100644 parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/read_rtofs_smos_woa.py diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 6c5f13f407..9fc35f1847 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -59,6 +59,11 @@ "index_list": "0-2", "run": false }, + { + "category": "marine_and_cryosphere", + "index_list": "3", + "run": true + }, { "category": "medium_range", "index_list": "0", diff --git a/docs/_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMOS_climWOA_sss.png b/docs/_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMOS_climWOA_sss.png new file mode 100644 index 0000000000000000000000000000000000000000..59435cf80338f0a313820a0c422e7102aa694fec GIT binary patch literal 189578 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

=}!O&bf;16A9bKNpr&QORhx^EqL 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%0D}43Q(&r3YMup-f(2x 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 -r1ojDmW_bS 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 for more details. +# +# **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** +# +# If there is a setting in the MET configuration file that is currently not supported by METplus you'd like to control, please refer to: +# :ref:`Overriding Unsupported MET config file settings` +# +# .. note:: See the :ref:`GridStat MET Configuration` section of the User's Guide for more information on the environment variables used in the file below: +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/met_config/GridStatConfig_wrapped + +############################################################################## +# Python Embedding +# ---------------- +# +# This use case uses one Python script to read forecast and observation data +# +# parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/read_rtofs_smos_woa.py +# +# .. highlight:: python +# .. literalinclude:: ../../../../parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/read_rtofs_smos_woa.py +# + +############################################################################## +# Running METplus +# --------------- +# +# This use case can be run two ways: +# +# 1) Passing in GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf then a user-specific system configuration file:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf -c /path/to/user_system.conf +# +# 2) Modifying the configurations in parm/metplus_config, then passing in GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf +# +# The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: +# +# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases +# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions +# * **MET_INSTALL_DIR** - Path to location where MET is installed locally +# +# Example User Configuration File:: +# +# [dir] +# INPUT_BASE = /path/to/sample/input/data +# OUTPUT_BASE = /path/to/output/dir +# MET_INSTALL_DIR = /path/to/met-X.Y +# +# **NOTE:** All of these items must be found under the [dir] section. +# + +############################################################################## +# Expected Output +# --------------- +# +# A successful run will output the following both to the screen and to the logfile:: +# +# INFO: METplus has successfully finished running. +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. +# Output for thisIce use case will be found in 20210503 (relative to **OUTPUT_BASE**) +# and will contain the following files: +# +# * grid_stat_SSS_000000L_20210503_000000V.stat +# * grid_stat_SSS_000000L_20210503_000000V_cnt.txt +# * grid_stat_SSS_000000L_20210503_000000V_pairs.nc + +############################################################################## +# Keywords +# -------- +# +# .. note:: +# +# * GridStatToolUseCase +# * PythonEmbeddingFileUseCase +# * MarineAndCryosphereAppUseCase +# +# Navigate to the :ref:`quick-search` page to discover other similar use cases. +# +# +# +# sphinx_gallery_thumbnail_path = '_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMOS_climWOA_sss.png' + diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index b6cd0a3af5..2582f198e9 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -89,6 +89,7 @@ Category: marine_and_cryosphere 0::GridStat_MODE_fcstIMS_obsNCEP_sea_ice::model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.conf 1::PlotDataPlane_obsHYCOM_coordTripolar::model_applications/marine_and_cryosphere/PlotDataPlane_obsHYCOM_coordTripolar.conf:: xesmf_env, py_embed 2::GridStat_fcstRTOFS_obsOSTIA_iceCover::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsOSTIA_iceCover.conf:: icecover_env, py_embed +3::GridStat_fcstRTOFS_obsSMOS_climWOA_sss::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf:: icecover_env, py_embed #X::GridStat_fcstRTOFS_obsGHRSST_climWOA_sst::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst.conf, model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst/ci_overrides.conf:: icecover_env, py_embed diff --git a/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf new file mode 100644 index 0000000000..72e97f663e --- /dev/null +++ b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf @@ -0,0 +1,267 @@ +# GridStat METplus Configuration + +# section heading for [config] variables - all items below this line and +# before the next section heading correspond to the [config] section +[config] + +# List of applications to run - only GridStat for this case +PROCESS_LIST = GridStat + +# time looping - options are INIT, VALID, RETRO, and REALTIME +# If set to INIT or RETRO: +# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set +# If set to VALID or REALTIME: +# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set +LOOP_BY = VALID + +# Format of INIT_BEG and INT_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d + +# Start time for METplus run - must match INIT_TIME_FMT +VALID_BEG=20210503 + +# End time for METplus run - must match INIT_TIME_FMT +VALID_END=20210503 + +# Increment between METplus runs (in seconds if no units are specified) +# Must be >= 60 seconds +VALID_INCREMENT = 1M + +# List of forecast leads to process for each run time (init or valid) +# In hours if units are not specified +# If unset, defaults to 0 (don't loop through forecast leads) +LEAD_SEQ = 0 + + +# Order of loops to process data - Options are times, processes +# Not relevant if only one item is in the PROCESS_LIST +# times = run all wrappers in the PROCESS_LIST for a single run time, then +# increment the run time and run all wrappers again until all times have +# been evaluated. +# processes = run the first wrapper in the PROCESS_LIST for all times +# specified, then repeat for the next item in the PROCESS_LIST until all +# wrappers have been run +LOOP_ORDER = times + +# Verbosity of MET output - overrides LOG_VERBOSITY for GridStat only +LOG_GRID_STAT_VERBOSITY = 2 + +# Location of MET config file to pass to GridStat +GRID_STAT_CONFIG_FILE = {PARM_BASE}/met_config/GridStatConfig_wrapped + +# grid to remap data. Value is set as the 'to_grid' variable in the 'regrid' dictionary +# See MET User's Guide for more information +GRID_STAT_REGRID_TO_GRID = NONE + +#GRID_STAT_INTERP_FIELD = +#GRID_STAT_INTERP_VLD_THRESH = +#GRID_STAT_INTERP_SHAPE = +#GRID_STAT_INTERP_TYPE_METHOD = +#GRID_STAT_INTERP_TYPE_WIDTH = + +#GRID_STAT_NC_PAIRS_VAR_NAME = + +#GRID_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = + +#GRID_STAT_GRID_WEIGHT_FLAG = AREA + +# Name to identify model (forecast) data in output +MODEL = RTOFS + +# Name to identify observation data in output +OBTYPE = SMOS + +# set the desc value in the GridStat MET config file +GRID_STAT_DESC = NA + +# List of variables to compare in GridStat - FCST_VAR1 variables correspond +# to OBS_VAR1 variables +# Note [FCST/OBS/BOTH]_GRID_STAT_VAR_NAME can be used instead if different evaluations +# are needed for different tools + +# Name of forecast variable 1 +FCST_VAR1_NAME = {CONFIG_DIR}/read_rtofs_smos_woa.py {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/{valid?fmt=%Y%m%d}_rtofs_glo_2ds_f024_prog.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/SMOS-L3-GLOB_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/OSTIA-UKMO-L4-GLOB-v2.0_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss {valid?fmt=%Y%m%d} fcst + +# List of levels to evaluate for forecast variable 1 +# A03 = 3 hour accumulation in GRIB file +FCST_VAR1_LEVELS = + +# List of thresholds to evaluate for each name/level combination for +# forecast variable 1 +FCST_VAR1_THRESH = + +#FCST_GRID_STAT_FILE_TYPE = + +# Name of observation variable 1 +OBS_VAR1_NAME = {CONFIG_DIR}/read_rtofs_smos_woa.py {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/{valid?fmt=%Y%m%d}_rtofs_glo_2ds_f024_prog.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/SMOS-L3-GLOB_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/OSTIA-UKMO-L4-GLOB-v2.0_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss {valid?fmt=%Y%m%d} obs + + +# List of levels to evaluate for observation variable 1 +# (*,*) is NetCDF notation - must include quotes around these values! +# must be the same length as FCST_VAR1_LEVELS +OBS_VAR1_LEVELS = + +# List of thresholds to evaluate for each name/level combination for +# observation variable 1 +OBS_VAR1_THRESH = + +#GRID_STAT_MET_CONFIG_OVERRIDES = cat_thresh = [>=0.15]; +#BOTH_VAR1_THRESH = >=0.15 + +#OBS_GRID_STAT_FILE_TYPE = + + +# Name of climatology variable 1 +GRID_STAT_CLIMO_MEAN_FIELD = {name="{CONFIG_DIR}/read_rtofs_smos_woa.py {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/{valid?fmt=%Y%m%d}_rtofs_glo_2ds_f024_prog.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/SMOS-L3-GLOB_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/OSTIA-UKMO-L4-GLOB-v2.0_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss {valid?fmt=%Y%m%d} climo"; level="(*,*)";} + + +# Time relative to valid time (in seconds) to allow files to be considered +# valid. Set both BEGIN and END to 0 to require the exact time in the filename +# Not used in this example. +FCST_GRID_STAT_FILE_WINDOW_BEGIN = 0 +FCST_GRID_STAT_FILE_WINDOW_END = 0 +OBS_GRID_STAT_FILE_WINDOW_BEGIN = 0 +OBS_GRID_STAT_FILE_WINDOW_END = 0 + +# MET GridStat neighborhood values +# See the MET User's Guide GridStat section for more information + +# width value passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_WIDTH = 1 + +# shape value passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_SHAPE = SQUARE + +# cov thresh list passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_COV_THRESH = >=0.5 + +# Set to true to run GridStat separately for each field specified +# Set to false to create one run of GridStat per run time that +# includes all fields specified. +GRID_STAT_ONCE_PER_FIELD = False + +# Set to true if forecast data is probabilistic +FCST_IS_PROB = false + +# Only used if FCST_IS_PROB is true - sets probabilistic threshold +FCST_GRID_STAT_PROB_THRESH = ==0.1 + +# Set to true if observation data is probabilistic +# Only used if configuring forecast data as the 'OBS' input +OBS_IS_PROB = false + +# Only used if OBS_IS_PROB is true - sets probabilistic threshold +OBS_GRID_STAT_PROB_THRESH = ==0.1 + +GRID_STAT_OUTPUT_PREFIX = SSS + +#GRID_STAT_CLIMO_MEAN_FILE_NAME = +#GRID_STAT_CLIMO_MEAN_FIELD = +#GRID_STAT_CLIMO_MEAN_REGRID_METHOD = +#GRID_STAT_CLIMO_MEAN_REGRID_WIDTH = +#GRID_STAT_CLIMO_MEAN_REGRID_VLD_THRESH = +#GRID_STAT_CLIMO_MEAN_REGRID_SHAPE = +#GRID_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_MEAN_MATCH_MONTH = +#GRID_STAT_CLIMO_MEAN_DAY_INTERVAL = +#GRID_STAT_CLIMO_MEAN_HOUR_INTERVAL = + +#GRID_STAT_CLIMO_STDEV_FILE_NAME = +#GRID_STAT_CLIMO_STDEV_FIELD = +#GRID_STAT_CLIMO_STDEV_REGRID_METHOD = +#GRID_STAT_CLIMO_STDEV_REGRID_WIDTH = +#GRID_STAT_CLIMO_STDEV_REGRID_VLD_THRESH = +#GRID_STAT_CLIMO_STDEV_REGRID_SHAPE = +#GRID_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_STDEV_MATCH_MONTH = +#GRID_STAT_CLIMO_STDEV_DAY_INTERVAL = +#GRID_STAT_CLIMO_STDEV_HOUR_INTERVAL = + + +#GRID_STAT_CLIMO_CDF_BINS = 1 +#GRID_STAT_CLIMO_CDF_CENTER_BINS = False +#GRID_STAT_CLIMO_CDF_WRITE_BINS = True + +#GRID_STAT_OUTPUT_FLAG_FHO = NONE +#GRID_STAT_OUTPUT_FLAG_CTC = NONE +#GRID_STAT_OUTPUT_FLAG_CTS = NONE +#GRID_STAT_OUTPUT_FLAG_MCTC = NONE +#GRID_STAT_OUTPUT_FLAG_MCTS = NONE +GRID_STAT_OUTPUT_FLAG_CNT = BOTH +#GRID_STAT_OUTPUT_FLAG_SL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_SAL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VAL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VCNT = NONE +#GRID_STAT_OUTPUT_FLAG_PCT = NONE +#GRID_STAT_OUTPUT_FLAG_PSTD = NONE +#GRID_STAT_OUTPUT_FLAG_PJC = NONE +#GRID_STAT_OUTPUT_FLAG_PRC = NONE +#GRID_STAT_OUTPUT_FLAG_ECLV = BOTH +#GRID_STAT_OUTPUT_FLAG_NBRCTC = NONE +#GRID_STAT_OUTPUT_FLAG_NBRCTS = NONE +#GRID_STAT_OUTPUT_FLAG_NBRCNT = NONE +#GRID_STAT_OUTPUT_FLAG_GRAD = BOTH +#GRID_STAT_OUTPUT_FLAG_DMAP = NONE + +#GRID_STAT_NC_PAIRS_FLAG_LATLON = FALSE +#GRID_STAT_NC_PAIRS_FLAG_RAW = FALSE +#GRID_STAT_NC_PAIRS_FLAG_DIFF = FALSE +#GRID_STAT_NC_PAIRS_FLAG_CLIMO = FALSE +#GRID_STAT_NC_PAIRS_FLAG_CLIMO_CDP = FALSE +#GRID_STAT_NC_PAIRS_FLAG_WEIGHT = FALSE +#GRID_STAT_NC_PAIRS_FLAG_NBRHD = FALSE +#GRID_STAT_NC_PAIRS_FLAG_FOURIER = FALSE +#GRID_STAT_NC_PAIRS_FLAG_GRADIENT = FALSE +#GRID_STAT_NC_PAIRS_FLAG_DISTANCE_MAP = FALSE +#GRID_STAT_NC_PAIRS_FLAG_APPLY_MASK = FALSE + + +# End of [config] section and start of [dir] section +[dir] +#use case configuration file directory +CONFIG_DIR = {PARM_BASE}/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss +# directory containing forecast input to GridStat +FCST_GRID_STAT_INPUT_DIR = + +# directory containing observation input to GridStat +OBS_GRID_STAT_INPUT_DIR = + +# directory containing climatology mean input to GridStat +# Not used in this example +GRID_STAT_CLIMO_MEAN_INPUT_DIR = + +# directory containing climatology mean input to GridStat +# Not used in this example +GRID_STAT_CLIMO_STDEV_INPUT_DIR = + +# directory to write output from GridStat +GRID_STAT_OUTPUT_DIR = {OUTPUT_BASE} + +# End of [dir] section and start of [filename_templates] section +[filename_templates] + +# Template to look for forecast input to GridStat relative to FCST_GRID_STAT_INPUT_DIR +FCST_GRID_STAT_INPUT_TEMPLATE = PYTHON_NUMPY + +# Template to look for observation input to GridStat relative to OBS_GRID_STAT_INPUT_DIR +OBS_GRID_STAT_INPUT_TEMPLATE = PYTHON_NUMPY + +# Optional subdirectories relative to GRID_STAT_OUTPUT_DIR to write output from GridStat +GRID_STAT_OUTPUT_TEMPLATE = {valid?fmt=%Y%m%d} + +# Template to look for climatology input to GridStat relative to GRID_STAT_CLIMO_MEAN_INPUT_DIR +# Not used in this example +GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE = PYTHON_NUMPY + +# Template to look for climatology input to GridStat relative to GRID_STAT_CLIMO_STDEV_INPUT_DIR +# Not used in this exampls +GRID_STAT_CLIMO_STDEV_INPUT_TEMPLATE = + +# Used to specify one or more verification mask files for GridStat +# Not used for this example +GRID_STAT_VERIFICATION_MASK_TEMPLATE = diff --git a/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/read_rtofs_smos_woa.py b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/read_rtofs_smos_woa.py new file mode 100644 index 0000000000..04017cf6c0 --- /dev/null +++ b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss/read_rtofs_smos_woa.py @@ -0,0 +1,346 @@ +#!/bin/env python +""" +Code adapted from +Todd Spindler +NOAA/NWS/NCEP/EMC +Designed to read in RTOFS,SMOS,WOA and OSTIA data +and based on user input, read sss data +and pass back in memory the forecast, observation, or climatology +data field +""" + +import numpy as np +import xarray as xr +import pandas as pd +import pyresample as pyr +from pandas.tseries.offsets import DateOffset +from datetime import datetime, timedelta +from sklearn.metrics import mean_squared_error +import io +from glob import glob +import warnings +import os, sys + + +if len(sys.argv) < 6: + print("Must specify the following elements: fcst_file obs_file ice_file, climo_file, valid_date, file_flag") + sys.exit(1) + +rtofsfile = os.path.expandvars(sys.argv[1]) +sssfile = os.path.expandvars(sys.argv[2]) +icefile = os.path.expandvars(sys.argv[3]) +climoDir = os.path.expandvars(sys.argv[4]) +vDate=datetime.strptime(sys.argv[5],'%Y%m%d') +file_flag = sys.argv[6] + +print('Starting Satellite SMOS V&V at',datetime.now(),'for',vDate, ' file_flag:',file_flag) + +pd.date_range(vDate,vDate) +platform='SMOS' +param='sss' + + +##################################################################### +# READ SMOS data ################################################## +##################################################################### + +if not os.path.exists(sssfile): + print('missing SMOS file for',vDate) + +sss_data=xr.open_dataset(sssfile,decode_times=True) +sss_data['time']=sss_data.time-pd.Timedelta('12H') # shift 12Z offset time to 00Z +sss_data2=sss_data['sss'].astype('single') +print('Retrieved SMOS data from NESDIS for',sss_data2.time.values) +sss_data2=sss_data2.rename({'longitude':'lon','latitude':'lat'}) + + +# all coords need to be single precision +sss_data2['lon']=sss_data2.lon.astype('single') +sss_data2['lat']=sss_data2.lat.astype('single') +sss_data2.attrs['platform']=platform +sss_data2.attrs['units']='PSU' + +##################################################################### +# READ RTOFS data (model output in Tri-polar coordinates) ########### +##################################################################### + +print('reading rtofs ice') +if not os.path.exists(rtofsfile): + print('missing rtofs file',rtofsfile) + sys.exit(1) + +indata=xr.open_dataset(rtofsfile,decode_times=True) + + +indata=indata.mean(dim='MT') +indata = indata[param][:-1,] +indata.coords['time']=vDate +#indata.coords['fcst']=fcst + +outdata=indata.copy() + +outdata=outdata.rename({'Longitude':'lon','Latitude':'lat',}) +# all coords need to be single precision +outdata['lon']=outdata.lon.astype('single') +outdata['lat']=outdata.lat.astype('single') +outdata.attrs['platform']='rtofs '+platform + +##################################################################### +# READ CLIMO WOA data - May require 2 files depending on the date ### +##################################################################### + +if not os.path.exists(climoDir): + print('missing climo file file for',vDate) + +vDate=pd.Timestamp(vDate) + +climofile="woa13_decav_s{:02n}_04v2.nc".format(vDate.month) +climo_data=xr.open_dataset(climoDir+'/'+climofile,decode_times=False) +climo_data=climo_data['s_an'].squeeze()[0,] + +if vDate.day==15: # even for Feb, just because + climofile="woa13_decav_s{:02n}_04v2.nc".format(vDate.month) + climo_data=xr.open_dataset(climoDir+'/'+climofile,decode_times=False) + climo_data=climo_data['s_an'].squeeze()[0,] # surface only +else: + if vDate.day < 15: + start=vDate - DateOffset(months=1,day=15) + stop=pd.Timestamp(vDate.year,vDate.month,15) + else: + start=pd.Timestamp(vDate.year,vDate.month,15) + stop=vDate + DateOffset(months=1,day=15) + left=(vDate-start)/(stop-start) + + climofile1="woa13_decav_s{:02n}_04v2.nc".format(start.month) + climofile2="woa13_decav_s{:02n}_04v2.nc".format(stop.month) + climo_data1=xr.open_dataset(climoDir+'/'+climofile1,decode_times=False) + climo_data2=xr.open_dataset(climoDir+'/'+climofile2,decode_times=False) + climo_data1=climo_data1['s_an'].squeeze()[0,] # surface only + climo_data2=climo_data2['s_an'].squeeze()[0,] # surface only + + print('climofile1 :', climofile1) + print('climofile2 :', climofile2) + climo_data=climo_data1+((climo_data2-climo_data1)*left) + climofile='weighted average of '+climofile1+' and '+climofile2 + +# all coords need to be single precision +climo_data['lon']=climo_data.lon.astype('single') +climo_data['lat']=climo_data.lat.astype('single') +climo_data.attrs['platform']='woa' +climo_data.attrs['filename']=climofile + +##################################################################### +# READ ICE data for masking ######################################### +##################################################################### + +if not os.path.exists(icefile): + print('missing OSTIA ice file for',vDate) + +ice_data=xr.open_dataset(icefile,decode_times=True) +ice_data=ice_data.rename({'sea_ice_fraction':'ice'}) + +# all coords need to be single precision +ice_data2=ice_data.ice.astype('single') +ice_data2['lon']=ice_data2.lon.astype('single') +ice_data2['lat']=ice_data2.lat.astype('single') + + +def regrid(model,obs): + """ + regrid data to obs -- this assumes DataArrays + """ + model2=model.copy() + model2_lon=model2.lon.values + model2_lat=model2.lat.values + model2_data=model2.to_masked_array() + if model2_lon.ndim==1: + model2_lon,model2_lat=np.meshgrid(model2_lon,model2_lat) + + obs2=obs.copy() + obs2_lon=obs2.lon.astype('single').values + obs2_lat=obs2.lat.astype('single').values + obs2_data=obs2.astype('single').to_masked_array() + if obs2.lon.ndim==1: + obs2_lon,obs2_lat=np.meshgrid(obs2.lon.values,obs2.lat.values) + + model2_lon1=pyr.utils.wrap_longitudes(model2_lon) + model2_lat1=model2_lat.copy() + obs2_lon1=pyr.utils.wrap_longitudes(obs2_lon) + obs2_lat1=obs2_lat.copy() + + # pyresample gausssian-weighted kd-tree interp + # define the grids + orig_def = pyr.geometry.GridDefinition(lons=model2_lon1,lats=model2_lat1) + targ_def = pyr.geometry.GridDefinition(lons=obs2_lon1,lats=obs2_lat1) + radius=50000 + sigmas=25000 + model2_data2=pyr.kd_tree.resample_gauss(orig_def,model2_data,targ_def, + radius_of_influence=radius, + sigmas=sigmas, + fill_value=None) + model=xr.DataArray(model2_data2,coords=[obs.lat.values,obs.lon.values],dims=['lat','lon']) + + return model + +def expand_grid(data): + """ + concatenate global data for edge wraps + """ + + data2=data.copy() + data2['lon']=data2.lon+360 + data3=xr.concat((data,data2),dim='lon') + return data3 + +sss_data2=sss_data2.squeeze() + +print('regridding climo to obs') +climo_data=climo_data.squeeze() +climo_data=regrid(climo_data,sss_data2) + +print('regridding ice to obs') +ice_data2=regrid(ice_data2,sss_data2) + +print('regridding model to obs') +model2=regrid(outdata,sss_data2) + +# combine obs ice mask with ncep +obs2=sss_data2.to_masked_array() +ice2=ice_data2.to_masked_array() +climo2=climo_data.to_masked_array() +model2=model2.to_masked_array() + +#reconcile with obs +obs2.mask=np.ma.mask_or(obs2.mask,ice2>0.0) +obs2.mask=np.ma.mask_or(obs2.mask,climo2.mask) +obs2.mask=np.ma.mask_or(obs2.mask,model2.mask) +climo2.mask=obs2.mask +model2.mask=obs2.mask + +obs2=xr.DataArray(obs2,coords=[sss_data2.lat.values,sss_data2.lon.values], dims=['lat','lon']) +model2=xr.DataArray(model2,coords=[sss_data2.lat.values,sss_data2.lon.values], dims=['lat','lon']) +climo2=xr.DataArray(climo2,coords=[sss_data2.lat.values,sss_data2.lon.values], dims=['lat','lon']) + +model2=expand_grid(model2) +climo2=expand_grid(climo2) +obs2=expand_grid(obs2) + +#Create the MET grids based on the file_flag +if file_flag == 'fcst': + met_data = model2[:,:] + #trim the lat/lon grids so they match the data fields + lat_met = model2.lat + lon_met = model2.lon + print(" RTOFS Data shape: "+repr(met_data.shape)) + v_str = vDate.strftime("%Y%m%d") + v_str = v_str + '_000000' + lat_ll = float(lat_met.min()) + lon_ll = float(lon_met.min()) + n_lat = lat_met.shape[0] + n_lon = lon_met.shape[0] + delta_lat = (float(lat_met.max()) - float(lat_met.min()))/float(n_lat) + delta_lon = (float(lon_met.max()) - float(lon_met.min()))/float(n_lon) + print(f"variables:" + f"lat_ll: {lat_ll} lon_ll: {lon_ll} n_lat: {n_lat} n_lon: {n_lon} delta_lat: {delta_lat} delta_lon: {delta_lon}") + met_data.attrs = { + 'valid': v_str, + 'init': v_str, + 'lead': "00", + 'accum': "00", + 'name': 'sss', + 'standard_name': 'sss', + 'long_name': 'sss', + 'level': "SURFACE", + 'units': "degC", + + 'grid': { + 'type': "LatLon", + 'name': "RTOFS Grid", + 'lat_ll': lat_ll, + 'lon_ll': lon_ll, + 'delta_lat': delta_lat, + 'delta_lon': delta_lon, + 'Nlat': n_lat, + 'Nlon': n_lon, + } + } + attrs = met_data.attrs + +if file_flag == 'obs': + met_data = obs2[:,:] + #trim the lat/lon grids so they match the data fields + lat_met = obs2.lat + lon_met = obs2.lon + v_str = vDate.strftime("%Y%m%d") + v_str = v_str + '_000000' + lat_ll = float(lat_met.min()) + lon_ll = float(lon_met.min()) + n_lat = lat_met.shape[0] + n_lon = lon_met.shape[0] + delta_lat = (float(lat_met.max()) - float(lat_met.min()))/float(n_lat) + delta_lon = (float(lon_met.max()) - float(lon_met.min()))/float(n_lon) + print(f"variables:" + f"lat_ll: {lat_ll} lon_ll: {lon_ll} n_lat: {n_lat} n_lon: {n_lon} delta_lat: {delta_lat} delta_lon: {delta_lon}") + met_data.attrs = { + 'valid': v_str, + 'init': v_str, + 'lead': "00", + 'accum': "00", + 'name': 'sss', + 'standard_name': 'analyzed sss', + 'long_name': 'analyzed sss', + 'level': "SURFACE", + 'units': "degC", + + 'grid': { + 'type': "LatLon", + 'name': "Lat Lon", + 'lat_ll': lat_ll, + 'lon_ll': lon_ll, + 'delta_lat': delta_lat, + 'delta_lon': delta_lon, + 'Nlat': n_lat, + 'Nlon': n_lon, + } + } + attrs = met_data.attrs + +if file_flag == 'climo': + met_data = climo2[:,:] + #modify the lat and lon grids since they need to match the data dimensions, and code cuts the last row/column of data + lat_met = climo2.lat + lon_met = climo2.lon + v_str = vDate.strftime("%Y%m%d") + v_str = v_str + '_000000' + lat_ll = float(lat_met.min()) + lon_ll = float(lon_met.min()) + n_lat = lat_met.shape[0] + n_lon = lon_met.shape[0] + delta_lat = (float(lat_met.max()) - float(lat_met.min()))/float(n_lat) + delta_lon = (float(lon_met.max()) - float(lon_met.min()))/float(n_lon) + print(f"variables:" + f"lat_ll: {lat_ll} lon_ll: {lon_ll} n_lat: {n_lat} n_lon: {n_lon} delta_lat: {delta_lat} delta_lon: {delta_lon}") + met_data.attrs = { + 'valid': v_str, + 'init': v_str, + 'lead': "00", + 'accum': "00", + 'name': 'sea_water_temperature', + 'standard_name': 'sea_water_temperature', + 'long_name': 'sea_water_temperature', + 'level': "SURFACE", + 'units': "degC", + + 'grid': { + 'type': "LatLon", + 'name': "crs Grid", + 'lat_ll': lat_ll, + 'lon_ll': lon_ll, + 'delta_lat': delta_lat, + 'delta_lon': delta_lon, + 'Nlat': n_lat, + 'Nlon': n_lon, + } + } + attrs = met_data.attrs + From 6890f39d7db6f0c78c7f834eb60cbed585bec8a0 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 13 Jan 2022 10:26:31 -0700 Subject: [PATCH 258/770] turn off new use case from every push --- .github/parm/use_case_groups.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 9fc35f1847..5413a364c3 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -62,7 +62,7 @@ { "category": "marine_and_cryosphere", "index_list": "3", - "run": true + "run": false }, { "category": "medium_range", From 8ef160961408de06de3940aca91d77f3aaa41b67 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 13 Jan 2022 14:11:26 -0700 Subject: [PATCH 259/770] feature 1236 Control Members in EnsembleStat and GenEnsProd (#1357) --- docs/Users_Guide/glossary.rst | 36 +++ docs/Users_Guide/wrappers.rst | 50 ++++ .../test_ensemble_stat_wrapper.py | 6 + .../gen_ens_prod/test_gen_ens_prod_wrapper.py | 5 + metplus/wrappers/ensemble_stat_wrapper.py | 18 ++ metplus/wrappers/gen_ens_prod_wrapper.py | 11 +- parm/met_config/EnsembleStatConfig_wrapped | 6 + parm/met_config/GenEnsProdConfig_wrapped | 7 + .../EnsembleStat/EnsembleStat.conf | 242 +++++++----------- .../GenEnsProd/GenEnsProd.conf | 3 + 10 files changed, 232 insertions(+), 152 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index f3e3f1ec54..4b3c5e6a65 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8723,3 +8723,39 @@ METplus Configuration Glossary case and the values differ between them. | *Used by:* All + + GEN_ENS_PROD_ENS_MEMBER_IDS + Specify the value for 'ens_member_ids' in the MET configuration file for GenEnsProd. + + | *Used by:* GenEnsProd + + GEN_ENS_PROD_CONTROL_ID + Specify the value for 'control_id' in the MET configuration file for GenEnsProd. + + | *Used by:* GenEnsProd + + ENSEMBLE_STAT_ENS_MEMBER_IDS + Specify the value for 'ens_member_ids' in the MET configuration file for EnsembleStat. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_CONTROL_ID + Specify the value for 'control_id' in the MET configuration file for EnsembleStat. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_CTRL_INPUT_DIR + Input directory for optional control file to use with EnsembleStat. + See also :term:`ENSEMBLE_STAT_CTRL_INPUT_TEMPLATE`. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_CTRL_INPUT_TEMPLATE + Template used to specify an optional control filename for EnsembleStat. + Note that if a control member file is found in the ensemble file list, + it will automatically be removed by the wrapper to prevent an error in the + MET tool. This may require adjusting the value for + :term:`ENSEMBLE_STAT_N_MEMBERS` and/or + :term:`ENSEMBLE_STAT_ENS_VLD_THRESH`. + + | *Used by:* EnsembleStat diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index e06bee76fa..5976640376 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -183,6 +183,8 @@ METplus Configuration | :term:`OBS_ENSEMBLE_STAT_GRID_INPUT_TEMPLATE` | :term:`FCST_ENSEMBLE_STAT_INPUT_TEMPLATE` | :term:`ENSEMBLE_STAT_OUTPUT_TEMPLATE` +| :term:`ENSEMBLE_STAT_CTRL_INPUT_DIR` +| :term:`ENSEMBLE_STAT_CTRL_INPUT_TEMPLATE` | :term:`LOG_ENSEMBLE_STAT_VERBOSITY` | :term:`FCST_ENSEMBLE_STAT_INPUT_DATATYPE` | :term:`OBS_ENSEMBLE_STAT_INPUT_POINT_DATATYPE` @@ -277,6 +279,8 @@ METplus Configuration | :term:`ENSEMBLE_STAT_OBS_QUALITY_INC` | :term:`ENSEMBLE_STAT_OBS_QUALITY_EXC` | :term:`ENSEMBLE_STAT_MET_CONFIG_OVERRIDES` +| :term:`ENSEMBLE_STAT_ENS_MEMBER_IDS` +| :term:`ENSEMBLE_STAT_CONTROL_ID` | :term:`ENSEMBLE_STAT_VERIFICATION_MASK_TEMPLATE` (optional) | :term:`ENS_VAR_NAME` (optional) | :term:`ENS_VAR_LEVELS` (optional) @@ -853,6 +857,28 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`ENSEMBLE_STAT_OBS_QUALITY_EXC` - obs_quality_exc +**${METPLUS_ENS_MEMBER_IDS}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`ENSEMBLE_STAT_ENS_MEMBER_IDS` + - ens_member_ids + +**${METPLUS_CONTROL_ID}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`ENSEMBLE_STAT_CONTROL_ID` + - control_id + **${METPLUS_MET_CONFIG_OVERRIDES}** .. list-table:: @@ -1049,6 +1075,8 @@ METplus Configuration | :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_NMEP` | :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO` | :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO_CDF` +| :term:`GEN_ENS_PROD_ENS_MEMBER_IDS` +| :term:`GEN_ENS_PROD_CONTROL_ID` | :term:`GEN_ENS_PROD_MET_CONFIG_OVERRIDES` .. _gen-ens-prod-met-conf: @@ -1339,6 +1367,28 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO_CDF` - ensemble_flag.climo_cdf +**${METPLUS_ENS_MEMBER_IDS}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`GEN_ENS_PROD_ENS_MEMBER_IDS` + - ens_member_ids + +**${METPLUS_CONTROL_ID}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`GEN_ENS_PROD_CONTROL_ID` + - control_id + **${METPLUS_MET_CONFIG_OVERRIDES}** .. list-table:: diff --git a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py index 1f864709a1..1ef41cf8e8 100644 --- a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py +++ b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py @@ -547,6 +547,12 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, ({'ENSEMBLE_STAT_OBS_QUALITY_EXC': '5,6,7', }, {'METPLUS_OBS_QUALITY_EXC': 'obs_quality_exc = ["5", "6", "7"];'}), + ({'ENSEMBLE_STAT_ENS_MEMBER_IDS': '1,2,3,4', }, + {'METPLUS_ENS_MEMBER_IDS': 'ens_member_ids = ["1", "2", "3", "4"];'}), + + ({'ENSEMBLE_STAT_CONTROL_ID': '0', }, + {'METPLUS_CONTROL_ID': 'control_id = "0";'}), + ] ) def test_ensemble_stat_single_field(metplus_config, config_overrides, diff --git a/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py b/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py index 60703aecb2..0ccc92117d 100644 --- a/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py +++ b/internal_tests/pytests/gen_ens_prod/test_gen_ens_prod_wrapper.py @@ -343,6 +343,11 @@ def set_minimum_config_settings(config): ) }), + ({'GEN_ENS_PROD_ENS_MEMBER_IDS': '1,2,3,4', }, + {'METPLUS_ENS_MEMBER_IDS': 'ens_member_ids = ["1", "2", "3", "4"];'}), + + ({'GEN_ENS_PROD_CONTROL_ID': '0', }, + {'METPLUS_CONTROL_ID': 'control_id = "0";'}), ] ) def test_gen_ens_prod_single_field(metplus_config, config_overrides, diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index 0bc64cd76b..0d8e0141af 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -64,6 +64,8 @@ class EnsembleStatWrapper(CompareGriddedWrapper): 'METPLUS_OUTPUT_PREFIX', 'METPLUS_OBS_QUALITY_INC', 'METPLUS_OBS_QUALITY_EXC', + 'METPLUS_ENS_MEMBER_IDS', + 'METPLUS_CONTROL_ID', ] # handle deprecated env vars used pre v4.0.0 @@ -203,6 +205,16 @@ def create_c_dict(self): '') ) + # get ctrl (control) template/dir - optional + c_dict['CTRL_INPUT_TEMPLATE'] = self.config.getraw( + 'config', + 'ENSEMBLE_STAT_CTRL_INPUT_TEMPLATE' + ) + c_dict['CTRL_INPUT_DIR'] = self.config.getdir( + 'ENSEMBLE_STAT_CTRL_INPUT_DIR', + '' + ) + # get climatology config variables self.handle_climo_dict() @@ -312,6 +324,12 @@ def create_c_dict(self): 'ENSEMBLE_STAT_OBS_QUALITY_EXCLUDE'] ) + self.add_met_config(name='ens_member_ids', + data_type='list') + + self.add_met_config(name='control_id', + data_type='string') + # old method of setting MET config values c_dict['ENS_THRESH'] = ( self.config.getstr('config', 'ENSEMBLE_STAT_ENS_THRESH', '1.0') diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index e8011fb0bd..5df8bbb1c3 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -30,6 +30,8 @@ class GenEnsProdWrapper(LoopTimesWrapper): 'METPLUS_CLIMO_MEAN_DICT', 'METPLUS_CLIMO_STDEV_DICT', 'METPLUS_ENSEMBLE_FLAG_DICT', + 'METPLUS_ENS_MEMBER_IDS', + 'METPLUS_CONTROL_ID', ] ENSEMBLE_FLAGS = [ @@ -199,6 +201,12 @@ def create_c_dict(self): self.handle_flags('ENSEMBLE') + self.add_met_config(name='ens_member_ids', + data_type='list') + + self.add_met_config(name='control_id', + data_type='string') + c_dict['ALLOW_MULTIPLE_FILES'] = True return c_dict @@ -257,5 +265,6 @@ def get_command(self): @return command to run """ return (f"{self.app_path} -v {self.c_dict['VERBOSITY']}" - f" -ens {self.infiles[0]} -out {self.get_output_path()}" + f" -ens {self.infiles[0]}" + f" -out {self.get_output_path()}" f" {' '.join(self.args)}") diff --git a/parm/met_config/EnsembleStatConfig_wrapped b/parm/met_config/EnsembleStatConfig_wrapped index 6374340917..e398ca1d1d 100644 --- a/parm/met_config/EnsembleStatConfig_wrapped +++ b/parm/met_config/EnsembleStatConfig_wrapped @@ -53,6 +53,12 @@ ens = { ${METPLUS_ENS_FIELD} } +//ens_member_ids = +${METPLUS_ENS_MEMBER_IDS} + +//control_id = +${METPLUS_CONTROL_ID} + //////////////////////////////////////////////////////////////////////////////// // diff --git a/parm/met_config/GenEnsProdConfig_wrapped b/parm/met_config/GenEnsProdConfig_wrapped index 2da107e1d4..9171881b35 100644 --- a/parm/met_config/GenEnsProdConfig_wrapped +++ b/parm/met_config/GenEnsProdConfig_wrapped @@ -63,6 +63,13 @@ ens = { } +//ens_member_ids = +${METPLUS_ENS_MEMBER_IDS} + +//control_id = +${METPLUS_CONTROL_ID} + + //////////////////////////////////////////////////////////////////////////////// // diff --git a/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf b/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf index c4c61ce8e7..c7714c0291 100644 --- a/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf +++ b/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf @@ -1,84 +1,117 @@ -# Ensemble Stat -# This METplus conf file runs the MET met_test unit test ensemble_stat command. -#ensemble_stat \ -# 6 \ -# /path/totrunk/met/data/sample_fcst/2009123112/*gep*/d01_2009123112_02400.grib \ -# /path/totrunk/met/scripts/config/EnsembleStatConfig \ -# -grid_obs /path/to/trunk/met/data/sample_obs/ST4/ST4.2010010112.24h \ -# -point_obs /path/to/MET_test_output/met_test_scripts/ascii2nc/precip24_2010010112.nc \ -# -outdir /path/to/MET_test_output/met_test_scripts/ensemble_stat \ -# -v 2 - [config] -## Configuration-related settings such as the process list, begin and end times, etc. PROCESS_LIST = EnsembleStat -# Looping by times: steps through each 'task' in the PROCESS_LIST for each -# defined time, and repeats until all times have been evaluated. -LOOP_ORDER = times +### +# Time Info +### -# LOOP_BY: Set to INIT to loop over initialization times LOOP_BY = INIT - -# Format of INIT_BEG and INT_END INIT_TIME_FMT = %Y%m%d%H - -# Start time for METplus run INIT_BEG=2009123112 - -# End time for METplus run INIT_END=2009123112 - -# Increment between METplus runs in seconds. Must be >= 60 INIT_INCREMENT=3600 -# List of forecast leads to process -LEAD_SEQ = 24 +LEAD_SEQ = 24H -# Used in the MET config file for: model, output_prefix -MODEL = WRF +LOOP_ORDER = times -ENSEMBLE_STAT_DESC = NA -# Name to identify observation data in output +### +# File I/O +### + +FCST_ENSEMBLE_STAT_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst +FCST_ENSEMBLE_STAT_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/arw-???-gep?/d01_{init?fmt=%Y%m%d%H}_0{lead?fmt=%HH}00.grib + +#ENSEMBLE_STAT_CTRL_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst +#ENSEMBLE_STAT_CTRL_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/arw-fer-gep1/d01_{init?fmt=%Y%m%d%H}_0{lead?fmt=%HH}00.grib + +ENSEMBLE_STAT_N_MEMBERS = 6 + + +OBS_ENSEMBLE_STAT_POINT_INPUT_DIR = {INPUT_BASE}/met_test/out/ascii2nc +OBS_ENSEMBLE_STAT_POINT_INPUT_TEMPLATE = precip24_{valid?fmt=%Y%m%d%H}.nc + + +OBS_ENSEMBLE_STAT_GRID_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_obs/ST4 +OBS_ENSEMBLE_STAT_GRID_INPUT_TEMPLATE = ST4.{valid?fmt=%Y%m%d%H}.24h + + +ENSEMBLE_STAT_CLIMO_MEAN_INPUT_DIR = +ENSEMBLE_STAT_CLIMO_MEAN_INPUT_TEMPLATE = + +ENSEMBLE_STAT_CLIMO_STDEV_INPUT_DIR = +ENSEMBLE_STAT_CLIMO_STDEV_INPUT_TEMPLATE = + + +ENSEMBLE_STAT_OUTPUT_DIR = {OUTPUT_BASE}/ensemble +ENSEMBLE_STAT_OUTPUT_TEMPLATE = {init?fmt=%Y%m%d%H%M}/ensemble_stat + + +### +# Field Info +### + +MODEL = WRF OBTYPE = MC_PCP -#ENSEMBLE_STAT_DESC = -# The MET ensemble_stat logging level -# 0 quiet to 5 loud, Verbosity setting for MET ensemble_stat output, 2 is default. -# This takes precendence over the general LOG_MET_VERBOSITY set in metplus_logging.conf +FCST_VAR1_NAME = APCP +FCST_VAR1_LEVELS = A24 +FCST_VAR1_OPTIONS = ens_ssvar_bin_size = 0.1; ens_phist_bin_size = 0.05; + + +OBS_VAR1_NAME = {FCST_VAR1_NAME} +OBS_VAR1_LEVELS = {FCST_VAR1_LEVELS} +OBS_VAR1_OPTIONS = {FCST_VAR1_OPTIONS} + + +ENS_VAR1_NAME = APCP +ENS_VAR1_LEVELS = A24 +ENS_VAR1_THRESH = >0.0, >=10.0 + +ENS_VAR2_NAME = REFC +ENS_VAR2_LEVELS = L0 +ENS_VAR2_THRESH = >=35.0 + +ENS_VAR2_OPTIONS = GRIB1_ptv = 129; + +ENS_VAR3_NAME = UGRD +ENS_VAR3_LEVELS = Z10 +ENS_VAR3_THRESH = >=5.0 + +ENS_VAR4_NAME = VGRD +ENS_VAR4_LEVELS = Z10 +ENS_VAR4_THRESH = >=5.0 + +ENS_VAR5_NAME = WIND +ENS_VAR5_LEVELS = Z10 +ENS_VAR5_THRESH = >=5.0 + + +### +# EnsembleStat +### + #LOG_ENSEMBLE_STAT_VERBOSITY = 2 +ENSEMBLE_STAT_CONFIG_FILE = {PARM_BASE}/met_config/EnsembleStatConfig_wrapped + +ENSEMBLE_STAT_DESC = NA + OBS_ENSEMBLE_STAT_WINDOW_BEGIN = -5400 OBS_ENSEMBLE_STAT_WINDOW_END = 5400 -OBS_FILE_WINDOW_BEGIN = 0 -OBS_FILE_WINDOW_END = 0 - -# number of expected members for ensemble. Should correspond with the -# number of items in the list for FCST_ENSEMBLE_STAT_INPUT_TEMPLATE -ENSEMBLE_STAT_N_MEMBERS = 6 -# ens.ens_thresh value in the MET config file -# threshold for ratio of valid files to expected files to allow app to run ENSEMBLE_STAT_ENS_THRESH = 1.0 -# ens.vld_thresh value in the MET config file ENSEMBLE_STAT_ENS_VLD_THRESH = 1.0 ENSEMBLE_STAT_OUTPUT_PREFIX = -ENSEMBLE_STAT_CONFIG_FILE = {PARM_BASE}/met_config/EnsembleStatConfig_wrapped - -# ENSEMBLE_STAT_MET_OBS_ERR_TABLE is not required. -# If the variable is not defined, or the value is not set -# than the MET default is used. #ENSEMBLE_STAT_MET_OBS_ERR_TABLE = - -# Used in the MET config file for: regrid to_grid field ENSEMBLE_STAT_REGRID_TO_GRID = NONE ENSEMBLE_STAT_REGRID_METHOD = NEAREST ENSEMBLE_STAT_REGRID_WIDTH = 1 @@ -131,12 +164,17 @@ ENSEMBLE_STAT_ENS_PHIST_BIN_SIZE = 0.05 #ENSEMBLE_STAT_CLIMO_STDEV_DAY_INTERVAL = 31 #ENSEMBLE_STAT_CLIMO_STDEV_HOUR_INTERVAL = 6 - ENSEMBLE_STAT_CLIMO_CDF_BINS = 1 ENSEMBLE_STAT_CLIMO_CDF_CENTER_BINS = False ENSEMBLE_STAT_CLIMO_CDF_WRITE_BINS = True ENSEMBLE_STAT_MASK_GRID = FULL +ENSEMBLE_STAT_MASK_POLY = + MET_BASE/poly/HMT_masks/huc4_1605_poly.nc, + MET_BASE/poly/HMT_masks/huc4_1803_poly.nc, + MET_BASE/poly/HMT_masks/huc4_1804_poly.nc, + MET_BASE/poly/HMT_masks/huc4_1805_poly.nc, + MET_BASE/poly/HMT_masks/huc4_1806_poly.nc ENSEMBLE_STAT_CI_ALPHA = 0.05 @@ -172,103 +210,5 @@ ENSEMBLE_STAT_ENSEMBLE_FLAG_WEIGHT = FALSE #ENSEMBLE_STAT_OBS_QUALITY_INC = #ENSEMBLE_STAT_OBS_QUALITY_EXC = -# Ensemble Variables and levels as specified in the ens field dictionary -# of the MET configuration file. Specify as ENS_VARn_NAME, ENS_VARn_LEVELS, -# (optional) ENS_VARn_OPTION -ENS_VAR1_NAME = APCP -ENS_VAR1_LEVELS = A24 -ENS_VAR1_THRESH = >0.0, >=10.0 - -ENS_VAR2_NAME = REFC -ENS_VAR2_LEVELS = L0 -ENS_VAR2_THRESH = >=35.0 - -ENS_VAR2_OPTIONS = GRIB1_ptv = 129; - -ENS_VAR3_NAME = UGRD -ENS_VAR3_LEVELS = Z10 -ENS_VAR3_THRESH = >=5.0 - -ENS_VAR4_NAME = VGRD -ENS_VAR4_LEVELS = Z10 -ENS_VAR4_THRESH = >=5.0 - -ENS_VAR5_NAME = WIND -ENS_VAR5_LEVELS = Z10 -ENS_VAR5_THRESH = >=5.0 - - - -# Forecast Variables and levels as specified in the fcst field dictionary -# of the MET configuration file. Specify as FCST_VARn_NAME, FCST_VARn_LEVELS, -# (optional) FCST_VARn_OPTION -FCST_VAR1_NAME = APCP -FCST_VAR1_LEVELS = A24 - -FCST_VAR1_OPTIONS = ens_ssvar_bin_size = 0.1; ens_phist_bin_size = 0.05; - - -# Observation Variables and levels as specified in the obs field dictionary -# of the MET configuration file. Specify as OBS_VARn_NAME, OBS_VARn_LEVELS, -# (optional) OBS_VARn_OPTION -OBS_VAR1_NAME = {FCST_VAR1_NAME} -OBS_VAR1_LEVELS = {FCST_VAR1_LEVELS} - -OBS_VAR1_OPTIONS = {FCST_VAR1_OPTIONS} - - -[dir] -# Forecast model input directory for ensemble_stat -FCST_ENSEMBLE_STAT_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst - -# Point observation input dir for ensemble_stat -OBS_ENSEMBLE_STAT_POINT_INPUT_DIR = {INPUT_BASE}/met_test/out/ascii2nc - -# Grid observation input dir for ensemble_stat -OBS_ENSEMBLE_STAT_GRID_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_obs/ST4 - -# directory containing climatology mean input to EnsembleStat -# Not used in this example -ENSEMBLE_STAT_CLIMO_MEAN_INPUT_DIR = - -# directory containing climatology mean input to EnsembleStat -# Not used in this example -ENSEMBLE_STAT_CLIMO_STDEV_INPUT_DIR = - -# output directory for ensemble_stat -ENSEMBLE_STAT_OUTPUT_DIR = {OUTPUT_BASE}/ensemble - - -[filename_templates] - -# FCST_ENSEMBLE_STAT_INPUT_TEMPLATE - comma separated list of ensemble members -# or a single line, - filename wildcard characters may be used, ? or *. - -FCST_ENSEMBLE_STAT_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/arw-???-gep?/d01_{init?fmt=%Y%m%d%H}_0{lead?fmt=%HH}00.grib - -# Template to look for point observations. -# Example precip24_2010010112.nc -OBS_ENSEMBLE_STAT_POINT_INPUT_TEMPLATE = precip24_{valid?fmt=%Y%m%d%H}.nc - -# Template to look for gridded observations. -# Example ST4.2010010112.24h -OBS_ENSEMBLE_STAT_GRID_INPUT_TEMPLATE = ST4.{valid?fmt=%Y%m%d%H}.24h - -ENSEMBLE_STAT_VERIFICATION_MASK_TEMPLATE = - MET_BASE/poly/HMT_masks/huc4_1605_poly.nc, - MET_BASE/poly/HMT_masks/huc4_1803_poly.nc, - MET_BASE/poly/HMT_masks/huc4_1804_poly.nc, - MET_BASE/poly/HMT_masks/huc4_1805_poly.nc, - MET_BASE/poly/HMT_masks/huc4_1806_poly.nc - -# Template to look for climatology input to EnsembleStat relative to ENSEMBLE_STAT_CLIMO_MEAN_INPUT_DIR -# Not used in this example -ENSEMBLE_STAT_CLIMO_MEAN_INPUT_TEMPLATE = - -# Template to look for climatology input to EnsembleStat relative to ENSEMBLE_STAT_CLIMO_STDEV_INPUT_DIR -# Not used in this example -ENSEMBLE_STAT_CLIMO_STDEV_INPUT_TEMPLATE = - - -ENSEMBLE_STAT_OUTPUT_TEMPLATE = {init?fmt=%Y%m%d%H%M}/ensemble_stat - +#ENSEMBLE_STAT_ENS_MEMBER_IDS = +#ENSEMBLE_STAT_CONTROL_ID = diff --git a/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf b/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf index b545614bde..0b576567e8 100644 --- a/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf +++ b/parm/use_cases/met_tool_wrapper/GenEnsProd/GenEnsProd.conf @@ -138,3 +138,6 @@ GEN_ENS_PROD_ENS_THRESH = 0.8 # GEN_ENS_PROD_ENSEMBLE_FLAG_NMEP = FALSE # GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO = FALSE # GEN_ENS_PROD_ENSEMBLE_FLAG_CLIMO_CDF = FALSE + +#GEN_ENS_PROD_ENS_MEMBER_IDS = +#GEN_ENS_PROD_CONTROL_ID = From 41add20d016c1d0122cf6d3906402cefb8ee5929 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 14 Jan 2022 16:31:46 -0700 Subject: [PATCH 260/770] added optional argument to change the directory to untar new input data into so the same Dockerfile can be used to add data for other METplus components such as MET --- ci/docker/docker_data/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/docker/docker_data/Dockerfile b/ci/docker/docker_data/Dockerfile index 1f883181c4..b5889ec1fd 100644 --- a/ci/docker/docker_data/Dockerfile +++ b/ci/docker/docker_data/Dockerfile @@ -17,7 +17,8 @@ RUN if [ "x${MOUNTPT}" == "x" ]; then \ exit 1; \ fi -ENV CASE_DIR=/data/input/METplus_Data +ARG DATA_DIR=/data/input/METplus_Data +ENV CASE_DIR=${DATA_DIR} RUN mkdir -p ${CASE_DIR} RUN for URL in `echo ${TARFILE_URL} | tr "," " "`; do \ From 61c5d185fda82af35b066a31f64e1f764114981b Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 14 Jan 2022 16:57:55 -0700 Subject: [PATCH 261/770] feature 1358 v4.1.0-beta5 release (#1359) --- docs/Users_Guide/release-notes.rst | 24 ++++++++++++++++++++++++ metplus/VERSION | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index 00078ee855..b54ec5fd43 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -38,6 +38,30 @@ When applicable, release notes are followed by the GitHub issue number which describes the bugfix, enhancement, or new feature: https://github.com/dtcenter/METplus/issues + +METplus Version 4.1.0-beta5 Release Notes (2022-01-14) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Enhancements: + + * **Add support for setting control members in EnsembleStat and GenEnsProd** (`#1236 `_) + * **Enhance SeriesAnalysis wrapper to allow different field info values for each file in a list** (`#1166 `_) + * Add support for setting INIT_LIST and VALID_LIST for irregular time intervals (`#1286 `_) + * Support setting the OMP_NUM_THREADS environment variable (`#1320 `_) + * Enhance ExtractTiles using MTD input to properly match times (`#1285 `_) + * Add support for commonly changed MET config variables part 2 (`#896 `_) + * Prevent wildcard character from being used in output file path (`#1291 `_) + +* New Use Cases: + + * Satellite verification of sea surface salinity: SMOS vs RTOFS output (`#1116 `_) + +* Internal: + + * **Create guidance for memory-intensive use cases, introduce Python memory profiler** (`#1183 `_) + * **Identify code throughout METplus components that are common utilities** (`#799 `_) + * **Add definitions to the Release Guide for the stages of the release cycle** (`#934 `_) + METplus Version 4.1.0-beta4 Release Notes (2021-11-16) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/metplus/VERSION b/metplus/VERSION index bcc7104f60..ef715f86b9 100644 --- a/metplus/VERSION +++ b/metplus/VERSION @@ -1 +1 @@ -4.1.0-beta5-dev \ No newline at end of file +4.1.0-beta5 \ No newline at end of file From 9aa9058673be7ef234d7b22e482ba65494079e5d Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 14 Jan 2022 17:00:02 -0700 Subject: [PATCH 262/770] update version for next development cycle --- metplus/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metplus/VERSION b/metplus/VERSION index ef715f86b9..ce8a4f6331 100644 --- a/metplus/VERSION +++ b/metplus/VERSION @@ -1 +1 @@ -4.1.0-beta5 \ No newline at end of file +4.1.0-beta6-dev \ No newline at end of file From d8dd6159b4f5eca792d98a56c471f80c1e6dee04 Mon Sep 17 00:00:00 2001 From: j-opatz <59586397+j-opatz@users.noreply.github.com> Date: Wed, 19 Jan 2022 15:58:05 -0700 Subject: [PATCH 263/770] Feature 1216 usecase smap (#1361) * Adding a conf file for SMAP * Adding a directory to host the read file * Removing temp file * Updated the valid dates to match Todd's code * Adding documentation for SMAP case * Updates the valis dates to match Todd's code * Removing a tmp file * Typo in file name * Updating the input RTOFS to have the init time instead of the valid time ii the file name * updated file paths, tesing * updated use case descriptions, rearranged use case group testing * put new use case into its own group so that the diff logic can evaluate marine_and_cryosphere:3. The truth data for 3-4 does not exist yet so the diff fails. Co-authored-by: Mrinal Biswas Co-authored-by: George McCabe <23407799+georgemccabe@users.noreply.github.com> --- .github/parm/use_case_groups.json | 5 + ...GridStat_fcstRTOFS_obsSMAP_climWOA_sss.png | Bin 0 -> 297270 bytes .../GridStat_fcstRTOFS_obsSMAP_climWOA_sss.py | 170 +++++++++ .../GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py | 4 +- internal_tests/use_cases/all_use_cases.txt | 1 + ...ridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf | 267 ++++++++++++++ .../read_rtofs_smap_woa.py | 346 ++++++++++++++++++ 7 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 docs/_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMAP_climWOA_sss.png create mode 100644 docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.py create mode 100644 parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf create mode 100644 parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/read_rtofs_smap_woa.py diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 5413a364c3..39eac94582 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -64,6 +64,11 @@ "index_list": "3", "run": false }, + { + "category": "marine_and_cryosphere", + "index_list": "4", + "run": false + }, { "category": "medium_range", "index_list": "0", diff --git a/docs/_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMAP_climWOA_sss.png b/docs/_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMAP_climWOA_sss.png new file mode 100644 index 0000000000000000000000000000000000000000..823dcfd3d9d0452c1b93f33092d039daade57ae7 GIT binary patch literal 297270 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�dyi;91O&fc~_KbcPlGTH)Bt? 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%0Dc2J~t2AR1I*q~}|zX#sIEy7IX?hnI?pYM(n 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�VRzKFOU&yy*`4BdX44 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

A$E;n_n6XcrF} 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Μ9g96p&k-kXHmX37F?Ddy#LUZSJ8{S&c&}nh3L#J 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 literal 0 HcmV?d00001 diff --git a/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.py b/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.py new file mode 100644 index 0000000000..8b9f5e18c3 --- /dev/null +++ b/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.py @@ -0,0 +1,170 @@ +""" +GridStat: Python Embedding for sea surface salinity using level 3, 8 day mean obs +================================================================================= + +model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf + +""" +############################################################################## +# Scientific Objective +# -------------------- +# +# This use case utilizes Python embedding to extract several statistics from the sea surface salinity data over the globe, +# which was already being done in a closed system. By producing the same output via METplus, this use case +# provides standardization and reproducible results. + +############################################################################## +# Datasets +# -------- +# +# | **Forecast:** RTOFS sss file via Python Embedding script/file +# +# | **Observations:** SMAP sss file via Python Embedding script/file +# +# | **Sea Ice Masking:** RTOFS ice cover file via Python Embedding script/file +# +# | **Climatology:** WOA sss file via Python Embedding script/file +# +# | **Location:** All of the input data required for this use case can be found in the met_test sample data tarball. Click here to the METplus releases page and download sample data for the appropriate release: https://github.com/dtcenter/METplus/releases +# | This tarball should be unpacked into the directory that you will set the value of INPUT_BASE. See `Running METplus`_ section for more information. +# +# | **Data Source:** JPL's PODAAC and NCEP's FTPPRD data servers +# | + +############################################################################## +# External Dependencies +# --------------------- +# +# You will need to use a version of Python 3.6+ that has the following packages installed: +# +# * scikit-learn +# * pyresample +# +# If the version of Python used to compile MET did not have these libraries at the time of compilation, you will need to add these packages or create a new Python environment with these packages. +# +# If this is the case, you will need to set the MET_PYTHON_EXE environment variable to the path of the version of Python you want to use. If you want this version of Python to only apply to this use case, set it in the [user_env_vars] section of a METplus configuration file.: +# +# [user_env_vars] +# MET_PYTHON_EXE = /path/to/python/with/required/packages/bin/python + +############################################################################## +# METplus Components +# ------------------ +# +# This use case utilizes the METplus GridStat wrapper to generate a +# command to run the MET tool GridStat with Python Embedding for the specified user hemispheres + +############################################################################## +# METplus Workflow +# ---------------- +# +# GridStat is the only tool called in this example. This use case will pass in both the observation, forecast, +# and climatology gridded data being pulled from the files via Python Embedding. All of the desired statistics +# reside in the CNT line type, so that is the only output requested. +# It processes the following run time: +# +# | **Valid:** 2021-05-02 0Z +# | + +############################################################################## +# METplus Configuration +# --------------------- +# +# METplus first loads all of the configuration files found in parm/metplus_config, +# then it loads any configuration files passed to METplus via the command line +# with the -c option, i.e. -c parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf + +############################################################################## +# MET Configuration +# --------------------- +# +# METplus sets environment variables based on user settings in the METplus configuration file. +# See :ref:`How METplus controls MET config file settings` for more details. +# +# **YOU SHOULD NOT SET ANY OF THESE ENVIRONMENT VARIABLES YOURSELF! THEY WILL BE OVERWRITTEN BY METPLUS WHEN IT CALLS THE MET TOOLS!** +# +# If there is a setting in the MET configuration file that is currently not supported by METplus you'd like to control, please refer to: +# :ref:`Overriding Unsupported MET config file settings` +# +# .. note:: See the :ref:`GridStat MET Configuration` section of the User's Guide for more information on the environment variables used in the file below: +# +# .. highlight:: bash +# .. literalinclude:: ../../../../parm/met_config/GridStatConfig_wrapped + +############################################################################## +# Python Embedding +# ---------------- +# +# This use case uses one Python script to read forecast and observation data +# +# parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/read_rtofs_smap_woa.py +# +# .. highlight:: python +# .. literalinclude:: ../../../../parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/read_rtofs_smap_woa.py +# + +############################################################################## +# Running METplus +# --------------- +# +# This use case can be run two ways: +# +# 1) Passing in GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf then a user-specific system configuration file:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf -c /path/to/user_system.conf +# +# 2) Modifying the configurations in parm/metplus_config, then passing in GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf:: +# +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf +# +# The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: +# +# * **INPUT_BASE** - Path to directory where sample data tarballs are unpacked (See Datasets section to obtain tarballs). This is not required to run METplus, but it is required to run the examples in parm/use_cases +# * **OUTPUT_BASE** - Path where METplus output will be written. This must be in a location where you have write permissions +# * **MET_INSTALL_DIR** - Path to location where MET is installed locally +# +# Example User Configuration File:: +# +# [dir] +# INPUT_BASE = /path/to/sample/input/data +# OUTPUT_BASE = /path/to/output/dir +# MET_INSTALL_DIR = /path/to/met-X.Y +# +# **NOTE:** All of these items must be found under the [dir] section. +# + +############################################################################## +# Expected Output +# --------------- +# +# A successful run will output the following both to the screen and to the logfile:: +# +# INFO: METplus has successfully finished running. +# +# Refer to the value set for **OUTPUT_BASE** to find where the output data was generated. +# Output for thisIce use case will be found in 20210503 (relative to **OUTPUT_BASE**) +# and will contain the following files: +# +# * grid_stat_SSS_000000L_20210502_000000V.stat +# * grid_stat_SSS_000000L_20210502_000000V_cnt.txt +# * grid_stat_SSS_000000L_20210502_000000V_pairs.nc + +############################################################################## +# Keywords +# -------- +# +# .. note:: +# +# * GridStatToolUseCase +# * PythonEmbeddingFileUseCase +# * MarineAndCryosphereAppUseCase +# +# Navigate to the :ref:`quick-search` page to discover other similar use cases. +# +# +# +# sphinx_gallery_thumbnail_path = '_static/marine_and_cryosphere-GridStat_fcstRTOFS_obsSMAP_climWOA_sss.png' + diff --git a/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py b/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py index 6321b24c2b..1788720dfd 100644 --- a/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py +++ b/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.py @@ -1,6 +1,6 @@ """ -GridStat: Python Embedding to read and process ice cover -======================================================== +GridStat: Python Embedding for sea surface salinity using level 3, 1 day composite obs +====================================================================================== model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index 2582f198e9..e1c9ad8e03 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -90,6 +90,7 @@ Category: marine_and_cryosphere 1::PlotDataPlane_obsHYCOM_coordTripolar::model_applications/marine_and_cryosphere/PlotDataPlane_obsHYCOM_coordTripolar.conf:: xesmf_env, py_embed 2::GridStat_fcstRTOFS_obsOSTIA_iceCover::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsOSTIA_iceCover.conf:: icecover_env, py_embed 3::GridStat_fcstRTOFS_obsSMOS_climWOA_sss::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMOS_climWOA_sss.conf:: icecover_env, py_embed +4::GridStat_fcstRTOFS_obsSMAP_climWOA_sss::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf:: icecover_env, py_embed #X::GridStat_fcstRTOFS_obsGHRSST_climWOA_sst::model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst.conf, model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsGHRSST_climWOA_sst/ci_overrides.conf:: icecover_env, py_embed diff --git a/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf new file mode 100644 index 0000000000..e47cae7aaf --- /dev/null +++ b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss.conf @@ -0,0 +1,267 @@ +# GridStat METplus Configuration + +# section heading for [config] variables - all items below this line and +# before the next section heading correspond to the [config] section +[config] + +# List of applications to run - only GridStat for this case +PROCESS_LIST = GridStat + +# time looping - options are INIT, VALID, RETRO, and REALTIME +# If set to INIT or RETRO: +# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set +# If set to VALID or REALTIME: +# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set +LOOP_BY = VALID + +# Format of INIT_BEG and INT_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +VALID_TIME_FMT = %Y%m%d + +# Start time for METplus run - must match INIT_TIME_FMT +VALID_BEG=20210502 + +# End time for METplus run - must match INIT_TIME_FMT +VALID_END=20210502 + +# Increment between METplus runs (in seconds if no units are specified) +# Must be >= 60 seconds +VALID_INCREMENT = 1M + +# List of forecast leads to process for each run time (init or valid) +# In hours if units are not specified +# If unset, defaults to 0 (don't loop through forecast leads) +LEAD_SEQ = 24 + + +# Order of loops to process data - Options are times, processes +# Not relevant if only one item is in the PROCESS_LIST +# times = run all wrappers in the PROCESS_LIST for a single run time, then +# increment the run time and run all wrappers again until all times have +# been evaluated. +# processes = run the first wrapper in the PROCESS_LIST for all times +# specified, then repeat for the next item in the PROCESS_LIST until all +# wrappers have been run +LOOP_ORDER = times + +# Verbosity of MET output - overrides LOG_VERBOSITY for GridStat only +LOG_GRID_STAT_VERBOSITY = 2 + +# Location of MET config file to pass to GridStat +GRID_STAT_CONFIG_FILE = {PARM_BASE}/met_config/GridStatConfig_wrapped + +# grid to remap data. Value is set as the 'to_grid' variable in the 'regrid' dictionary +# See MET User's Guide for more information +GRID_STAT_REGRID_TO_GRID = NONE + +#GRID_STAT_INTERP_FIELD = +#GRID_STAT_INTERP_VLD_THRESH = +#GRID_STAT_INTERP_SHAPE = +#GRID_STAT_INTERP_TYPE_METHOD = +#GRID_STAT_INTERP_TYPE_WIDTH = + +#GRID_STAT_NC_PAIRS_VAR_NAME = + +#GRID_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = + +#GRID_STAT_GRID_WEIGHT_FLAG = AREA + +# Name to identify model (forecast) data in output +MODEL = RTOFS + +# Name to identify observation data in output +OBTYPE = SMAP + +# set the desc value in the GridStat MET config file +GRID_STAT_DESC = NA + +# List of variables to compare in GridStat - FCST_VAR1 variables correspond +# to OBS_VAR1 variables +# Note [FCST/OBS/BOTH]_GRID_STAT_VAR_NAME can be used instead if different evaluations +# are needed for different tools + +# Name of forecast variable 1 +FCST_VAR1_NAME = {CONFIG_DIR}/read_rtofs_smap_woa.py {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/{init?fmt=%Y%m%d}_rtofs_glo_2ds_f024_prog.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/SMAP-L3-GLOB_{valid?fmt=%Y%m%d?shift=86400}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/OSTIA-UKMO-L4-GLOB-v2.0_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss {valid?fmt=%Y%m%d} fcst + +# List of levels to evaluate for forecast variable 1 +# A03 = 3 hour accumulation in GRIB file +FCST_VAR1_LEVELS = + +# List of thresholds to evaluate for each name/level combination for +# forecast variable 1 +FCST_VAR1_THRESH = + +#FCST_GRID_STAT_FILE_TYPE = + +# Name of observation variable 1 +OBS_VAR1_NAME = {CONFIG_DIR}/read_rtofs_smap_woa.py {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/{init?fmt=%Y%m%d}_rtofs_glo_2ds_f024_prog.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/SMAP-L3-GLOB_{valid?fmt=%Y%m%d?shift=86400}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/OSTIA-UKMO-L4-GLOB-v2.0_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss {valid?fmt=%Y%m%d} obs + + +# List of levels to evaluate for observation variable 1 +# (*,*) is NetCDF notation - must include quotes around these values! +# must be the same length as FCST_VAR1_LEVELS +OBS_VAR1_LEVELS = + +# List of thresholds to evaluate for each name/level combination for +# observation variable 1 +OBS_VAR1_THRESH = + +#GRID_STAT_MET_CONFIG_OVERRIDES = cat_thresh = [>=0.15]; +#BOTH_VAR1_THRESH = >=0.15 + +#OBS_GRID_STAT_FILE_TYPE = + + +# Name of climatology variable 1 +GRID_STAT_CLIMO_MEAN_FIELD = {name="{CONFIG_DIR}/read_rtofs_smap_woa.py {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/{init?fmt=%Y%m%d}_rtofs_glo_2ds_f024_prog.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/SMAP-L3-GLOB_{valid?fmt=%Y%m%d?shift=86400}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/OSTIA-UKMO-L4-GLOB-v2.0_{valid?fmt=%Y%m%d}.nc {INPUT_BASE}/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss {valid?fmt=%Y%m%d} climo"; level="(*,*)";} + + +# Time relative to valid time (in seconds) to allow files to be considered +# valid. Set both BEGIN and END to 0 to require the exact time in the filename +# Not used in this example. +FCST_GRID_STAT_FILE_WINDOW_BEGIN = 0 +FCST_GRID_STAT_FILE_WINDOW_END = 0 +OBS_GRID_STAT_FILE_WINDOW_BEGIN = 0 +OBS_GRID_STAT_FILE_WINDOW_END = 0 + +# MET GridStat neighborhood values +# See the MET User's Guide GridStat section for more information + +# width value passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_WIDTH = 1 + +# shape value passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_SHAPE = SQUARE + +# cov thresh list passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_COV_THRESH = >=0.5 + +# Set to true to run GridStat separately for each field specified +# Set to false to create one run of GridStat per run time that +# includes all fields specified. +GRID_STAT_ONCE_PER_FIELD = False + +# Set to true if forecast data is probabilistic +FCST_IS_PROB = false + +# Only used if FCST_IS_PROB is true - sets probabilistic threshold +FCST_GRID_STAT_PROB_THRESH = ==0.1 + +# Set to true if observation data is probabilistic +# Only used if configuring forecast data as the 'OBS' input +OBS_IS_PROB = false + +# Only used if OBS_IS_PROB is true - sets probabilistic threshold +OBS_GRID_STAT_PROB_THRESH = ==0.1 + +GRID_STAT_OUTPUT_PREFIX = SSS + +#GRID_STAT_CLIMO_MEAN_FILE_NAME = +#GRID_STAT_CLIMO_MEAN_FIELD = +#GRID_STAT_CLIMO_MEAN_REGRID_METHOD = +#GRID_STAT_CLIMO_MEAN_REGRID_WIDTH = +#GRID_STAT_CLIMO_MEAN_REGRID_VLD_THRESH = +#GRID_STAT_CLIMO_MEAN_REGRID_SHAPE = +#GRID_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_MEAN_MATCH_MONTH = +#GRID_STAT_CLIMO_MEAN_DAY_INTERVAL = +#GRID_STAT_CLIMO_MEAN_HOUR_INTERVAL = + +#GRID_STAT_CLIMO_STDEV_FILE_NAME = +#GRID_STAT_CLIMO_STDEV_FIELD = +#GRID_STAT_CLIMO_STDEV_REGRID_METHOD = +#GRID_STAT_CLIMO_STDEV_REGRID_WIDTH = +#GRID_STAT_CLIMO_STDEV_REGRID_VLD_THRESH = +#GRID_STAT_CLIMO_STDEV_REGRID_SHAPE = +#GRID_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_STDEV_MATCH_MONTH = +#GRID_STAT_CLIMO_STDEV_DAY_INTERVAL = +#GRID_STAT_CLIMO_STDEV_HOUR_INTERVAL = + + +#GRID_STAT_CLIMO_CDF_BINS = 1 +#GRID_STAT_CLIMO_CDF_CENTER_BINS = False +#GRID_STAT_CLIMO_CDF_WRITE_BINS = True + +#GRID_STAT_OUTPUT_FLAG_FHO = NONE +#GRID_STAT_OUTPUT_FLAG_CTC = NONE +#GRID_STAT_OUTPUT_FLAG_CTS = NONE +#GRID_STAT_OUTPUT_FLAG_MCTC = NONE +#GRID_STAT_OUTPUT_FLAG_MCTS = NONE +GRID_STAT_OUTPUT_FLAG_CNT = BOTH +#GRID_STAT_OUTPUT_FLAG_SL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_SAL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VAL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VCNT = NONE +#GRID_STAT_OUTPUT_FLAG_PCT = NONE +#GRID_STAT_OUTPUT_FLAG_PSTD = NONE +#GRID_STAT_OUTPUT_FLAG_PJC = NONE +#GRID_STAT_OUTPUT_FLAG_PRC = NONE +#GRID_STAT_OUTPUT_FLAG_ECLV = BOTH +#GRID_STAT_OUTPUT_FLAG_NBRCTC = NONE +#GRID_STAT_OUTPUT_FLAG_NBRCTS = NONE +#GRID_STAT_OUTPUT_FLAG_NBRCNT = NONE +#GRID_STAT_OUTPUT_FLAG_GRAD = BOTH +#GRID_STAT_OUTPUT_FLAG_DMAP = NONE + +#GRID_STAT_NC_PAIRS_FLAG_LATLON = FALSE +#GRID_STAT_NC_PAIRS_FLAG_RAW = FALSE +#GRID_STAT_NC_PAIRS_FLAG_DIFF = FALSE +#GRID_STAT_NC_PAIRS_FLAG_CLIMO = FALSE +#GRID_STAT_NC_PAIRS_FLAG_CLIMO_CDP = FALSE +#GRID_STAT_NC_PAIRS_FLAG_WEIGHT = FALSE +#GRID_STAT_NC_PAIRS_FLAG_NBRHD = FALSE +#GRID_STAT_NC_PAIRS_FLAG_FOURIER = FALSE +#GRID_STAT_NC_PAIRS_FLAG_GRADIENT = FALSE +#GRID_STAT_NC_PAIRS_FLAG_DISTANCE_MAP = FALSE +#GRID_STAT_NC_PAIRS_FLAG_APPLY_MASK = FALSE + + +# End of [config] section and start of [dir] section +[dir] +#use case configuration file directory +CONFIG_DIR = {PARM_BASE}/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss +# directory containing forecast input to GridStat +FCST_GRID_STAT_INPUT_DIR = + +# directory containing observation input to GridStat +OBS_GRID_STAT_INPUT_DIR = + +# directory containing climatology mean input to GridStat +# Not used in this example +GRID_STAT_CLIMO_MEAN_INPUT_DIR = + +# directory containing climatology mean input to GridStat +# Not used in this example +GRID_STAT_CLIMO_STDEV_INPUT_DIR = + +# directory to write output from GridStat +GRID_STAT_OUTPUT_DIR = {OUTPUT_BASE} + +# End of [dir] section and start of [filename_templates] section +[filename_templates] + +# Template to look for forecast input to GridStat relative to FCST_GRID_STAT_INPUT_DIR +FCST_GRID_STAT_INPUT_TEMPLATE = PYTHON_NUMPY + +# Template to look for observation input to GridStat relative to OBS_GRID_STAT_INPUT_DIR +OBS_GRID_STAT_INPUT_TEMPLATE = PYTHON_NUMPY + +# Optional subdirectories relative to GRID_STAT_OUTPUT_DIR to write output from GridStat +GRID_STAT_OUTPUT_TEMPLATE = {valid?fmt=%Y%m%d} + +# Template to look for climatology input to GridStat relative to GRID_STAT_CLIMO_MEAN_INPUT_DIR +# Not used in this example +GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE = PYTHON_NUMPY + +# Template to look for climatology input to GridStat relative to GRID_STAT_CLIMO_STDEV_INPUT_DIR +# Not used in this exampls +GRID_STAT_CLIMO_STDEV_INPUT_TEMPLATE = + +# Used to specify one or more verification mask files for GridStat +# Not used for this example +GRID_STAT_VERIFICATION_MASK_TEMPLATE = diff --git a/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/read_rtofs_smap_woa.py b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/read_rtofs_smap_woa.py new file mode 100644 index 0000000000..b5a0c3f19d --- /dev/null +++ b/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_fcstRTOFS_obsSMAP_climWOA_sss/read_rtofs_smap_woa.py @@ -0,0 +1,346 @@ +#!/bin/env python +""" +Code adapted from +Todd Spindler +NOAA/NWS/NCEP/EMC +Designed to read in RTOFS,SMAP,WOA and OSTIA data +and based on user input, read sss data +and pass back in memory the forecast, observation, or climatology +data field +""" + +import numpy as np +import xarray as xr +import pandas as pd +import pyresample as pyr +from pandas.tseries.offsets import DateOffset +from datetime import datetime, timedelta +from sklearn.metrics import mean_squared_error +import io +from glob import glob +import warnings +import os, sys + + +if len(sys.argv) < 6: + print("Must specify the following elements: fcst_file obs_file ice_file, climo_file, valid_date, file_flag") + sys.exit(1) + +rtofsfile = os.path.expandvars(sys.argv[1]) +sssfile = os.path.expandvars(sys.argv[2]) +icefile = os.path.expandvars(sys.argv[3]) +climoDir = os.path.expandvars(sys.argv[4]) +vDate=datetime.strptime(sys.argv[5],'%Y%m%d') +file_flag = sys.argv[6] + +print('Starting Satellite SMAP V&V at',datetime.now(),'for',vDate, ' file_flag:',file_flag) + +pd.date_range(vDate,vDate) +platform='SMAP' +param='sss' + + +##################################################################### +# READ SMAP data ################################################## +##################################################################### + +if not os.path.exists(sssfile): + print('missing SMAP file for',vDate) + +sss_data=xr.open_dataset(sssfile,decode_times=True) +sss_data['time']=sss_data.time-pd.Timedelta('12H') # shift 12Z offset time to 00Z +sss_data2=sss_data['sss'].astype('single') +print('Retrieved SMAP data from NESDIS for',sss_data2.time.values) +#sss_data2=sss_data2.rename({'longitude':'lon','latitude':'lat'}) + + +# all coords need to be single precision +sss_data2['lon']=sss_data2.lon.astype('single') +sss_data2['lat']=sss_data2.lat.astype('single') +sss_data2.attrs['platform']=platform +sss_data2.attrs['units']='PSU' + +##################################################################### +# READ RTOFS data (model output in Tri-polar coordinates) ########### +##################################################################### + +print('reading rtofs ice') +if not os.path.exists(rtofsfile): + print('missing rtofs file',rtofsfile) + sys.exit(1) + +indata=xr.open_dataset(rtofsfile,decode_times=True) + + +indata=indata.mean(dim='MT') +indata = indata[param][:-1,] +indata.coords['time']=vDate +#indata.coords['fcst']=fcst + +outdata=indata.copy() + +outdata=outdata.rename({'Longitude':'lon','Latitude':'lat',}) +# all coords need to be single precision +outdata['lon']=outdata.lon.astype('single') +outdata['lat']=outdata.lat.astype('single') +outdata.attrs['platform']='rtofs '+platform + +##################################################################### +# READ CLIMO WOA data - May require 2 files depending on the date ### +##################################################################### + +if not os.path.exists(climoDir): + print('missing climo file file for',vDate) + +vDate=pd.Timestamp(vDate) + +climofile="woa13_decav_s{:02n}_04v2.nc".format(vDate.month) +climo_data=xr.open_dataset(climoDir+'/'+climofile,decode_times=False) +climo_data=climo_data['s_an'].squeeze()[0,] + +if vDate.day==15: # even for Feb, just because + climofile="woa13_decav_s{:02n}_04v2.nc".format(vDate.month) + climo_data=xr.open_dataset(climoDir+'/'+climofile,decode_times=False) + climo_data=climo_data['s_an'].squeeze()[0,] # surface only +else: + if vDate.day < 15: + start=vDate - DateOffset(months=1,day=15) + stop=pd.Timestamp(vDate.year,vDate.month,15) + else: + start=pd.Timestamp(vDate.year,vDate.month,15) + stop=vDate + DateOffset(months=1,day=15) + left=(vDate-start)/(stop-start) + + climofile1="woa13_decav_s{:02n}_04v2.nc".format(start.month) + climofile2="woa13_decav_s{:02n}_04v2.nc".format(stop.month) + climo_data1=xr.open_dataset(climoDir+'/'+climofile1,decode_times=False) + climo_data2=xr.open_dataset(climoDir+'/'+climofile2,decode_times=False) + climo_data1=climo_data1['s_an'].squeeze()[0,] # surface only + climo_data2=climo_data2['s_an'].squeeze()[0,] # surface only + + print('climofile1 :', climofile1) + print('climofile2 :', climofile2) + climo_data=climo_data1+((climo_data2-climo_data1)*left) + climofile='weighted average of '+climofile1+' and '+climofile2 + +# all coords need to be single precision +climo_data['lon']=climo_data.lon.astype('single') +climo_data['lat']=climo_data.lat.astype('single') +climo_data.attrs['platform']='woa' +climo_data.attrs['filename']=climofile + +##################################################################### +# READ ICE data for masking ######################################### +##################################################################### + +if not os.path.exists(icefile): + print('missing OSTIA ice file for',vDate) + +ice_data=xr.open_dataset(icefile,decode_times=True) +ice_data=ice_data.rename({'sea_ice_fraction':'ice'}) + +# all coords need to be single precision +ice_data2=ice_data.ice.astype('single') +ice_data2['lon']=ice_data2.lon.astype('single') +ice_data2['lat']=ice_data2.lat.astype('single') + + +def regrid(model,obs): + """ + regrid data to obs -- this assumes DataArrays + """ + model2=model.copy() + model2_lon=model2.lon.values + model2_lat=model2.lat.values + model2_data=model2.to_masked_array() + if model2_lon.ndim==1: + model2_lon,model2_lat=np.meshgrid(model2_lon,model2_lat) + + obs2=obs.copy() + obs2_lon=obs2.lon.astype('single').values + obs2_lat=obs2.lat.astype('single').values + obs2_data=obs2.astype('single').to_masked_array() + if obs2.lon.ndim==1: + obs2_lon,obs2_lat=np.meshgrid(obs2.lon.values,obs2.lat.values) + + model2_lon1=pyr.utils.wrap_longitudes(model2_lon) + model2_lat1=model2_lat.copy() + obs2_lon1=pyr.utils.wrap_longitudes(obs2_lon) + obs2_lat1=obs2_lat.copy() + + # pyresample gausssian-weighted kd-tree interp + # define the grids + orig_def = pyr.geometry.GridDefinition(lons=model2_lon1,lats=model2_lat1) + targ_def = pyr.geometry.GridDefinition(lons=obs2_lon1,lats=obs2_lat1) + radius=50000 + sigmas=25000 + model2_data2=pyr.kd_tree.resample_gauss(orig_def,model2_data,targ_def, + radius_of_influence=radius, + sigmas=sigmas, + fill_value=None) + model=xr.DataArray(model2_data2,coords=[obs.lat.values,obs.lon.values],dims=['lat','lon']) + + return model + +def expand_grid(data): + """ + concatenate global data for edge wraps + """ + + data2=data.copy() + data2['lon']=data2.lon+360 + data3=xr.concat((data,data2),dim='lon') + return data3 + +sss_data2=sss_data2.squeeze() + +print('regridding climo to obs') +climo_data=climo_data.squeeze() +climo_data=regrid(climo_data,sss_data2) + +print('regridding ice to obs') +ice_data2=regrid(ice_data2,sss_data2) + +print('regridding model to obs') +model2=regrid(outdata,sss_data2) + +# combine obs ice mask with ncep +obs2=sss_data2.to_masked_array() +ice2=ice_data2.to_masked_array() +climo2=climo_data.to_masked_array() +model2=model2.to_masked_array() + +#reconcile with obs +obs2.mask=np.ma.mask_or(obs2.mask,ice2>0.0) +obs2.mask=np.ma.mask_or(obs2.mask,climo2.mask) +obs2.mask=np.ma.mask_or(obs2.mask,model2.mask) +climo2.mask=obs2.mask +model2.mask=obs2.mask + +obs2=xr.DataArray(obs2,coords=[sss_data2.lat.values,sss_data2.lon.values], dims=['lat','lon']) +model2=xr.DataArray(model2,coords=[sss_data2.lat.values,sss_data2.lon.values], dims=['lat','lon']) +climo2=xr.DataArray(climo2,coords=[sss_data2.lat.values,sss_data2.lon.values], dims=['lat','lon']) + +model2=expand_grid(model2) +climo2=expand_grid(climo2) +obs2=expand_grid(obs2) + +#Create the MET grids based on the file_flag +if file_flag == 'fcst': + met_data = model2[:,:] + #trim the lat/lon grids so they match the data fields + lat_met = model2.lat + lon_met = model2.lon + print(" RTOFS Data shape: "+repr(met_data.shape)) + v_str = vDate.strftime("%Y%m%d") + v_str = v_str + '_000000' + lat_ll = float(lat_met.min()) + lon_ll = float(lon_met.min()) + n_lat = lat_met.shape[0] + n_lon = lon_met.shape[0] + delta_lat = (float(lat_met.max()) - float(lat_met.min()))/float(n_lat) + delta_lon = (float(lon_met.max()) - float(lon_met.min()))/float(n_lon) + print(f"variables:" + f"lat_ll: {lat_ll} lon_ll: {lon_ll} n_lat: {n_lat} n_lon: {n_lon} delta_lat: {delta_lat} delta_lon: {delta_lon}") + met_data.attrs = { + 'valid': v_str, + 'init': v_str, + 'lead': "00", + 'accum': "00", + 'name': 'sss', + 'standard_name': 'sea_surface_salinity', + 'long_name': 'sea_surface_salinity', + 'level': "SURFACE", + 'units': "psu", + + 'grid': { + 'type': "LatLon", + 'name': "RTOFS Grid", + 'lat_ll': lat_ll, + 'lon_ll': lon_ll, + 'delta_lat': delta_lat, + 'delta_lon': delta_lon, + 'Nlat': n_lat, + 'Nlon': n_lon, + } + } + attrs = met_data.attrs + +if file_flag == 'obs': + met_data = obs2[:,:] + #trim the lat/lon grids so they match the data fields + lat_met = obs2.lat + lon_met = obs2.lon + v_str = vDate.strftime("%Y%m%d") + v_str = v_str + '_000000' + lat_ll = float(lat_met.min()) + lon_ll = float(lon_met.min()) + n_lat = lat_met.shape[0] + n_lon = lon_met.shape[0] + delta_lat = (float(lat_met.max()) - float(lat_met.min()))/float(n_lat) + delta_lon = (float(lon_met.max()) - float(lon_met.min()))/float(n_lon) + print(f"variables:" + f"lat_ll: {lat_ll} lon_ll: {lon_ll} n_lat: {n_lat} n_lon: {n_lon} delta_lat: {delta_lat} delta_lon: {delta_lon}") + met_data.attrs = { + 'valid': v_str, + 'init': v_str, + 'lead': "00", + 'accum': "00", + 'name': 'sss', + 'standard_name': 'analyzed sea surface salinity', + 'long_name': 'sea_surface_salinity', + 'level': "SURFACE", + 'units': "psu", + + 'grid': { + 'type': "LatLon", + 'name': "Lat Lon", + 'lat_ll': lat_ll, + 'lon_ll': lon_ll, + 'delta_lat': delta_lat, + 'delta_lon': delta_lon, + 'Nlat': n_lat, + 'Nlon': n_lon, + } + } + attrs = met_data.attrs + +if file_flag == 'climo': + met_data = climo2[:,:] + #modify the lat and lon grids since they need to match the data dimensions, and code cuts the last row/column of data + lat_met = climo2.lat + lon_met = climo2.lon + v_str = vDate.strftime("%Y%m%d") + v_str = v_str + '_000000' + lat_ll = float(lat_met.min()) + lon_ll = float(lon_met.min()) + n_lat = lat_met.shape[0] + n_lon = lon_met.shape[0] + delta_lat = (float(lat_met.max()) - float(lat_met.min()))/float(n_lat) + delta_lon = (float(lon_met.max()) - float(lon_met.min()))/float(n_lon) + print(f"variables:" + f"lat_ll: {lat_ll} lon_ll: {lon_ll} n_lat: {n_lat} n_lon: {n_lon} delta_lat: {delta_lat} delta_lon: {delta_lon}") + met_data.attrs = { + 'valid': v_str, + 'init': v_str, + 'lead': "00", + 'accum': "00", + 'name': 'sea_water_salinity', + 'standard_name': 'sea_water_salinity', + 'long_name': 'sea_water_salinity', + 'level': "SURFACE", + 'units': "psu", + + 'grid': { + 'type': "LatLon", + 'name': "crs Grid", + 'lat_ll': lat_ll, + 'lon_ll': lon_ll, + 'delta_lat': delta_lat, + 'delta_lon': delta_lon, + 'Nlat': n_lat, + 'Nlon': n_lon, + } + } + attrs = met_data.attrs + From 50e54ccb93d1c580ff00fe8fc373f186bc245301 Mon Sep 17 00:00:00 2001 From: j-opatz Date: Wed, 19 Jan 2022 16:00:45 -0700 Subject: [PATCH 264/770] updated marine_and_cryo grouping --- .github/parm/use_case_groups.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 39eac94582..96f44cd91c 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -61,12 +61,7 @@ }, { "category": "marine_and_cryosphere", - "index_list": "3", - "run": false - }, - { - "category": "marine_and_cryosphere", - "index_list": "4", + "index_list": "3-4", "run": false }, { From 77d954dfd705def5f992f0310428852b575f09d6 Mon Sep 17 00:00:00 2001 From: Hank Fisher Date: Fri, 21 Jan 2022 12:06:27 -0700 Subject: [PATCH 265/770] Feature 1230 stratosphere metrics (#1354) * Initial checkin for Meridial Mean use case * Changed the name and directories * Added some documentation * Fixed config variables * Issue 1230 Stratospheric metrics (zonal/meridional use case) put INPUT_BASE in the user_env_vars so the user doesn't need to set the INPUT_BASE environment in the current working shell * Issue #1230 remove import of metplotpy-this isn't called * Issue #1230_stratosphere_metric Add use case to the list of use cases to be run for testing * Issue 1230 Remove the INPUT_BASE from the user_env_vars section * Issue #1230 redundant files * Issue #1230 redundant files * Issue #1230 redundant files * Issue #120 replace INPUT_BASE with INPUT_FILE_NAME * Issue #1230 remove entry for INPUT_FILE_NAME, this goes in the system.conf * Update all_use_cases.txt Copy and pasted #11 from s2s use case for the Stratosphere use case but forgot to update the index to 12. * Update use_case_groups.json added use case #12 from s2s to test stratosphere use case * Issue #1230 put INPUT_FILE_NAME back under the user_env_vars * Issue #1230 forgot to include the filename * Issue #1230 clean up config file, remove uneccessary comments, group related entries in the user_env_vars * Issue #1230 remove extraneous and incorrect path to the input_filename setting * Removed pingouin dependency * Issue #1230 another cut and paste error fixed for the Stratosphere use case * Issue #1230 type in name, obs_Only should be obsOnly * Issue #1230 added the metdatadb to the env, code imports metdatadb and may require some of these dependencies * issue #1230 removed pingouin dependency from comment to reduce any confusion * issue #1230 turn off the test for the Stratosphere metrics use case * Added use case image Co-authored-by: Hank Fisher Co-authored-by: Minna Win Co-authored-by: bikegeek <3753118+bikegeek@users.noreply.github.com> Co-authored-by: Christina Kalb --- .github/parm/use_case_groups.json | 5 + ci/docker/docker_env/scripts/metplotpy_env.sh | 2 - docs/_static/s2s-zonal_means.png | Bin 0 -> 26841 bytes .../UserScript_obsERA_obsOnly_Stratosphere.py | 137 ++++++++++++++++++ internal_tests/use_cases/all_use_cases.txt | 1 + ...serScript_obsERA_obsOnly_Stratosphere.conf | 56 +++++++ .../README | 9 ++ .../meridonial_mean.py | 82 +++++++++++ .../meridonial_mean.yaml | 2 + 9 files changed, 292 insertions(+), 2 deletions(-) create mode 100755 docs/_static/s2s-zonal_means.png create mode 100644 docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.py create mode 100644 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf create mode 100644 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/README create mode 100755 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.py create mode 100644 parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.yaml diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 96f44cd91c..85959b3fb9 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -154,6 +154,11 @@ "index_list": "11", "run": false }, + { + "category": "s2s", + "index_list": "12", + "run": false + }, { "category": "space_weather", "index_list": "0-1", diff --git a/ci/docker/docker_env/scripts/metplotpy_env.sh b/ci/docker/docker_env/scripts/metplotpy_env.sh index 1db44769ee..914c1b074b 100755 --- a/ci/docker/docker_env/scripts/metplotpy_env.sh +++ b/ci/docker/docker_env/scripts/metplotpy_env.sh @@ -10,7 +10,6 @@ # matplotlib==3.3.0 # scipy==1.5.1 # plotly==4.9.0 -# pingouin==0.3.8 # cartopy==0.18.0 # eofs==1.3.0 # cmocean==2.0 @@ -34,7 +33,6 @@ conda create -y --clone ${BASE_ENV} --name ${ENV_NAME} conda install -y --name ${ENV_NAME} -c conda-forge matplotlib==3.3.0 conda install -y --name ${ENV_NAME} -c conda-forge scipy==1.5.1 conda install -y --name ${ENV_NAME} -c conda-forge plotly==4.9.0 -conda install -y --name ${ENV_NAME} -c conda-forge pingouin==0.3.8 conda install -y --name ${ENV_NAME} -c conda-forge cartopy==0.18.0 conda install -y --name ${ENV_NAME} -c conda-forge eofs==1.3.0 conda install -y --name ${ENV_NAME} -c conda-forge cmocean==2.0 diff --git a/docs/_static/s2s-zonal_means.png b/docs/_static/s2s-zonal_means.png new file mode 100755 index 0000000000000000000000000000000000000000..3e5ab96dc491306227d6acb7dc71062a5734858b GIT binary patch literal 26841 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

6iwz1ux-INc2FaXv}R>^It)Q&Pnu93|+ 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 d@Tg9&*e)xucMQ6/METdatadb:/METcalcpy + +The file SSWC_v1.0_varFull_ERAi_d20130106_s20121107_e20130307_c20160701.nc needs to be +on disk somewhere on your computer and referenced correctly in the file +meridial_mean.yaml diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.py new file mode 100755 index 0000000000..777be36ea6 --- /dev/null +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +""" +Create meridonial mean statistics + +""" +import os +import sys +import logging +import yaml +import xarray as xr # http://xarray.pydata.org/ +import metcalcpy.util.read_env_vars_in_config as readconfig +import metcalcpy.pre_processing.directional_means as directional_means +import METreadnc.util.read_netcdf as read_netcdf + + +def main(): + """ + Use existing default meridonial mean config file found in METcalcpy to + grab the test file + """ + + + """ + Read Meridial Mean YAML configuration file + user can use their own, if none specified at the command line, + use the "default" example YAML config file, spectra_plot_coh2.py + Using a custom YAML reader so we can use environment variables + """ + + try: + input_config_file = os.getenv("YAML_CONFIG_NAME","meridonial_mean.yaml") + config = readconfig.parse_config(input_config_file) + logging.info(config) + except yaml.YAMLError as exc: + logging.error(exc) + + """ + Read METplus config file paramaters + """ + #input_file_name = os.environ.get("INPUT_FILE_NAME","SSWC_v1.0_varFull_ERAi_d20130106_s20121107_e20130307_c20160701.nc") + input_file = config["input_filename"] + + """ + Setup logging + """ + logfile = "meridonial_mean.log" + logging_level = os.environ.get("LOG_LEVEL","logging.INFO") + logging.basicConfig(stream=logfile, level=logging_level) + + """ + Read dataset + """ + try: + logging.info('Opening ' + input_file[0]) + file_reader = read_netcdf.ReadNetCDF() + + #file_reader returns a list of xarrays even if there is only one file requested to be read + #so we change it from a list to a single + ds = file_reader.read_into_xarray(input_file)[0] + except IOError as exc: + logging.error('Unable to open ' + input_file) + logging.error(exc) + sys.exit(1) + logging.debug(ds) + ds = ds[['uwndFull_TS','vwndFull_TS','tempFull_TS','geopFull_TS']] + ds = ds.rename({'timeEv60':'time', + 'lat':'latitude', # pyzome currently expects dimensions named latitude and longitude + 'lon':'longitude', + 'uwndFull_TS':'u', + 'vwndFull_TS':'v', + 'tempFull_TS':'T', + 'geopFull_TS':'Z'}) + + uzm = directional_means.zonal_mean(ds.u) + Tzm = directional_means.zonal_mean(ds.T) + T_6090 = directional_means.meridional_mean(Tzm, 60, 90) + + print(T_6090) + +if __name__ == '__main__': + main() diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.yaml b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.yaml new file mode 100644 index 0000000000..6cadc5160b --- /dev/null +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere/meridonial_mean.yaml @@ -0,0 +1,2 @@ +input_filename: +- !ENV '${INPUT_FILE_NAME}' From e4805a9cfffa0852b03224984f1405cd280747fa Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Mon, 24 Jan 2022 14:27:24 -0700 Subject: [PATCH 266/770] Corrected spelling of occurrence in two places --- docs/Users_Guide/systemconfiguration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Users_Guide/systemconfiguration.rst b/docs/Users_Guide/systemconfiguration.rst index 3f97b5be84..044fb505de 100644 --- a/docs/Users_Guide/systemconfiguration.rst +++ b/docs/Users_Guide/systemconfiguration.rst @@ -1116,9 +1116,9 @@ no space between the process name and the parenthesis. [my_instance_name] GRID_STAT_OUTPUT_DIR = /my/instance/name/output/dir -In this example, the first occurence of GridStat in the PROCESS_LIST does +In this example, the first occurrence of GridStat in the PROCESS_LIST does not have an instance name associated with it, so it will use the value -/grid/stat/output/dir as the output directory. The second occurence has +/grid/stat/output/dir as the output directory. The second occurrence has an instance name 'my_instance_name' and there is a section header with the same name, so this instance will use /my/instance/name/output/dir as the output directory. From 85e882c84dfc06fca3fd418f3c8ea5d033fd029b Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Mon, 24 Jan 2022 15:07:11 -0700 Subject: [PATCH 267/770] Fixed misspelling of occurrence --- metplus/util/string_template_substitution.py | 2 +- .../UserScript_obsERA_obsOnly_WeatherRegime/WeatherRegime.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metplus/util/string_template_substitution.py b/metplus/util/string_template_substitution.py index 7aeb709942..d389075e88 100644 --- a/metplus/util/string_template_substitution.py +++ b/metplus/util/string_template_substitution.py @@ -330,7 +330,7 @@ def do_string_sub(tmpl, defined in the call to this function. @param kwargs any additional arguments that are passed to the function. These will be used to replace values in the template. For example, if - my_arg=my_value is passed to the function, any occurence of {my_arg} + my_arg=my_value is passed to the function, any occurrence of {my_arg} in the template will be substituted with 'my_value'. @returns template with tags substituted with values """ diff --git a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime/WeatherRegime.py b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime/WeatherRegime.py index e7c338d6c6..98ae695988 100644 --- a/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime/WeatherRegime.py +++ b/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime/WeatherRegime.py @@ -147,7 +147,7 @@ def run_K_means(self,a1,timedict,arr_shape): yf = np.reshape(y,[self.wrnum,arr_shape[arrdims-2],arr_shape[arrdims-1]]) # reshape cluster anomalies to latlon - #Get frequency of occurence for each cluster + #Get frequency of occurrence for each cluster perc=np.zeros(self.wrnum) for ii in np.arange(0,self.wrnum,1): perc[ii] = self.get_cluster_fraction(f,ii) From 069572b5f4f86816c9883cab4efca000fee365ff Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Mon, 24 Jan 2022 15:15:56 -0700 Subject: [PATCH 268/770] Found and fixed two more misspellings of occurence --- .../s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py | 2 +- .../s2s/UserScript_obsERA_obsOnly_WeatherRegime.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py index fbc6a76928..ddad051554 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py +++ b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py @@ -20,7 +20,7 @@ # clusters. This helps determine the optimal cluster number by examining the largest # difference between the curve and the straight line. The EOFs step is optional. It # computes an empirical orthogonal function analysis. The K means step uses clustering -# to compute the frequency of occurrernce and anomalies for each cluster to give the most +# to compute the frequency of occurrence and anomalies for each cluster to give the most # common weather regimes. Then, the time frequency computes the frequency of each weather # regime over a user specified time frame. Finally, stat_analysis can be run to compute # an categorical analysis of the weather regime classification or an anomaly correlation of diff --git a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime.py b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime.py index f196e84988..3e6f896957 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime.py +++ b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_WeatherRegime.py @@ -20,7 +20,7 @@ # clusters. This helps determine the optimal cluster number by examining the largest # difference between the curve and the straight line. The EOFs step is optional. It # computes an empirical orthogonal function analysis. The K means step uses clustering -# to compute the frequency of occurrernce and anomalies for each cluster to give the most +# to compute the frequency of occurrence and anomalies for each cluster to give the most # common weather regimes. Then, the time frequency computes the frequency of each weather # regime over a user specified time frame. Finally, stat_analysis can be run to compute # an categorical analysis of the weather regime classification or an anomaly correlation of From 473fcab78500e5b5dfca2c19f6b075639fea514b Mon Sep 17 00:00:00 2001 From: Julie Prestopnik Date: Tue, 25 Jan 2022 12:00:56 -0700 Subject: [PATCH 269/770] Changed version specific information to by X.Y.Z moved text indicating to click save to the bottom of the list. --- .../release_steps/common/update_dtc_website.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/Release_Guide/release_steps/common/update_dtc_website.rst b/docs/Release_Guide/release_steps/common/update_dtc_website.rst index 22e37d90fe..a26934aec5 100644 --- a/docs/Release_Guide/release_steps/common/update_dtc_website.rst +++ b/docs/Release_Guide/release_steps/common/update_dtc_website.rst @@ -28,14 +28,14 @@ Update DTC Website * Add Link: Link text should be "User's Guide" and the URL should be the top level directory of the User's Guide hosted on the web. Beta releases can use "develop" in the URL, but for official releases, please ensure the - link uses the branch name (e.g. main_v4.0) as opposed to the tag name - (e.g. v4.0.0). For example, use - "https://metplus.readthedocs.io/en/main_v4.0/Users_Guide/" and NOT - "https://metplus.readthedocs.io/en/v4.0.0/Users_Guide/" + link uses the branch name (e.g. main_vX.Y) as opposed to the tag name + (e.g. vX.Y.Z). For example, use + "https://metplus.readthedocs.io/en/main_vX.Y/Users_Guide/" and NOT + "https://metplus.readthedocs.io/en/vX.Y.Z/Users_Guide/" * Add Link: Link text should be "Existing Builds and Docker" and the URL should be the latest Existing Builds page, i.e. - https://dtcenter.org/community-code/metplus/metplus-4-0-existing-builds + https://dtcenter.org/community-code/metplus/metplus-X-Y-existing-builds (If creating a new official release, be sure to add a new *Existing Builds and Docker* page, if one was not already created.) @@ -44,8 +44,6 @@ Update DTC Website * Click on "Create Release". - * Click on "Save". - * Update the existing releases, as needed. * For a development release, change any previous *Development* @@ -58,3 +56,6 @@ Update DTC Website releases. * |otherWebsiteUpdates| + + * Click on "Save". + From baacb56291dd87d92f2d9752d6207a1110865457 Mon Sep 17 00:00:00 2001 From: jprestop Date: Wed, 26 Jan 2022 12:32:12 -0700 Subject: [PATCH 270/770] Feature 1374 python packages (#1378) * Added documentation about updating spreadsheet of Python requirements * Changed references to master_metplus.py to run_metplus.py * Fixed formatting of section with run_metplus.py commands * Updated text for updating the spreadsheet * Removed a newly added section that wasn't needed Co-authored-by: Julie Prestopnik --- docs/Contributors_Guide/add_use_case.rst | 23 ++++++++++++++----- .../MODE/MODE_python_embedding.py | 4 ++-- .../TCPairs/TCPairs_tropical.py | 1 + .../GridStat_MODE_fcstIMS_obsNCEP_sea_ice.py | 1 + .../s2s/UserScript_fcstGFS_obsERA_Blocking.py | 4 ++-- ...UserScript_fcstGFS_obsERA_WeatherRegime.py | 4 ++-- .../s2s/UserScript_obsERA_obsOnly_Blocking.py | 4 ++-- .../UserScript_obsERA_obsOnly_Stratosphere.py | 7 ++---- ...ript_obsPrecip_obsOnly_CrossSpectraPlot.py | 7 ++---- 9 files changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/Contributors_Guide/add_use_case.rst b/docs/Contributors_Guide/add_use_case.rst index eb4e07ad2c..fe01f46ca0 100644 --- a/docs/Contributors_Guide/add_use_case.rst +++ b/docs/Contributors_Guide/add_use_case.rst @@ -203,21 +203,32 @@ use case OR category directory for a model_applications use case * Users are encouraged to copy an existing documentation file and modify it to describe the new use case. - * Update any references to the .conf file to use the correct name + * Update any references to the .conf file to use the correct name. - * Update the Scientific Objective section to describe the use case + * Update the Scientific Objective section to describe the use case. - * Update the description of the input data in the Datasets section + * Update the description of the input data in the Datasets section. - * Update the list of tools used in the METplus Components section + * Update the list of External Dependencies (if applicable) to include any + required Python packages. Update the + `METplus Components Python Requirements `_ + spreadsheet. If the package is already listed in the spreadsheet, add + a link to the documentation page for this new use case, following the + format in the spreadsheet. If the package is not already listed, update + the spreadsheet to include the name of the required package, the version, + the METplus component (e.g. METplus wrappers, METcalcpy, METplotpy), the + source, a brief description, and a link to this new use case that uses + this new Python package. + + * Update the list of tools used in the METplus Components section. - * Update the list of run times in the METplus Workflow section + * Update the list of run times in the METplus Workflow section. * Update the list of keywords, referring to :ref:`quick-search` for a list of possible keywords to use (Note: The link text for the keywords must match the actual keyword exactly or it will not show up in the search, i.e. **ASCII2NCToolUseCase** must match - https://metplus.readthedocs.io/en/latest/search.html?q=**ASCII2NCToolUseCase** + https://metplus.readthedocs.io/en/latest/search.html?q=**ASCII2NCToolUseCase**. * Add an image to use as the thumbnail (if desired). Images can be added to the docs/_static directory and should be named -.png diff --git a/docs/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.py b/docs/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.py index 957b99b679..3138e02293 100644 --- a/docs/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.py +++ b/docs/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.py @@ -83,11 +83,11 @@ # # 1) Passing in MODE_python_embedding.conf then a user-specific system configuration file:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.conf -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.conf -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in MODE_python_embedding.conf:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/met_tool_wrapper/MODE/MODE_python_embedding.conf # # The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: # diff --git a/docs/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.py b/docs/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.py index 07de6b52f4..9a9e174b61 100644 --- a/docs/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.py +++ b/docs/use_cases/met_tool_wrapper/TCPairs/TCPairs_tropical.py @@ -85,6 +85,7 @@ # run_metplus.py -c /path/to/TCPairs_tropical.conf -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in TCPairs_tropical.conf:: +# # run_metplus.py -c /path/to/TCPairs_tropical.conf # # The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following METplus configuration variables must be set correctly to run this example.: diff --git a/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.py b/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.py index 134cd15f85..e6f2b6ab2d 100644 --- a/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.py +++ b/docs/use_cases/model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.py @@ -105,6 +105,7 @@ # run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.conf -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in GridStat_MODE_fcstIMS_obsNCEP_sea_ice.conf:: +# # run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/marine_and_cryosphere/GridStat_MODE_fcstIMS_obsNCEP_sea_ice.conf # # The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: diff --git a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py index 722e9d2969..89d2f00cef 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py +++ b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py @@ -140,11 +140,11 @@ # # 1) Passing in UserScript_fcstGFS_obsERA_Blocking.py then a user-specific system configuration file:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_fcstGFS_obsERA_Blocking.py:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_Blocking.py # # The following variables must be set correctly: # diff --git a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py index ddad051554..5adeaad9a3 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py +++ b/docs/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py @@ -148,11 +148,11 @@ # # 1) Passing in UserScript_fcstGFS_obsERA_WeatherRegime.py then a user-specific system configuration file:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_fcstGFS_obsERA_WeatherRegime.py:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.py # # The following variables must be set correctly: # diff --git a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py index 3ff0464e3c..12f604b6b2 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py +++ b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py @@ -137,11 +137,11 @@ # # 1) Passing in UserScript_obsERA_obsOnly_Blocking.py then a user-specific system configuration file:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_obsERA_obsOnly_Blocking.py:: # -# master_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Blocking.py # # The following variables must be set correctly: # diff --git a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.py b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.py index 3fd01a05c2..eac201a110 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.py +++ b/docs/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.py @@ -76,14 +76,11 @@ # 1) Passing in meridonial_means.conf, # then a user-specific system configuration file:: # -# run_metplus.py \ -# -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf \ -# -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in meridonial.conf:: # -# run_metplus.py \ -# -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf # # The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: # diff --git a/docs/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.py b/docs/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.py index cf64fbbd18..215ce24346 100644 --- a/docs/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.py +++ b/docs/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.py @@ -88,14 +88,11 @@ # 1) Passing in UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf, # then a user-specific system configuration file:: # -# run_metplus.py \ -# -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf \ -# -c /path/to/user_system.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf -c /path/to/user_system.conf # # 2) Modifying the configurations in parm/metplus_config, then passing in UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf:: # -# run_metplus.py \ -# -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf +# run_metplus.py -c /path/to/METplus/parm/use_cases/model_applications/s2s/UserScript_obsPrecip_obsOnly_CrossSpectraPlot.conf # # The former method is recommended. Whether you add them to a user-specific configuration file or modify the metplus_config files, the following variables must be set correctly: # From 7d7283f160067887570b4135f0743ad31659249e Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 26 Jan 2022 17:10:57 -0700 Subject: [PATCH 271/770] feature 1368 PCPCombine use zero accum (#1381) --- docs/Users_Guide/glossary.rst | 16 ++++++++++++++++ docs/Users_Guide/wrappers.rst | 2 ++ .../pcp_combine/test_pcp_combine_wrapper.py | 6 +++++- metplus/wrappers/pcp_combine_wrapper.py | 13 ++++++++++--- .../PCPCombine/PCPCombine_subtract.conf | 2 ++ 5 files changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 4b3c5e6a65..3c71c5b9aa 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8759,3 +8759,19 @@ METplus Configuration Glossary :term:`ENSEMBLE_STAT_ENS_VLD_THRESH`. | *Used by:* EnsembleStat + + FCST_PCP_COMBINE_USE_ZERO_ACCUM + Only used if running PCPCombine wrapper with + :term:`FCST_PCP_COMBINE_METHOD` = SUBTRACT. If True, build a -subtract + command using the 0 accumulation as the 2nd input. If False (default), + instead build an -add command with a single input if the 2nd input is + a 0 accumulation. + + | *Used by:* PCPCombine + + OBS_PCP_COMBINE_USE_ZERO_ACCUM + Only used if running PCPCombine wrapper with + :term:`OBS_PCP_COMBINE_METHOD` = SUBTRACT. + See :term:`FCST_PCP_COMBINE_USE_ZERO_ACCUM` for more information. + + | *Used by:* PCPCombine diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 5976640376..68c73be4d6 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -5130,6 +5130,8 @@ METplus Configuration | :term:`PCP_COMBINE_CUSTOM_LOOP_LIST` | :term:`FCST_PCP_COMBINE_LOOKBACK` | :term:`OBS_PCP_COMBINE_LOOKBACK` +| :term:`FCST_PCP_COMBINE_USE_ZERO_ACCUM` +| :term:`OBS_PCP_COMBINE_USE_ZERO_ACCUM` | :term:`FCST_PCP_COMBINE_EXTRA_NAMES` (optional) | :term:`FCST_PCP_COMBINE_EXTRA_LEVELS` (optional) | :term:`FCST_PCP_COMBINE_EXTRA_OUTPUT_NAMES` (optional) diff --git a/internal_tests/pytests/pcp_combine/test_pcp_combine_wrapper.py b/internal_tests/pytests/pcp_combine/test_pcp_combine_wrapper.py index 10a80164db..f95b757911 100644 --- a/internal_tests/pytests/pcp_combine/test_pcp_combine_wrapper.py +++ b/internal_tests/pytests/pcp_combine/test_pcp_combine_wrapper.py @@ -718,7 +718,6 @@ def test_add_method_single_file(metplus_config): assert cmd == expected_cmd def test_subtract_method_zero_accum(metplus_config): - data_src = 'FCST' input_name = 'stratiform_rainfall_amount' input_level = '"(*,*)"' in_dir = '/some/input/dir' @@ -749,6 +748,8 @@ def test_subtract_method_zero_accum(metplus_config): config.set('config', 'FCST_PCP_COMBINE_OUTPUT_ACCUM', '1H') config.set('config', 'FCST_PCP_COMBINE_OUTPUT_NAME', input_name) + + # NETCDF example should use zero accum, GRIB example should not (use -add) expected_cmds_dict = {} expected_cmds_dict['NETCDF'] = [ (f"-subtract " @@ -774,6 +775,9 @@ def test_subtract_method_zero_accum(metplus_config): if data_type == 'NETCDF': config.set('config', 'FCST_PCP_COMBINE_INPUT_NAMES', input_name) config.set('config', 'FCST_PCP_COMBINE_INPUT_LEVELS', input_level) + config.set('config', 'FCST_PCP_COMBINE_USE_ZERO_ACCUM', 'True') + else: + config.set('config', 'FCST_PCP_COMBINE_USE_ZERO_ACCUM', 'False') wrapper = PCPCombineWrapper(config) assert wrapper.isOK diff --git a/metplus/wrappers/pcp_combine_wrapper.py b/metplus/wrappers/pcp_combine_wrapper.py index d34eaa609b..c23993e401 100755 --- a/metplus/wrappers/pcp_combine_wrapper.py +++ b/metplus/wrappers/pcp_combine_wrapper.py @@ -189,6 +189,11 @@ def set_fcst_or_obs_dict_items(self, d_type, c_dict): f'{d_type}_PCP_COMBINE_EXTRA_OUTPUT_NAMES', '') ) + c_dict[f'{d_type}_USE_ZERO_ACCUM'] = self.config.getbool( + 'config', + f'{d_type}_PCP_COMBINE_USE_ZERO_ACCUM', False + ) + if run_method == 'DERIVE' and not c_dict[f'{d_type}_STAT_LIST']: self.log_error('Statistic list is empty. Must set ' f'{d_type}_PCP_COMBINE_STAT_LIST if running ' @@ -364,9 +369,11 @@ def setup_subtract_method(self, time_info, accum, data_src): # if data is GRIB and second lead is 0, then # run PCPCombine in -add mode with just the first file - if lead2 == 0 and self.c_dict[data_src+'_INPUT_DATATYPE'] == 'GRIB': - self.logger.debug("Subtracted accumulation is 0 for GRIB data," - " so running ADD mode on one file") + if lead2 == 0 and not self.c_dict[f'{data_src}_USE_ZERO_ACCUM']: + self.logger.info("Subtracted accumulation is 0," + " so running ADD mode on one file." + "To use 0 accum data, set " + f"{data_src}_PCP_COMBINE_USE_ZERO_ACCUM = True") self.args.clear() self.args.append('-add') field_info = self.get_field_string( diff --git a/parm/use_cases/met_tool_wrapper/PCPCombine/PCPCombine_subtract.conf b/parm/use_cases/met_tool_wrapper/PCPCombine/PCPCombine_subtract.conf index f1172bc4a5..38001d23a3 100644 --- a/parm/use_cases/met_tool_wrapper/PCPCombine/PCPCombine_subtract.conf +++ b/parm/use_cases/met_tool_wrapper/PCPCombine/PCPCombine_subtract.conf @@ -33,3 +33,5 @@ FCST_PCP_COMBINE_INPUT_DATATYPE = GRIB FCST_PCP_COMBINE_OUTPUT_ACCUM = 3H FCST_PCP_COMBINE_OUTPUT_NAME = APCP_03 + +FCST_PCP_COMBINE_USE_ZERO_ACCUM = False From 3095217059ca17ae00401d0cdfa7d75d7dc6d900 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 28 Jan 2022 10:09:30 -0700 Subject: [PATCH 272/770] feature 1369 grid_weight_flag in EnsembleStat (#1379) --- docs/Users_Guide/glossary.rst | 5 +++++ docs/Users_Guide/wrappers.rst | 13 +++++++++++++ .../ensemble_stat/test_ensemble_stat_wrapper.py | 3 +++ metplus/wrappers/ensemble_stat_wrapper.py | 6 ++++++ parm/met_config/EnsembleStatConfig_wrapped | 4 +++- .../met_tool_wrapper/EnsembleStat/EnsembleStat.conf | 2 ++ 6 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 3c71c5b9aa..2ec8305eaf 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8760,6 +8760,11 @@ METplus Configuration Glossary | *Used by:* EnsembleStat + ENSEMBLE_STAT_GRID_WEIGHT_FLAG + Specify the value for 'grid_weight_flag' in the MET configuration file for EnsembleStat. + + | *Used by:* EnsembleStat + FCST_PCP_COMBINE_USE_ZERO_ACCUM Only used if running PCPCombine wrapper with :term:`FCST_PCP_COMBINE_METHOD` = SUBTRACT. If True, build a -subtract diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 68c73be4d6..0abd7debb0 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -281,6 +281,7 @@ METplus Configuration | :term:`ENSEMBLE_STAT_MET_CONFIG_OVERRIDES` | :term:`ENSEMBLE_STAT_ENS_MEMBER_IDS` | :term:`ENSEMBLE_STAT_CONTROL_ID` +| :term:`ENSEMBLE_STAT_GRID_WEIGHT_FLAG` | :term:`ENSEMBLE_STAT_VERIFICATION_MASK_TEMPLATE` (optional) | :term:`ENS_VAR_NAME` (optional) | :term:`ENS_VAR_LEVELS` (optional) @@ -890,6 +891,18 @@ see :ref:`How METplus controls MET config file settings`. * - :term:`ENSEMBLE_STAT_MET_CONFIG_OVERRIDES` - n/a +**${METPLUS_GRID_WEIGHT_FLAG}** + +.. list-table:: + :widths: 5 5 + :header-rows: 0 + + * - METplus Config(s) + - MET Config File + * - :term:`ENSEMBLE_STAT_GRID_WEIGHT_FLAG` + - grid_weight_flag + + .. _example_wrapper: Example diff --git a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py index 1ef41cf8e8..4a12ceef65 100644 --- a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py +++ b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py @@ -553,6 +553,9 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, ({'ENSEMBLE_STAT_CONTROL_ID': '0', }, {'METPLUS_CONTROL_ID': 'control_id = "0";'}), + ({'ENSEMBLE_STAT_GRID_WEIGHT_FLAG': 'COS_LAT', }, + {'METPLUS_GRID_WEIGHT_FLAG': 'grid_weight_flag = COS_LAT;'}), + ] ) def test_ensemble_stat_single_field(metplus_config, config_overrides, diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index 0d8e0141af..b3762465b6 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -66,6 +66,7 @@ class EnsembleStatWrapper(CompareGriddedWrapper): 'METPLUS_OBS_QUALITY_EXC', 'METPLUS_ENS_MEMBER_IDS', 'METPLUS_CONTROL_ID', + 'METPLUS_GRID_WEIGHT_FLAG', ] # handle deprecated env vars used pre v4.0.0 @@ -330,6 +331,11 @@ def create_c_dict(self): self.add_met_config(name='control_id', data_type='string') + self.add_met_config(name='grid_weight_flag', + data_type='string', + extra_args={'remove_quotes': True, + 'uppercase': True}) + # old method of setting MET config values c_dict['ENS_THRESH'] = ( self.config.getstr('config', 'ENSEMBLE_STAT_ENS_THRESH', '1.0') diff --git a/parm/met_config/EnsembleStatConfig_wrapped b/parm/met_config/EnsembleStatConfig_wrapped index e398ca1d1d..1ed2dd7b21 100644 --- a/parm/met_config/EnsembleStatConfig_wrapped +++ b/parm/met_config/EnsembleStatConfig_wrapped @@ -223,7 +223,9 @@ rng = { //////////////////////////////////////////////////////////////////////////////// -grid_weight_flag = NONE; +//grid_weight_flag = +${METPLUS_GRID_WEIGHT_FLAG} + ${METPLUS_OUTPUT_PREFIX} //version = "V9.0"; diff --git a/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf b/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf index c7714c0291..6485f36212 100644 --- a/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf +++ b/parm/use_cases/met_tool_wrapper/EnsembleStat/EnsembleStat.conf @@ -212,3 +212,5 @@ ENSEMBLE_STAT_ENSEMBLE_FLAG_WEIGHT = FALSE #ENSEMBLE_STAT_ENS_MEMBER_IDS = #ENSEMBLE_STAT_CONTROL_ID = + +#ENSEMBLE_STAT_GRID_WEIGHT_FLAG = From 527f7da444693f401ce1bf858ff6780373ac4270 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 28 Jan 2022 12:09:21 -0700 Subject: [PATCH 273/770] Per #1356, change how wrappers create instances of other wrappers to ensure that config settings for the created instance do not change values in METplusConfig used by the rest of the wrappers, ci-run-all-diff --- metplus/wrappers/extract_tiles_wrapper.py | 10 +++++++++- metplus/wrappers/py_embed_ingest_wrapper.py | 11 ++++++++--- metplus/wrappers/series_analysis_wrapper.py | 8 +++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/metplus/wrappers/extract_tiles_wrapper.py b/metplus/wrappers/extract_tiles_wrapper.py index c04d995806..ac7f82f0da 100755 --- a/metplus/wrappers/extract_tiles_wrapper.py +++ b/metplus/wrappers/extract_tiles_wrapper.py @@ -187,8 +187,16 @@ def regrid_data_plane_init(self): ) overrides[f'{rdp}_ONCE_PER_FIELD'] = False overrides[f'{rdp}_MANDATORY'] = False + + # set all config variables in a new section + instance = 'extract_tiles_rdp' + if not self.config.has_section(instance): + self.config.add_section(instance) + for key, value in overrides.items(): + self.config.set(instance, key, value) + rdp_wrapper = RegridDataPlaneWrapper(self.config, - config_overrides=overrides) + instance=instance) rdp_wrapper.c_dict['SHOW_WARNINGS'] = False return rdp_wrapper diff --git a/metplus/wrappers/py_embed_ingest_wrapper.py b/metplus/wrappers/py_embed_ingest_wrapper.py index 5cbb4b229a..81bc72d559 100755 --- a/metplus/wrappers/py_embed_ingest_wrapper.py +++ b/metplus/wrappers/py_embed_ingest_wrapper.py @@ -100,12 +100,17 @@ def create_c_dict(self): c_dict['INGESTERS'].append(ingester_dict) - skip = c_dict['SKIP_IF_OUTPUT_EXISTS'] - config_overrides = {'REGRID_DATA_PLANE_SKIP_IF_OUTPUT_EXISTS': skip} + # set config values for RegridDataPlane instance + instance = 'py_embed_ingest_rdp' + if not self.config.has_section(instance): + self.config.add_section(instance) + self.config.set(instance, + 'REGRID_DATA_PLANE_SKIP_IF_OUTPUT_EXISTS', + c_dict['SKIP_IF_OUTPUT_EXISTS']) c_dict['regrid_data_plane'] = ( RegridDataPlaneWrapper(self.config, - config_overrides=config_overrides) + instance=instance) ) return c_dict diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 578dad2736..3b87e99293 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -371,8 +371,14 @@ def _plot_data_plane_init(self): "map_data={ source=[];}" ) + instance = 'plot_data_plane_sa' + if not self.config.has_section(instance): + self.config.add_section(instance) + for key, value in plot_overrides.items(): + self.config.set(instance, key, value) + pdp_wrapper = PlotDataPlaneWrapper(self.config, - config_overrides=plot_overrides) + instance=instance) return pdp_wrapper def clear(self): From 3e19c6777aa40a59fc5a539139439921c1849337 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 28 Jan 2022 12:44:41 -0700 Subject: [PATCH 274/770] Per #1356, remove config_overrides functionality in favor of using instances -- this prevents the issue where overrides for a given tool affect the global config settings. it also forces the configs to override for an instance to be put into another config section so that it will be available in the final conf, ci-run-all-diff --- .../pytests/ascii2nc/test_ascii2nc_wrapper.py | 8 ++++- .../command_builder/test_command_builder.py | 33 ------------------- metplus/wrappers/ascii2nc_wrapper.py | 6 ++-- metplus/wrappers/command_builder.py | 14 +------- metplus/wrappers/compare_gridded_wrapper.py | 6 ++-- metplus/wrappers/cyclone_plotter_wrapper.py | 18 +++++----- metplus/wrappers/ensemble_stat_wrapper.py | 6 ++-- metplus/wrappers/example_wrapper.py | 6 ++-- metplus/wrappers/extract_tiles_wrapper.py | 6 ++-- metplus/wrappers/gempak_to_cf_wrapper.py | 6 ++-- metplus/wrappers/gen_ens_prod_wrapper.py | 6 ++-- metplus/wrappers/gen_vx_mask_wrapper.py | 6 ++-- metplus/wrappers/gfdl_tracker_wrapper.py | 6 ++-- metplus/wrappers/grid_diag_wrapper.py | 6 ++-- metplus/wrappers/grid_stat_wrapper.py | 6 ++-- metplus/wrappers/ioda2nc_wrapper.py | 6 ++-- metplus/wrappers/loop_times_wrapper.py | 6 ++-- metplus/wrappers/make_plots_wrapper.py | 6 ++-- metplus/wrappers/met_db_load_wrapper.py | 6 ++-- metplus/wrappers/mode_wrapper.py | 6 ++-- metplus/wrappers/mtd_wrapper.py | 6 ++-- metplus/wrappers/pb2nc_wrapper.py | 6 ++-- metplus/wrappers/pcp_combine_wrapper.py | 6 ++-- metplus/wrappers/plot_data_plane_wrapper.py | 6 ++-- metplus/wrappers/point2grid_wrapper.py | 6 ++-- metplus/wrappers/point_stat_wrapper.py | 6 ++-- metplus/wrappers/py_embed_ingest_wrapper.py | 6 ++-- metplus/wrappers/reformat_gridded_wrapper.py | 6 ++-- metplus/wrappers/regrid_data_plane_wrapper.py | 6 ++-- metplus/wrappers/runtime_freq_wrapper.py | 6 ++-- metplus/wrappers/series_analysis_wrapper.py | 6 ++-- metplus/wrappers/stat_analysis_wrapper.py | 6 ++-- metplus/wrappers/tc_gen_wrapper.py | 6 ++-- metplus/wrappers/tc_pairs_wrapper.py | 6 ++-- metplus/wrappers/tc_stat_wrapper.py | 6 ++-- metplus/wrappers/tcmpr_plotter_wrapper.py | 6 ++-- metplus/wrappers/tcrmw_wrapper.py | 7 ++-- metplus/wrappers/usage_wrapper.py | 6 ++-- metplus/wrappers/user_script_wrapper.py | 6 ++-- 39 files changed, 88 insertions(+), 196 deletions(-) diff --git a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py index b7c3b72767..e746de6be9 100644 --- a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py +++ b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py @@ -28,8 +28,14 @@ def ascii2nc_wrapper(metplus_config, config_path=None, config_overrides=None): for key, value in config_overrides.items(): overrides[key] = value + instance = 'overrides' + if not config.has_section(instance): + config.add_section(instance) + for key, value in overrides.items(): + config.set(instance, key, value) + return ASCII2NCWrapper(config, - config_overrides=overrides) + instance=instance) @pytest.mark.parametrize( 'config_overrides, env_var_values', [ diff --git a/internal_tests/pytests/command_builder/test_command_builder.py b/internal_tests/pytests/command_builder/test_command_builder.py index 355cb66b87..98aed81ffd 100644 --- a/internal_tests/pytests/command_builder/test_command_builder.py +++ b/internal_tests/pytests/command_builder/test_command_builder.py @@ -236,39 +236,6 @@ def test_find_obs_dated_next_day(metplus_config): obs_file = pcw.find_obs(time_info, v) assert obs_file == pcw.c_dict['OBS_INPUT_DIR']+'/20180202/20180202_0013' -@pytest.mark.parametrize( - 'overrides, c_dict', [ - ({'LOG_MET_VERBOSITY': '5', }, # string - {'VERBOSITY': '5', }), - ({'CUSTOM_LOOP_LIST': 'a,b,c', }, # list - {'CUSTOM_LOOP_LIST': ['a', 'b', 'c'], }), - ({'SKIP_TIMES': '"%H:12,18", "%Y%m%d:20200201"', }, # dict - {'SKIP_TIMES': {'%H': ['12', '18'], - '%Y%m%d': ['20200201'], }}), - ] -) -def test_override_config_in_c_dict(metplus_config, overrides, c_dict): - config = metplus_config() - - pcw = CommandBuilder(config, config_overrides=overrides) - for key, expected_value in c_dict.items(): - assert pcw.c_dict.get(key) == expected_value - -@pytest.mark.parametrize( - 'overrides', [ - ({'LOG_MET_VERBOSITY': '5', }), - ({'CUSTOM_LOOP_LIST': 'a,b,c', }), - ({'SKIP_TIMES': '"%H:12,18", "%Y%m%d:20200201"', }), - ({'FAKE_TEMPLATE': '{valid?fmt=%Y%m%d%H}', }), - ] -) -def test_override_config(metplus_config, overrides): - config = metplus_config() - - pcw = CommandBuilder(config, config_overrides=overrides) - for key, expected_value in overrides.items(): - assert pcw.config.getraw('config', key) == expected_value - # dictionary items with values will be set in [test_section] # items with value None will not be set, so it should use # the value in [config], which is always 'default' diff --git a/metplus/wrappers/ascii2nc_wrapper.py b/metplus/wrappers/ascii2nc_wrapper.py index e68d78ac23..3e110e01a6 100755 --- a/metplus/wrappers/ascii2nc_wrapper.py +++ b/metplus/wrappers/ascii2nc_wrapper.py @@ -29,13 +29,11 @@ class ASCII2NCWrapper(CommandBuilder): 'METPLUS_TIME_SUMMARY_DICT', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "ascii2nc" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 4eccbb92d3..8ba8bbcce5 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -50,7 +50,7 @@ class CommandBuilder: # name of variable to hold any MET config overrides MET_OVERRIDES_KEY = 'METPLUS_MET_CONFIG_OVERRIDES' - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.isOK = True self.errors = 0 self.config = config @@ -89,9 +89,6 @@ def __init__(self, config, instance=None, config_overrides=None): self.instance = instance - # override config if any were supplied - self.override_config(config_overrides) - self.env = os.environ.copy() if hasattr(config, 'env'): self.env = config.env @@ -139,15 +136,6 @@ def __init__(self, config, instance=None, config_overrides=None): self.clear() - def override_config(self, config_overrides): - if not config_overrides: - return - - self.logger.debug("Overriding config with explicit values:") - for key, value in config_overrides.items(): - self.logger.debug(f"Setting [config] {key} = {value}") - self.config.set('config', key, value) - def check_for_unused_env_vars(self): config_file = self.c_dict.get('CONFIG_FILE') if not config_file: diff --git a/metplus/wrappers/compare_gridded_wrapper.py b/metplus/wrappers/compare_gridded_wrapper.py index e9c4e2bafe..66caa20ec0 100755 --- a/metplus/wrappers/compare_gridded_wrapper.py +++ b/metplus/wrappers/compare_gridded_wrapper.py @@ -31,14 +31,12 @@ class CompareGriddedWrapper(CommandBuilder): that reformat gridded data """ - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): # set app_name if not set by child class to allow tests to run on this wrapper if not hasattr(self, 'app_name'): self.app_name = 'compare_gridded' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) # check to make sure all necessary probabilistic settings are set correctly # this relies on the subclass to finish creating the c_dict, so it has to # be checked after that happens diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index e401c9bd88..5edc6333e5 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -1,5 +1,5 @@ -"""!@namespace ExtraTropicalCyclonePlotter +"""!@namespace CyclonePlotter A Python class that generates plots of extra tropical cyclone forecast data, replicating the NCEP tropical and extra tropical cyclone tracks and verification plots http://www.emc.ncep.noaa.gov/mmb/gplou/emchurr/glblgen/ @@ -25,11 +25,13 @@ import cartopy from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER - ##If the script is run on a limited-internet access machine, the CARTOPY_DIR environment setting - ##will need to be set in the user-specific system configuration file. Review the Installation section - ##of the User's Guide for more details. + # If the script is run on a limited-internet access machine, + # the CARTOPY_DIR environment setting + # will need to be set in the user-specific system configuration file. + # Review the Installation section of the User's Guide for more details. if os.getenv('CARTOPY_DIR'): - cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir')) + cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', + cartopy.config.get('data_dir')) except Exception as err_msg: WRAPPER_CANNOT_RUN = True @@ -48,12 +50,10 @@ class CyclonePlotterWrapper(CommandBuilder): Reads input from ATCF files generated from MET TC-Pairs """ - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'cyclone_plotter' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) if WRAPPER_CANNOT_RUN: self.log_error("There was a problem importing modules: " diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index b3762465b6..453946a3b5 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -100,13 +100,11 @@ class EnsembleStatWrapper(CompareGriddedWrapper): 'weight', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'ensemble_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """!Create a dictionary containing the values set in the config file diff --git a/metplus/wrappers/example_wrapper.py b/metplus/wrappers/example_wrapper.py index 821365b72d..04f8ddcd07 100755 --- a/metplus/wrappers/example_wrapper.py +++ b/metplus/wrappers/example_wrapper.py @@ -18,11 +18,9 @@ class ExampleWrapper(CommandBuilder): """!Wrapper can be used as a base to develop a new wrapper""" - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'example' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/extract_tiles_wrapper.py b/metplus/wrappers/extract_tiles_wrapper.py index ac7f82f0da..b0e3f99824 100755 --- a/metplus/wrappers/extract_tiles_wrapper.py +++ b/metplus/wrappers/extract_tiles_wrapper.py @@ -50,11 +50,9 @@ class ExtractTilesWrapper(CommandBuilder): 'MTD': 'OBJECT_CAT', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'extract_tiles' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.regrid_data_plane = self.regrid_data_plane_init() def create_c_dict(self): diff --git a/metplus/wrappers/gempak_to_cf_wrapper.py b/metplus/wrappers/gempak_to_cf_wrapper.py index 6813282c9e..6f618427c9 100755 --- a/metplus/wrappers/gempak_to_cf_wrapper.py +++ b/metplus/wrappers/gempak_to_cf_wrapper.py @@ -24,12 +24,10 @@ class GempakToCFWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "GempakToCF" self.app_path = config.getstr('exe', 'GEMPAKTOCF_JAR', '') - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """!Create dictionary from config items to be used in the wrapper diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index 5df8bbb1c3..aeb6e706fd 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -51,13 +51,11 @@ class GenEnsProdWrapper(LoopTimesWrapper): 'weight', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'gen_ens_prod' self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/gen_vx_mask_wrapper.py b/metplus/wrappers/gen_vx_mask_wrapper.py index ac5459ec1d..b3ad0eeb2c 100755 --- a/metplus/wrappers/gen_vx_mask_wrapper.py +++ b/metplus/wrappers/gen_vx_mask_wrapper.py @@ -26,13 +26,11 @@ class GenVxMaskWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "gen_vx_mask" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/gfdl_tracker_wrapper.py b/metplus/wrappers/gfdl_tracker_wrapper.py index 4cf8f9176b..ff192bb179 100755 --- a/metplus/wrappers/gfdl_tracker_wrapper.py +++ b/metplus/wrappers/gfdl_tracker_wrapper.py @@ -117,11 +117,9 @@ class GFDLTrackerWrapper(CommandBuilder): "VERBOSE_VERB_G2": "int", } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'gfdl_tracker' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/grid_diag_wrapper.py b/metplus/wrappers/grid_diag_wrapper.py index d6d306f758..dcdd3501b8 100755 --- a/metplus/wrappers/grid_diag_wrapper.py +++ b/metplus/wrappers/grid_diag_wrapper.py @@ -35,13 +35,11 @@ class GridDiagWrapper(RuntimeFreqWrapper): 'METPLUS_MASK_DICT', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "grid_diag" self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/grid_stat_wrapper.py b/metplus/wrappers/grid_stat_wrapper.py index 35451c9ff2..86ea6dfbfa 100755 --- a/metplus/wrappers/grid_stat_wrapper.py +++ b/metplus/wrappers/grid_stat_wrapper.py @@ -96,13 +96,11 @@ class GridStatWrapper(CompareGriddedWrapper): 'apply_mask', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'grid_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/ioda2nc_wrapper.py b/metplus/wrappers/ioda2nc_wrapper.py index bfdd2e42df..4ff71addea 100755 --- a/metplus/wrappers/ioda2nc_wrapper.py +++ b/metplus/wrappers/ioda2nc_wrapper.py @@ -34,13 +34,11 @@ class IODA2NCWrapper(LoopTimesWrapper): 'METPLUS_TIME_SUMMARY_DICT', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "ioda2nc" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Read METplusConfig object and sets values in dictionary to be diff --git a/metplus/wrappers/loop_times_wrapper.py b/metplus/wrappers/loop_times_wrapper.py index 87781e76a2..9b7b4fae9d 100755 --- a/metplus/wrappers/loop_times_wrapper.py +++ b/metplus/wrappers/loop_times_wrapper.py @@ -15,14 +15,12 @@ class LoopTimesWrapper(RuntimeFreqWrapper): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): # set app_name if not set by child class to allow tests to run if not hasattr(self, 'app_name'): self.app_name = 'loop_times' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/make_plots_wrapper.py b/metplus/wrappers/make_plots_wrapper.py index 43d3183633..29bbcc8aac 100755 --- a/metplus/wrappers/make_plots_wrapper.py +++ b/metplus/wrappers/make_plots_wrapper.py @@ -70,12 +70,10 @@ class MakePlotsWrapper(CommandBuilder): 'VERIF_GRID', 'EVENT_EQUALIZATION', 'LOG_METPLUS', 'LOG_LEVEL' ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_path = 'python' self.app_name = 'make_plots' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) if WRAPPER_CANNOT_RUN: self.log_error(f"There was a problem importing modules: {EXCEPTION_ERR}\n") diff --git a/metplus/wrappers/met_db_load_wrapper.py b/metplus/wrappers/met_db_load_wrapper.py index 8437229c94..65f075792b 100755 --- a/metplus/wrappers/met_db_load_wrapper.py +++ b/metplus/wrappers/met_db_load_wrapper.py @@ -44,16 +44,14 @@ class METDbLoadWrapper(RuntimeFreqWrapper): 'LOAD_MPR': 'bool', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): met_data_db_dir = config.getdir('MET_DATA_DB_DIR') self.app_path = os.path.join(met_data_db_dir, 'METdbLoad', 'ush', 'met_db_load') self.app_name = os.path.basename(self.app_path) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/mode_wrapper.py b/metplus/wrappers/mode_wrapper.py index 23f958ac10..edd59aeb4c 100755 --- a/metplus/wrappers/mode_wrapper.py +++ b/metplus/wrappers/mode_wrapper.py @@ -96,15 +96,13 @@ class MODEWrapper(CompareGriddedWrapper): '(400.0/grid_res,0.0))'), } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): # only set app variables if not already set by MTD (subclass) if not hasattr(self, 'app_name'): self.app_name = 'mode' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def add_merge_config_file(self, time_info): """!If merge config file is defined, add it to the command""" diff --git a/metplus/wrappers/mtd_wrapper.py b/metplus/wrappers/mtd_wrapper.py index 6bf2bc19be..beb5eddd5c 100755 --- a/metplus/wrappers/mtd_wrapper.py +++ b/metplus/wrappers/mtd_wrapper.py @@ -37,13 +37,11 @@ class MTDWrapper(CompareGriddedWrapper): 'METPLUS_OUTPUT_PREFIX', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'mtd' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.fcst_file = None self.obs_file = None diff --git a/metplus/wrappers/pb2nc_wrapper.py b/metplus/wrappers/pb2nc_wrapper.py index 31828f145b..9fb28f4a3c 100755 --- a/metplus/wrappers/pb2nc_wrapper.py +++ b/metplus/wrappers/pb2nc_wrapper.py @@ -39,13 +39,11 @@ class PB2NCWrapper(CommandBuilder): 'METPLUS_OBS_PREPBUFR_MAP', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'pb2nc' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Create a data structure (dictionary) that contains all the diff --git a/metplus/wrappers/pcp_combine_wrapper.py b/metplus/wrappers/pcp_combine_wrapper.py index c23993e401..8afbc171a6 100755 --- a/metplus/wrappers/pcp_combine_wrapper.py +++ b/metplus/wrappers/pcp_combine_wrapper.py @@ -26,13 +26,11 @@ class PCPCombineWrapper(ReformatGriddedWrapper): # valid values for [FCST/OBS]_PCP_COMBINE_METHOD valid_run_methods = ['ADD', 'SUM', 'SUBTRACT', 'DERIVE', 'USER_DEFINED'] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'pcp_combine' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Create dictionary from config items to be used in the wrapper diff --git a/metplus/wrappers/plot_data_plane_wrapper.py b/metplus/wrappers/plot_data_plane_wrapper.py index 2ed866c54d..831cd5a330 100755 --- a/metplus/wrappers/plot_data_plane_wrapper.py +++ b/metplus/wrappers/plot_data_plane_wrapper.py @@ -24,13 +24,11 @@ class PlotDataPlaneWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "plot_data_plane" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/point2grid_wrapper.py b/metplus/wrappers/point2grid_wrapper.py index 5c71eebba7..f9ba942df1 100755 --- a/metplus/wrappers/point2grid_wrapper.py +++ b/metplus/wrappers/point2grid_wrapper.py @@ -25,13 +25,11 @@ class Point2GridWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "point2grid" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/point_stat_wrapper.py b/metplus/wrappers/point_stat_wrapper.py index 669ab8252c..2ea826e33b 100755 --- a/metplus/wrappers/point_stat_wrapper.py +++ b/metplus/wrappers/point_stat_wrapper.py @@ -74,13 +74,11 @@ class PointStatWrapper(CompareGriddedWrapper): 'orank', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'point_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Create a dictionary that holds all the values set in the diff --git a/metplus/wrappers/py_embed_ingest_wrapper.py b/metplus/wrappers/py_embed_ingest_wrapper.py index 81bc72d559..c59847deda 100755 --- a/metplus/wrappers/py_embed_ingest_wrapper.py +++ b/metplus/wrappers/py_embed_ingest_wrapper.py @@ -24,11 +24,9 @@ class PyEmbedIngestWrapper(CommandBuilder): """!Wrapper to utilize Python Embedding in the MET tools to read in data using a python script""" - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'py_embed_ingest' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/reformat_gridded_wrapper.py b/metplus/wrappers/reformat_gridded_wrapper.py index feafd54384..da38e4f1de 100755 --- a/metplus/wrappers/reformat_gridded_wrapper.py +++ b/metplus/wrappers/reformat_gridded_wrapper.py @@ -31,10 +31,8 @@ class ReformatGriddedWrapper(CommandBuilder): """! Common functionality to wrap similar MET applications that reformat gridded data """ - def __init__(self, config, instance=None, config_overrides=None): - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + def __init__(self, config, instance=None): + super().__init__(config, instance=instance) # this class should not be called directly # pylint:disable=unused-argument diff --git a/metplus/wrappers/regrid_data_plane_wrapper.py b/metplus/wrappers/regrid_data_plane_wrapper.py index a3d544a0db..e03ae0e158 100755 --- a/metplus/wrappers/regrid_data_plane_wrapper.py +++ b/metplus/wrappers/regrid_data_plane_wrapper.py @@ -27,13 +27,11 @@ class RegridDataPlaneWrapper(ReformatGriddedWrapper): '''! Wraps the MET tool regrid_data_plane to reformat gridded datasets ''' - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'regrid_data_plane' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/runtime_freq_wrapper.py b/metplus/wrappers/runtime_freq_wrapper.py index 105bc552e9..adfaec06a9 100755 --- a/metplus/wrappers/runtime_freq_wrapper.py +++ b/metplus/wrappers/runtime_freq_wrapper.py @@ -34,10 +34,8 @@ class RuntimeFreqWrapper(CommandBuilder): 'RUN_ONCE_FOR_EACH' ] - def __init__(self, config, instance=None, config_overrides=None): - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + def __init__(self, config, instance=None): + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 3b87e99293..d4e48f95f9 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -81,14 +81,12 @@ class SeriesAnalysisWrapper(RuntimeFreqWrapper): 'prc', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'series_analysis' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) if self.c_dict['GENERATE_PLOTS']: self.plot_data_plane = self._plot_data_plane_init() diff --git a/metplus/wrappers/stat_analysis_wrapper.py b/metplus/wrappers/stat_analysis_wrapper.py index d95bb32305..3664d4b939 100755 --- a/metplus/wrappers/stat_analysis_wrapper.py +++ b/metplus/wrappers/stat_analysis_wrapper.py @@ -113,13 +113,11 @@ class StatAnalysisWrapper(CommandBuilder): 'FCST_INIT_HOUR_LIST', 'OBS_INIT_HOUR_LIST' ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), 'stat_analysis') self.app_name = os.path.basename(self.app_path) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def get_command(self): diff --git a/metplus/wrappers/tc_gen_wrapper.py b/metplus/wrappers/tc_gen_wrapper.py index 3f6393b841..70b31df854 100755 --- a/metplus/wrappers/tc_gen_wrapper.py +++ b/metplus/wrappers/tc_gen_wrapper.py @@ -88,13 +88,11 @@ class TCGenWrapper(CommandBuilder): ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "tc_gen" self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/tc_pairs_wrapper.py b/metplus/wrappers/tc_pairs_wrapper.py index 369779ed43..424b25a4af 100755 --- a/metplus/wrappers/tc_pairs_wrapper.py +++ b/metplus/wrappers/tc_pairs_wrapper.py @@ -76,13 +76,11 @@ class TCPairsWrapper(CommandBuilder): 'cyclone': r'[0-9]{2,4}', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'tc_pairs' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.adeck = [] self.bdeck = [] self.edeck = [] diff --git a/metplus/wrappers/tc_stat_wrapper.py b/metplus/wrappers/tc_stat_wrapper.py index bb3c33e62c..cbe3379144 100755 --- a/metplus/wrappers/tc_stat_wrapper.py +++ b/metplus/wrappers/tc_stat_wrapper.py @@ -79,14 +79,12 @@ class TCStatWrapper(CommandBuilder): 'METPLUS_INIT_STR_EXC_VAL', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'tc_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.logger.debug("Initialized TCStatWrapper") def create_c_dict(self): diff --git a/metplus/wrappers/tcmpr_plotter_wrapper.py b/metplus/wrappers/tcmpr_plotter_wrapper.py index 3a3b6fb6e7..54288bdfe1 100755 --- a/metplus/wrappers/tcmpr_plotter_wrapper.py +++ b/metplus/wrappers/tcmpr_plotter_wrapper.py @@ -58,12 +58,10 @@ class TCMPRPlotterWrapper(CommandBuilder): 'save': 'bool', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'tcmpr_plotter' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/tcrmw_wrapper.py b/metplus/wrappers/tcrmw_wrapper.py index a5ce97553d..7c0e438d70 100755 --- a/metplus/wrappers/tcrmw_wrapper.py +++ b/metplus/wrappers/tcrmw_wrapper.py @@ -47,13 +47,12 @@ class TCRMWWrapper(CommandBuilder): 'METPLUS_DELTA_RANGE_KM', 'METPLUS_RMW_SCALE', ] - def __init__(self, config, instance=None, config_overrides=None): + + def __init__(self, config, instance=None): self.app_name = "tc_rmw" self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/usage_wrapper.py b/metplus/wrappers/usage_wrapper.py index 11a2ef8ba4..d3fb8cf852 100644 --- a/metplus/wrappers/usage_wrapper.py +++ b/metplus/wrappers/usage_wrapper.py @@ -10,11 +10,9 @@ class UsageWrapper(CommandBuilder): """! A default process, prints out usage when nothing is defined in the PROCESS_LIST """ - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'Usage' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) # get unique list of processes from met_util self.available_processes = list(set(val for val in LOWER_TO_WRAPPER_NAME.values())) self.available_processes.sort() diff --git a/metplus/wrappers/user_script_wrapper.py b/metplus/wrappers/user_script_wrapper.py index 89934edb64..de420fd861 100755 --- a/metplus/wrappers/user_script_wrapper.py +++ b/metplus/wrappers/user_script_wrapper.py @@ -24,11 +24,9 @@ ''' class UserScriptWrapper(RuntimeFreqWrapper): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "user_script" - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() From a70e8d5527f9db375bbf68248616df8e89f58ac2 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 28 Jan 2022 13:01:54 -0700 Subject: [PATCH 275/770] Per #1356, update documentation to reflect changes --- docs/Contributors_Guide/basic_components.rst | 6 ++---- docs/Contributors_Guide/create_wrapper.rst | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/Contributors_Guide/basic_components.rst b/docs/Contributors_Guide/basic_components.rst index 7de9af11d8..487d89a66f 100644 --- a/docs/Contributors_Guide/basic_components.rst +++ b/docs/Contributors_Guide/basic_components.rst @@ -33,13 +33,11 @@ executable, relative to MET_BIN_DIR. The init function also calls the parent's initialization function using super() function:: - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "ascii2nc" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) The above code block is an excerpt from the ASCII2NCWrapper, found in metplus/wrappers/ascii2nc_wrapper.py. diff --git a/docs/Contributors_Guide/create_wrapper.rst b/docs/Contributors_Guide/create_wrapper.rst index e051a04211..bf016d3aac 100644 --- a/docs/Contributors_Guide/create_wrapper.rst +++ b/docs/Contributors_Guide/create_wrapper.rst @@ -96,13 +96,11 @@ If the application is a MET tool, then set self.app_path to the full path of the tool under **MET_BIN_DIR**. See the Basic Components :ref:`bc_init_function` section for more information:: - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'new_tool' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) Read Configuration Variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 9832871412406f6f817096be3c54df5e4231f6b6 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 28 Jan 2022 13:59:05 -0700 Subject: [PATCH 276/770] Per #1356, fixed unit test to no longer use deprecated approach to overriding config variables --- internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py b/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py index b464c2aa85..e84b90b09a 100644 --- a/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py +++ b/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py @@ -108,8 +108,13 @@ def tc_stat_wrapper(metplus_config): ] ) def test_override_config_in_c_dict(metplus_config, overrides, c_dict): - wrapper = TCStatWrapper(get_config(metplus_config), - config_overrides=overrides) + config = get_config(metplus_config) + instance = 'tc_stat_overrides' + if not config.has_section(instance): + config.add_section(instance) + for key, value in overrides.items(): + config.set(instance, key, value) + wrapper = TCStatWrapper(config, instance=instance) for key, expected_value in c_dict.items(): assert (wrapper.env_var_dict.get(f'METPLUS_{key}') == expected_value or wrapper.c_dict.get(key) == expected_value) From 7daa7118b42a8d6963aa0cb972d3f9cb577fe91b Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 31 Jan 2022 13:07:02 -0700 Subject: [PATCH 277/770] feature 1247 climatology settings (#1385) --- docs/Users_Guide/glossary.rst | 180 ++++++++++++++++++ docs/Users_Guide/wrappers.rst | 20 ++ .../grid_stat/test_grid_stat_wrapper.py | 51 +++++ .../pytests/ioda2nc/test_ioda2nc_wrapper.py | 2 +- .../pytests/met_util/test_met_util.py | 12 -- .../point_stat/test_point_stat_wrapper.py | 2 +- .../string_manip/test_util_string_manip.py | 91 +++++---- metplus/util/config_metplus.py | 4 +- metplus/util/met_config.py | 2 +- metplus/util/met_util.py | 8 - metplus/util/string_manip.py | 104 +++++----- metplus/wrappers/command_builder.py | 40 +++- metplus/wrappers/plot_data_plane_wrapper.py | 4 +- metplus/wrappers/regrid_data_plane_wrapper.py | 3 +- metplus/wrappers/stat_analysis_wrapper.py | 6 +- metplus/wrappers/tcmpr_plotter_wrapper.py | 3 +- 16 files changed, 401 insertions(+), 131 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 2ec8305eaf..80ee89c001 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8780,3 +8780,183 @@ METplus Configuration Glossary See :term:`FCST_PCP_COMBINE_USE_ZERO_ACCUM` for more information. | *Used by:* PCPCombine + + ENSEMBLE_STAT_CLIMO_MEAN_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_mean fields for EnsembleStat. + Sets "climo_mean = fcst;" in the wrapped MET config file. + Only used if :term:`ENSEMBLE_STAT_CLIMO_MEAN_FIELD` is unset. + See also :term:`ENSEMBLE_STAT_CLIMO_MEAN_USE_OBS`. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_CLIMO_MEAN_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_mean fields for EnsembleStat. + Sets "climo_mean = obs;" in the wrapped MET config file. + Only used if :term:`ENSEMBLE_STAT_CLIMO_MEAN_FIELD` is unset. + See also :term:`ENSEMBLE_STAT_CLIMO_MEAN_USE_FCST`. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_CLIMO_STDEV_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_stdev fields for EnsembleStat. + Sets "climo_stdev = fcst;" in the wrapped MET config file. + Only used if :term:`ENSEMBLE_STAT_CLIMO_STDEV_FIELD` is unset. + See also :term:`ENSEMBLE_STAT_CLIMO_STDEV_USE_OBS`. + + | *Used by:* EnsembleStat + + ENSEMBLE_STAT_CLIMO_STDEV_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_stdev fields for EnsembleStat. + Sets "climo_stdev = obs;" in the wrapped MET config file. + Only used if :term:`ENSEMBLE_STAT_CLIMO_STDEV_FIELD` is unset. + See also :term:`ENSEMBLE_STAT_CLIMO_STDEV_USE_FCST`. + + | *Used by:* EnsembleStat + + GEN_ENS_PROD_CLIMO_MEAN_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_mean fields for GenEnsProd. + Sets "climo_mean = fcst;" in the wrapped MET config file. + Only used if :term:`GEN_ENS_PROD_CLIMO_MEAN_FIELD` is unset. + See also :term:`GEN_ENS_PROD_CLIMO_MEAN_USE_OBS`. + + | *Used by:* GenEnsProd + + GEN_ENS_PROD_CLIMO_MEAN_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_mean fields for GenEnsProd. + Sets "climo_mean = obs;" in the wrapped MET config file. + Only used if :term:`GEN_ENS_PROD_CLIMO_MEAN_FIELD` is unset. + See also :term:`GEN_ENS_PROD_CLIMO_MEAN_USE_FCST`. + + | *Used by:* GenEnsProd + + GEN_ENS_PROD_CLIMO_STDEV_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_stdev fields for GenEnsProd. + Sets "climo_stdev = fcst;" in the wrapped MET config file. + Only used if :term:`GEN_ENS_PROD_CLIMO_STDEV_FIELD` is unset. + See also :term:`GEN_ENS_PROD_CLIMO_STDEV_USE_OBS`. + + | *Used by:* GenEnsProd + + GEN_ENS_PROD_CLIMO_STDEV_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_stdev fields for GenEnsProd. + Sets "climo_stdev = obs;" in the wrapped MET config file. + Only used if :term:`GEN_ENS_PROD_CLIMO_STDEV_FIELD` is unset. + See also :term:`GEN_ENS_PROD_CLIMO_STDEV_USE_FCST`. + + | *Used by:* GenEnsProd + + GRID_STAT_CLIMO_MEAN_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_mean fields for GridStat. + Sets "climo_mean = fcst;" in the wrapped MET config file. + Only used if :term:`GRID_STAT_CLIMO_MEAN_FIELD` is unset. + See also :term:`GRID_STAT_CLIMO_MEAN_USE_OBS`. + + | *Used by:* GridStat + + GRID_STAT_CLIMO_MEAN_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_mean fields for GridStat. + Sets "climo_mean = obs;" in the wrapped MET config file. + Only used if :term:`GRID_STAT_CLIMO_MEAN_FIELD` is unset. + See also :term:`GRID_STAT_CLIMO_MEAN_USE_FCST`. + + | *Used by:* GridStat + + GRID_STAT_CLIMO_STDEV_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_stdev fields for GridStat. + Sets "climo_stdev = fcst;" in the wrapped MET config file. + Only used if :term:`GRID_STAT_CLIMO_STDEV_FIELD` is unset. + See also :term:`GRID_STAT_CLIMO_STDEV_USE_OBS`. + + | *Used by:* GridStat + + GRID_STAT_CLIMO_STDEV_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_stdev fields for GridStat. + Sets "climo_stdev = obs;" in the wrapped MET config file. + Only used if :term:`GRID_STAT_CLIMO_STDEV_FIELD` is unset. + See also :term:`GRID_STAT_CLIMO_STDEV_USE_FCST`. + + | *Used by:* GridStat + + POINT_STAT_CLIMO_MEAN_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_mean fields for PointStat. + Sets "climo_mean = fcst;" in the wrapped MET config file. + Only used if :term:`POINT_STAT_CLIMO_MEAN_FIELD` is unset. + See also :term:`POINT_STAT_CLIMO_MEAN_USE_OBS`. + + | *Used by:* PointStat + + POINT_STAT_CLIMO_MEAN_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_mean fields for PointStat. + Sets "climo_mean = obs;" in the wrapped MET config file. + Only used if :term:`POINT_STAT_CLIMO_MEAN_FIELD` is unset. + See also :term:`POINT_STAT_CLIMO_MEAN_USE_FCST`. + + | *Used by:* PointStat + + POINT_STAT_CLIMO_STDEV_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_stdev fields for PointStat. + Sets "climo_stdev = fcst;" in the wrapped MET config file. + Only used if :term:`POINT_STAT_CLIMO_STDEV_FIELD` is unset. + See also :term:`POINT_STAT_CLIMO_STDEV_USE_OBS`. + + | *Used by:* PointStat + + POINT_STAT_CLIMO_STDEV_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_stdev fields for PointStat. + Sets "climo_stdev = obs;" in the wrapped MET config file. + Only used if :term:`POINT_STAT_CLIMO_STDEV_FIELD` is unset. + See also :term:`POINT_STAT_CLIMO_STDEV_USE_FCST`. + + | *Used by:* PointStat + + SERIES_ANALYSIS_CLIMO_MEAN_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_mean fields for SeriesAnalysis. + Sets "climo_mean = fcst;" in the wrapped MET config file. + Only used if :term:`SERIES_ANALYSIS_CLIMO_MEAN_FIELD` is unset. + See also :term:`SERIES_ANALYSIS_CLIMO_MEAN_USE_OBS`. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_CLIMO_MEAN_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_mean fields for SeriesAnalysis. + Sets "climo_mean = obs;" in the wrapped MET config file. + Only used if :term:`SERIES_ANALYSIS_CLIMO_MEAN_FIELD` is unset. + See also :term:`SERIES_ANALYSIS_CLIMO_MEAN_USE_FCST`. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_CLIMO_STDEV_USE_FCST + If set to True, use the field array from the fcst dictionary for the + climo_stdev fields for SeriesAnalysis. + Sets "climo_stdev = fcst;" in the wrapped MET config file. + Only used if :term:`SERIES_ANALYSIS_CLIMO_STDEV_FIELD` is unset. + See also :term:`SERIES_ANALYSIS_CLIMO_STDEV_USE_OBS`. + + | *Used by:* SeriesAnalysis + + SERIES_ANALYSIS_CLIMO_STDEV_USE_OBS + If set to True, use the field array from the obs dictionary for the + climo_stdev fields for SeriesAnalysis. + Sets "climo_stdev = obs;" in the wrapped MET config file. + Only used if :term:`SERIES_ANALYSIS_CLIMO_STDEV_FIELD` is unset. + See also :term:`SERIES_ANALYSIS_CLIMO_STDEV_USE_FCST`. + + | *Used by:* SeriesAnalysis diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index 0abd7debb0..f63cc7b0fa 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -238,6 +238,8 @@ METplus Configuration | :term:`ENSEMBLE_STAT_CLIMO_MEAN_MATCH_MONTH` | :term:`ENSEMBLE_STAT_CLIMO_MEAN_DAY_INTERVAL` | :term:`ENSEMBLE_STAT_CLIMO_MEAN_HOUR_INTERVAL` +| :term:`ENSEMBLE_STAT_CLIMO_MEAN_USE_FCST` +| :term:`ENSEMBLE_STAT_CLIMO_MEAN_USE_OBS` | :term:`ENSEMBLE_STAT_CLIMO_STDEV_FILE_NAME` | :term:`ENSEMBLE_STAT_CLIMO_STDEV_FIELD` | :term:`ENSEMBLE_STAT_CLIMO_STDEV_REGRID_METHOD` @@ -248,6 +250,8 @@ METplus Configuration | :term:`ENSEMBLE_STAT_CLIMO_STDEV_MATCH_MONTH` | :term:`ENSEMBLE_STAT_CLIMO_STDEV_DAY_INTERVAL` | :term:`ENSEMBLE_STAT_CLIMO_STDEV_HOUR_INTERVAL` +| :term:`ENSEMBLE_STAT_CLIMO_STDEV_USE_FCST` +| :term:`ENSEMBLE_STAT_CLIMO_STDEV_USE_OBS` | :term:`ENSEMBLE_STAT_MASK_GRID` | :term:`ENSEMBLE_STAT_CI_ALPHA` | :term:`ENSEMBLE_STAT_INTERP_FIELD` @@ -1064,6 +1068,8 @@ METplus Configuration | :term:`GEN_ENS_PROD_CLIMO_MEAN_MATCH_MONTH` | :term:`GEN_ENS_PROD_CLIMO_MEAN_DAY_INTERVAL` | :term:`GEN_ENS_PROD_CLIMO_MEAN_HOUR_INTERVAL` +| :term:`GEN_ENS_PROD_CLIMO_MEAN_USE_FCST` +| :term:`GEN_ENS_PROD_CLIMO_MEAN_USE_OBS` | :term:`GEN_ENS_PROD_CLIMO_STDEV_FILE_NAME` | :term:`GEN_ENS_PROD_CLIMO_STDEV_FIELD` | :term:`GEN_ENS_PROD_CLIMO_STDEV_REGRID_METHOD` @@ -1074,6 +1080,8 @@ METplus Configuration | :term:`GEN_ENS_PROD_CLIMO_STDEV_MATCH_MONTH` | :term:`GEN_ENS_PROD_CLIMO_STDEV_DAY_INTERVAL` | :term:`GEN_ENS_PROD_CLIMO_STDEV_HOUR_INTERVAL` +| :term:`GEN_ENS_PROD_CLIMO_STDEV_USE_FCST` +| :term:`GEN_ENS_PROD_CLIMO_STDEV_USE_OBS` | :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_LATLON` | :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_MEAN` | :term:`GEN_ENS_PROD_ENSEMBLE_FLAG_STDEV` @@ -2847,6 +2855,8 @@ METplus Configuration | :term:`GRID_STAT_CLIMO_MEAN_MATCH_MONTH` | :term:`GRID_STAT_CLIMO_MEAN_DAY_INTERVAL` | :term:`GRID_STAT_CLIMO_MEAN_HOUR_INTERVAL` +| :term:`GRID_STAT_CLIMO_MEAN_USE_FCST` +| :term:`GRID_STAT_CLIMO_MEAN_USE_OBS` | :term:`GRID_STAT_CLIMO_STDEV_FILE_NAME` | :term:`GRID_STAT_CLIMO_STDEV_FIELD` | :term:`GRID_STAT_CLIMO_STDEV_REGRID_METHOD` @@ -2857,6 +2867,8 @@ METplus Configuration | :term:`GRID_STAT_CLIMO_STDEV_MATCH_MONTH` | :term:`GRID_STAT_CLIMO_STDEV_DAY_INTERVAL` | :term:`GRID_STAT_CLIMO_STDEV_HOUR_INTERVAL` +| :term:`GRID_STAT_CLIMO_STDEV_USE_FCST` +| :term:`GRID_STAT_CLIMO_STDEV_USE_OBS` | :term:`GRID_STAT_HSS_EC_VALUE` | :term:`GRID_STAT_DISTANCE_MAP_BADDELEY_P` | :term:`GRID_STAT_DISTANCE_MAP_BADDELEY_MAX_DIST` @@ -5334,6 +5346,8 @@ Configuration | :term:`POINT_STAT_CLIMO_MEAN_MATCH_MONTH` | :term:`POINT_STAT_CLIMO_MEAN_DAY_INTERVAL` | :term:`POINT_STAT_CLIMO_MEAN_HOUR_INTERVAL` +| :term:`POINT_STAT_CLIMO_MEAN_USE_FCST` +| :term:`POINT_STAT_CLIMO_MEAN_USE_OBS` | :term:`POINT_STAT_CLIMO_STDEV_FILE_NAME` | :term:`POINT_STAT_CLIMO_STDEV_FIELD` | :term:`POINT_STAT_CLIMO_STDEV_REGRID_METHOD` @@ -5344,6 +5358,8 @@ Configuration | :term:`POINT_STAT_CLIMO_STDEV_MATCH_MONTH` | :term:`POINT_STAT_CLIMO_STDEV_DAY_INTERVAL` | :term:`POINT_STAT_CLIMO_STDEV_HOUR_INTERVAL` +| :term:`POINT_STAT_CLIMO_STDEV_USE_FCST` +| :term:`POINT_STAT_CLIMO_STDEV_USE_OBS` | :term:`POINT_STAT_HSS_EC_VALUE` | :term:`POINT_STAT_HIRA_FLAG` | :term:`POINT_STAT_HIRA_WIDTH` @@ -5937,6 +5953,8 @@ METplus Configuration | :term:`SERIES_ANALYSIS_CLIMO_MEAN_DAY_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_MEAN_HOUR_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_MEAN_FILE_TYPE` +| :term:`SERIES_ANALYSIS_CLIMO_MEAN_USE_FCST` +| :term:`SERIES_ANALYSIS_CLIMO_MEAN_USE_OBS` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_FILE_NAME` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_FIELD` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_REGRID_METHOD` @@ -5948,6 +5966,8 @@ METplus Configuration | :term:`SERIES_ANALYSIS_CLIMO_STDEV_DAY_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_HOUR_INTERVAL` | :term:`SERIES_ANALYSIS_CLIMO_STDEV_FILE_TYPE` +| :term:`SERIES_ANALYSIS_CLIMO_STDEV_USE_FCST` +| :term:`SERIES_ANALYSIS_CLIMO_STDEV_USE_OBS` | :term:`SERIES_ANALYSIS_HSS_EC_VALUE` | :term:`SERIES_ANALYSIS_OUTPUT_STATS_FHO` | :term:`SERIES_ANALYSIS_OUTPUT_STATS_CTC` diff --git a/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py b/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py index 80c7393c83..72b234ea74 100644 --- a/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py +++ b/internal_tests/pytests/grid_stat/test_grid_stat_wrapper.py @@ -423,8 +423,33 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, '["/some/climo_mean/file.txt"];}'), 'CLIMO_MEAN_FILE': '"/some/climo_mean/file.txt"'}), + ({'GRID_STAT_CLIMO_MEAN_FIELD': '{name="UGRD"; level=["P850","P500","P250"];}', }, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {field = [{name="UGRD"; level=["P850","P500","P250"];}];}'}), + ({'GRID_STAT_CLIMO_MEAN_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), + # ignore USE_FCST because FIELD is set + ({'GRID_STAT_CLIMO_MEAN_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', + 'GRID_STAT_CLIMO_MEAN_USE_FCST': 'TRUE'}, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), + # ignore USE_OBS because FIELD is set + ({'GRID_STAT_CLIMO_MEAN_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', + 'GRID_STAT_CLIMO_MEAN_USE_OBS': 'TRUE'}, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), + # use fcst no other climo_mean + ({'GRID_STAT_CLIMO_MEAN_USE_FCST': 'TRUE'}, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = fcst;'}), + # use obs no other climo_mean + ({'GRID_STAT_CLIMO_MEAN_USE_OBS': 'TRUE'}, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = obs;'}), + # use fcst with other climo_mean + ({'GRID_STAT_CLIMO_MEAN_REGRID_METHOD': 'NEAREST', + 'GRID_STAT_CLIMO_MEAN_USE_FCST': 'TRUE'}, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {method = NEAREST;}}climo_mean = fcst;'}), + # use obs with other climo_mean + ({'GRID_STAT_CLIMO_MEAN_REGRID_METHOD': 'NEAREST', + 'GRID_STAT_CLIMO_MEAN_USE_OBS': 'TRUE'}, + {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {method = NEAREST;}}climo_mean = obs;'}), ({'GRID_STAT_CLIMO_MEAN_REGRID_METHOD': 'NEAREST', }, {'METPLUS_CLIMO_MEAN_DICT': 'climo_mean = {regrid = {method = NEAREST;}}'}), @@ -532,6 +557,32 @@ def test_handle_climo_file_variables(metplus_config, config_overrides, 'match_month = TRUE;day_interval = 30;' 'hour_interval = 12;}'), 'CLIMO_STDEV_FILE': '"/some/climo_stdev/file.txt"'}), + # ignore USE_FCST because FIELD is set + ( + {'GRID_STAT_CLIMO_STDEV_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', + 'GRID_STAT_CLIMO_STDEV_USE_FCST': 'TRUE'}, + { + 'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), + # ignore USE_OBS because FIELD is set + ( + {'GRID_STAT_CLIMO_STDEV_FIELD': '{name="CLM_NAME"; level="(0,0,*,*)";}', + 'GRID_STAT_CLIMO_STDEV_USE_OBS': 'TRUE'}, + { + 'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {field = [{name="CLM_NAME"; level="(0,0,*,*)";}];}'}), + # use fcst no other climo_stdev + ({'GRID_STAT_CLIMO_STDEV_USE_FCST': 'TRUE'}, + {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = fcst;'}), + # use obs no other climo_stdev + ({'GRID_STAT_CLIMO_STDEV_USE_OBS': 'TRUE'}, + {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = obs;'}), + # use fcst with other climo_stdev + ({'GRID_STAT_CLIMO_STDEV_REGRID_METHOD': 'NEAREST', + 'GRID_STAT_CLIMO_STDEV_USE_FCST': 'TRUE'}, + {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {regrid = {method = NEAREST;}}climo_stdev = fcst;'}), + # use obs with other climo_stdev + ({'GRID_STAT_CLIMO_STDEV_REGRID_METHOD': 'NEAREST', + 'GRID_STAT_CLIMO_STDEV_USE_OBS': 'TRUE'}, + {'METPLUS_CLIMO_STDEV_DICT': 'climo_stdev = {regrid = {method = NEAREST;}}climo_stdev = obs;'}), ({'GRID_STAT_GRID_WEIGHT_FLAG': 'COS_LAT', }, {'METPLUS_GRID_WEIGHT_FLAG': 'grid_weight_flag = COS_LAT;'}), diff --git a/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py b/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py index d07460055e..7effa04e77 100644 --- a/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py +++ b/internal_tests/pytests/ioda2nc/test_ioda2nc_wrapper.py @@ -39,7 +39,7 @@ def set_minimum_config_settings(config): {'METPLUS_MESSAGE_TYPE_MAP': 'message_type_map = [{ key = “AIRCAR”; val = “AIRCAR_PROFILES”; }];'}, ''), # 2 ({'IODA2NC_MESSAGE_TYPE_GROUP_MAP': '{ key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET";},{ key = "ANYAIR"; val = "AIRCAR,AIRCFT";}', }, - {'METPLUS_MESSAGE_TYPE_GROUP_MAP': 'message_type_group_map = [{ key = "SURFACE"; val = "ADPSFC, SFCSHP, MSONET";}, { key = "ANYAIR"; val = "AIRCAR, AIRCFT";}];'}, ''), + {'METPLUS_MESSAGE_TYPE_GROUP_MAP': 'message_type_group_map = [{ key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET";}, { key = "ANYAIR"; val = "AIRCAR,AIRCFT";}];'}, ''), # 3 ({'IODA2NC_STATION_ID': 'value1, value2', }, {'METPLUS_STATION_ID': 'station_id = ["value1", "value2"];'}, ''), diff --git a/internal_tests/pytests/met_util/test_met_util.py b/internal_tests/pytests/met_util/test_met_util.py index 7ff9bc2b74..ccc7d6bcd9 100644 --- a/internal_tests/pytests/met_util/test_met_util.py +++ b/internal_tests/pytests/met_util/test_met_util.py @@ -11,18 +11,6 @@ from metplus.util import time_util from metplus.util.config_metplus import parse_var_list -@pytest.mark.parametrize( - 'before, after', [ - ('string', 'string'), - ('"string"', 'string'), - ('', ''), - ('""', ''), - (None, ''), - ] -) -def test_remove_quotes(before, after): - assert(util.remove_quotes(before) == after) - @pytest.mark.parametrize( 'key, value', [ ({"gt2.3", "gt5.5"}, True), diff --git a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py index 118a6f7dcc..53aefbf198 100755 --- a/internal_tests/pytests/point_stat/test_point_stat_wrapper.py +++ b/internal_tests/pytests/point_stat/test_point_stat_wrapper.py @@ -442,7 +442,7 @@ def test_met_dictionary_in_var_options(metplus_config): 'shape = SQUARE;' 'prob_cat_thresh = [>1, <=2];}')}), ({'POINT_STAT_MESSAGE_TYPE_GROUP_MAP': '{ key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET";},{ key = "ANYAIR"; val = "AIRCAR,AIRCFT";}', }, - {'METPLUS_MESSAGE_TYPE_GROUP_MAP': 'message_type_group_map = [{ key = "SURFACE"; val = "ADPSFC, SFCSHP, MSONET";}, { key = "ANYAIR"; val = "AIRCAR, AIRCFT";}];'}), + {'METPLUS_MESSAGE_TYPE_GROUP_MAP': 'message_type_group_map = [{ key = "SURFACE"; val = "ADPSFC,SFCSHP,MSONET";}, { key = "ANYAIR"; val = "AIRCAR,AIRCFT";}];'}), ] ) diff --git a/internal_tests/pytests/util/string_manip/test_util_string_manip.py b/internal_tests/pytests/util/string_manip/test_util_string_manip.py index 9ceab10ed3..1c6f2dfc35 100644 --- a/internal_tests/pytests/util/string_manip/test_util_string_manip.py +++ b/internal_tests/pytests/util/string_manip/test_util_string_manip.py @@ -7,26 +7,65 @@ from metplus.util.string_manip import * from metplus.util.string_manip import _fix_list -def test_getlist(): - string_list = 'gt2.7, >3.6, eq42' +@pytest.mark.parametrize( + 'before, after', [ + ('string', 'string'), + ('"string"', 'string'), + ('', ''), + ('""', ''), + (None, ''), + ] +) +def test_remove_quotes(before, after): + assert(remove_quotes(before) == after) + +@pytest.mark.parametrize( + 'string_list, output_list', [ + # 0: list of strings + ('gt2.7, >3.6, eq42', + ['gt2.7', '>3.6', 'eq42']), + # 1: one string has commas within quotes + ('gt2.7, >3.6, eq42, "has,commas,in,it"', + ['gt2.7', '>3.6', 'eq42', 'has,commas,in,it']), + # 2: one string has commas and spaces within quotes + ('gt2.7, >3.6, eq42, "has some,commas,in,it"', + ['gt2.7', '>3.6', 'eq42', 'has some,commas,in,it']), + # 3: empty string + ('', + []), + # 4: string with commas between ()s + ('name="CLM_NAME"; level="(0,0,*,*)"', + ['name="CLM_NAME"; level="(0,0,*,*)"']), + # 5: string with commas between ()s and commas not between ()s + ('name="CLM_NAME"; level="(0,0,*,*)";, name="OTHER"; level="A06"', + ['name="CLM_NAME"; level="(0,0,*,*)";', 'name="OTHER"; level="A06"']), + # 6: string with commas between ()s within {}s + ('{name="CLM_NAME"; level="(0,0,*,*)";}', + ['{name="CLM_NAME"; level="(0,0,*,*)";}']), + # 7: multiple {}s with string with commas between ()s + ('{name="CLM_NAME"; level="(0,0,*,*)";},{name="CLM_NAME"; level="(0,0,*,*)";}', + ['{name="CLM_NAME"; level="(0,0,*,*)";}', + '{name="CLM_NAME"; level="(0,0,*,*)";}']), + # 8: read example with commas beween ()s + ('-input_field \'name="TEC"; level="({valid?fmt=%Y%m%d_%H%M%S},*,*)"; file_type=NETCDF_NCCF;\'', + ['-input_field \'name="TEC"; level="({valid?fmt=%Y%m%d_%H%M%S},*,*)"; file_type=NETCDF_NCCF;\'']), + # 9: read example commas separating quotes within []s + ('{name="UGRD"; level=["P850","P500","P250"];}', + ['{name="UGRD"; level=["P850","P500","P250"];}']), + # 10: multiples {}s with commas separating quotes within []s + ('{name="UGRD"; level=["P850","P500","P250"];}, {name="UGRD"; level=["P750","P600"];}', + ['{name="UGRD"; level=["P850","P500","P250"];}', '{name="UGRD"; level=["P750","P600"];}']), + ] +) +def test_getlist(string_list, output_list): test_list = getlist(string_list) - assert test_list == ['gt2.7', '>3.6', 'eq42'] + assert test_list == output_list def test_getlist_int(): string_list = '6, 7, 42' test_list = getlistint(string_list) assert test_list == [6, 7, 42] -def test_getlist_has_commas(): - string_list = 'gt2.7, >3.6, eq42, "has,commas,in,it"' - test_list = getlist(string_list) - assert test_list == ['gt2.7', '>3.6', 'eq42', 'has,commas,in,it'] - -def test_getlist_empty(): - string_list = '' - test_list = getlist(string_list) - assert test_list == [] - @pytest.mark.parametrize( 'list_string, output_list', [ ('begin_end_incr(3,12,3)', @@ -84,29 +123,3 @@ def test_getlist_empty(): ) def test_getlist_begin_end_incr(list_string, output_list): assert getlist(list_string) == output_list - -@pytest.mark.parametrize( - 'list_str, expected_fixed_list', [ - ('some,items,here', ['some', - 'items', - 'here']), - ('(*,*)', ['(*,*)']), - ("-type solar_alt -thresh 'ge45' -name solar_altitude_ge_45_mask -input_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;' -mask_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;\'", - ["-type solar_alt -thresh 'ge45' -name solar_altitude_ge_45_mask -input_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;' -mask_field 'name=\"TEC\"; level=\"(0,*,*)\"; file_type=NETCDF_NCCF;\'"]), - ("(*,*),'level=\"(0,*,*)\"' -censor_thresh [lt12.3,gt8.8],other", ['(*,*)', - "'level=\"(0,*,*)\"' -censor_thresh [lt12.3,gt8.8]", - 'other']), - ] -) -def test_fix_list(list_str, expected_fixed_list): - item_list = list(reader([list_str]))[0] - fixed_list = _fix_list(item_list) - print("FIXED LIST:") - for fixed in fixed_list: - print(f"ITEM: {fixed}") - - print("EXPECTED LIST") - for expected in expected_fixed_list: - print(f"ITEM: {expected}") - - assert(fixed_list == expected_fixed_list) diff --git a/metplus/util/config_metplus.py b/metplus/util/config_metplus.py index e2cc3981cd..353eed0d31 100644 --- a/metplus/util/config_metplus.py +++ b/metplus/util/config_metplus.py @@ -23,7 +23,7 @@ from . import met_util as util from .string_template_substitution import get_tags, do_string_sub from .met_util import is_python_script, format_var_items -from .string_manip import getlist +from .string_manip import getlist, remove_quotes from .doc_util import get_wrapper_name """!Creates the initial METplus directory structure, @@ -793,7 +793,7 @@ def getbool(self, sec, name, default=None, badtypeok=False, morevars=None, return False # check if value is y/Y/n/N and return True/False if so - value_string = util.remove_quotes(value_string) + value_string = remove_quotes(value_string) if value_string.lower() == 'y': return True if value_string.lower() == 'n': diff --git a/metplus/util/met_config.py b/metplus/util/met_config.py index e8ef186e94..55e753883d 100644 --- a/metplus/util/met_config.py +++ b/metplus/util/met_config.py @@ -7,7 +7,7 @@ from .string_manip import getlist from .met_util import get_threshold_via_regex, MISSING_DATA_VALUE -from .met_util import remove_quotes as util_remove_quotes +from .string_manip import remove_quotes as util_remove_quotes from .config_metplus import find_indices_in_config_section class METConfig: diff --git a/metplus/util/met_util.py b/metplus/util/met_util.py index 0949726a9e..91e4593b96 100644 --- a/metplus/util/met_util.py +++ b/metplus/util/met_util.py @@ -1052,14 +1052,6 @@ def split_level(level): return '', '' -def remove_quotes(input_string): - """!Remove quotes from string""" - if not input_string: - return '' - - # strip off double and single quotes - return input_string.strip('"').strip("'") - def get_filetype(filepath, logger=None): """!This function determines if the filepath is a NETCDF or GRIB file based on the first eight bytes of the file. diff --git a/metplus/util/string_manip.py b/metplus/util/string_manip.py index f6326d50fb..af1d719016 100644 --- a/metplus/util/string_manip.py +++ b/metplus/util/string_manip.py @@ -7,6 +7,14 @@ import re from csv import reader +def remove_quotes(input_string): + """!Remove quotes from string""" + if not input_string: + return '' + + # strip off double and single quotes + return input_string.strip('"').strip("'") + def getlist(list_str, expand_begin_end_incr=True): """! Returns a list of string elements from a comma separated string of values. @@ -35,11 +43,12 @@ def getlist(list_str, expand_begin_end_incr=True): if expand_begin_end_incr: list_str = _handle_begin_end_incr(list_str) - # use csv reader to divide comma list while preserving strings with comma - # convert the csv reader to a list and get first item - # (which is the whole list) - item_list = list(reader([list_str], escapechar='\\'))[0] + # use regex split to split list string by commas that are not + # found within []s or ()s + item_list = re.split(r',\s*(?![^\[\]]*\]|[^()]*\))', list_str) + # regex split will still split by commas that are found between + # quotation marks, so call function to put them back together properly item_list = _fix_list(item_list) return item_list @@ -128,62 +137,41 @@ def _begin_end_incr_evaluate(item): return None def _fix_list(item_list): - item_list = _fix_list_helper(item_list, '(') - item_list = _fix_list_helper(item_list, '[') - return item_list + """! The logic that calls this function may have incorrectly split up + a string that contains commas within quotation marks. This function + looks through the list and finds items that appear to have been split up + incorrectly and puts them back together properly. -def _fix_list_helper(item_list, type): - if type == '(': - close_regex = r"[^(]+\).*" - open_regex = r".*\([^)]*$" - elif type == '[': - close_regex = r"[^\[]+\].*" - open_regex = r".*\[[^\]]*$" - else: - return item_list - - # combine items that had a comma between ()s or []s + @param item_list list of items to be corrected + @returns corrected list + """ fixed_list = [] - incomplete_item = None - found_close = False - for index, item in enumerate(item_list): - # if we have found an item that ends with ( but - if incomplete_item: - # check if item has ) before ( - match = re.match(close_regex, item) - if match: - # add rest of text, add it to output list, - # then reset incomplete_item - incomplete_item += ',' + item - found_close = True - else: - # if not ) before (, add text and continue - incomplete_item += ',' + item - - match = re.match(open_regex, item) - # if we find ( without ) after it - if match: - # if we are still putting together an item, - # append comma and new item - if incomplete_item: - if not found_close: - incomplete_item += ',' + item - # if not, start new incomplete item to put together + list_buffer = [] + for item in item_list: + quote_count = item.count('"') + if not list_buffer: + # if there are an even number of quotation marks, add to list + if quote_count % 2 == 0: + fixed_list.append(item) + # otherwise add it to the list buffer else: - incomplete_item = item - - found_close = False - # if we don't find ( without ) + list_buffer.append(item) else: - # if we are putting together item, we can add to the - # output list and reset incomplete_item - if incomplete_item: - if found_close: - fixed_list.append(incomplete_item) - incomplete_item = None - # if we are not within brackets and we found no brackets, - # add item to output list - else: - fixed_list.append(item) + list_buffer.append(item) + if quote_count == 1: + fixed_list.append(','.join(list_buffer)) + list_buffer.clear() + + # if there are still items in the buffer, add them to end of list + if list_buffer: + fixed_list.append(','.join(list_buffer)) + + # remove extra quotation marks around string + out_list = [] + for item in fixed_list: + if item[0] == '"' and item[-1] == '"': + out_list.append(item.strip('"')) + else: + out_list.append(item) - return fixed_list + return out_list diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 4eccbb92d3..05e498016c 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -27,6 +27,7 @@ from ..util import MISSING_DATA_VALUE from ..util import get_custom_string_list from ..util import get_wrapped_met_config_file, add_met_config_item, format_met_config +from ..util import remove_quotes from ..util.met_config import add_met_config_dict # pylint:disable=pointless-string-statement @@ -1215,7 +1216,7 @@ def get_field_info(self, d_type='', v_name='', v_level='', v_thresh=None, field = f'name="{v_name}";' if v_level: - field += f' level="{util.remove_quotes(v_level)}";' + field += f' level="{remove_quotes(v_level)}";' if self.c_dict.get(d_type + '_IS_PROB', False): field += " prob=TRUE;" @@ -1474,7 +1475,7 @@ def handle_description(self): # if the value is set, set the DESC c_dict if conf_value: self.env_var_dict['METPLUS_DESC'] = ( - f'desc = "{util.remove_quotes(conf_value)}";' + f'desc = "{remove_quotes(conf_value)}";' ) def get_output_prefix(self, time_info=None, set_env_vars=True): @@ -1536,6 +1537,9 @@ def handle_climo_dict(self): self.add_met_config_dict(dict_name, items) + # handle use_fcst or use_obs options for setting field list + self.climo_use_fcst_or_obs_fields(dict_name) + # handle deprecated env vars CLIMO_MEAN_FILE and CLIMO_STDEV_FILE # that are used by pre v4.0.0 wrapped MET config files env_var_name = f'METPLUS_{dict_name.upper()}_DICT' @@ -1595,6 +1599,38 @@ def read_climo_file_name(self, climo_type): self.config.set('config', f'{prefix}FILE_NAME', template_list_string) + def climo_use_fcst_or_obs_fields(self, dict_name): + """! If climo field is not explicitly set, check if config is set + to use forecast or observation fields. + + @param dict_name name of climo to check: climo_mean or climo_stdev + """ + # if {APP}_CLIMO_[MEAN/STDEV]_FIELD is set, do nothing + field_conf = f'{self.app_name}_{dict_name}_FIELD'.upper() + if self.config.has_option('config', field_conf): + return + + use_fcst_conf = f'{self.app_name}_{dict_name}_USE_FCST'.upper() + use_obs_conf = f'{self.app_name}_{dict_name}_USE_OBS'.upper() + + use_fcst = self.config.getbool('config', use_fcst_conf, False) + use_obs = self.config.getbool('config', use_obs_conf, False) + + # if both are set, report an error + if use_fcst and use_obs: + self.log_error(f'Cannot set both {use_fcst_conf} and ' + f'{use_obs_conf} in config.') + return + + # if neither are set, do nothing + if not use_fcst and not use_obs: + return + + env_var_name = f'METPLUS_{dict_name.upper()}_DICT' + rvalue = 'fcst' if use_fcst else 'obs' + + self.env_var_dict[env_var_name] += f'{dict_name} = {rvalue};' + def get_wrapper_or_generic_config(self, generic_config_name): """! Check for config variable with _ prepended first. If set use that value. If not, check for config without prefix. diff --git a/metplus/wrappers/plot_data_plane_wrapper.py b/metplus/wrappers/plot_data_plane_wrapper.py index 2ed866c54d..3eb642037f 100755 --- a/metplus/wrappers/plot_data_plane_wrapper.py +++ b/metplus/wrappers/plot_data_plane_wrapper.py @@ -15,7 +15,7 @@ from ..util import met_util as util from ..util import time_util from . import CommandBuilder -from ..util import do_string_sub +from ..util import do_string_sub, remove_quotes '''!@namespace PlotDataPlaneWrapper @brief Wraps the PlotDataPlane tool to plot data @@ -187,7 +187,7 @@ def set_command_line_arguments(self, time_info): **time_info) field_info = f" 'name=\"{field_name}\";" if self.c_dict['FIELD_LEVEL']: - field_level = util.remove_quotes(self.c_dict['FIELD_LEVEL']) + field_level = remove_quotes(self.c_dict['FIELD_LEVEL']) field_info += f" level=\"{field_level}\";" if self.c_dict['FIELD_EXTRA']: diff --git a/metplus/wrappers/regrid_data_plane_wrapper.py b/metplus/wrappers/regrid_data_plane_wrapper.py index a3d544a0db..58588c4ceb 100755 --- a/metplus/wrappers/regrid_data_plane_wrapper.py +++ b/metplus/wrappers/regrid_data_plane_wrapper.py @@ -17,6 +17,7 @@ from ..util import do_string_sub from ..util import parse_var_list from ..util import get_process_list +from ..util import remove_quotes from . import ReformatGriddedWrapper # pylint:disable=pointless-string-statement @@ -383,7 +384,7 @@ def set_field_command_line_arguments(self, field_info, data_type): field_name = field_info[f'{data_type.lower()}_name'] # strip off quotes around input_level if found - input_level = util.remove_quotes(field_info[f'{data_type.lower()}_level']) + input_level = remove_quotes(field_info[f'{data_type.lower()}_level']) field_text = f"-field 'name=\"{field_name}\";" diff --git a/metplus/wrappers/stat_analysis_wrapper.py b/metplus/wrappers/stat_analysis_wrapper.py index d95bb32305..5c8fc82256 100755 --- a/metplus/wrappers/stat_analysis_wrapper.py +++ b/metplus/wrappers/stat_analysis_wrapper.py @@ -21,7 +21,7 @@ from ..util import getlist from ..util import met_util as util from ..util import do_string_sub, find_indices_in_config_section -from ..util import parse_var_list +from ..util import parse_var_list, remove_quotes from . import CommandBuilder class StatAnalysisWrapper(CommandBuilder): @@ -1325,7 +1325,7 @@ def get_level_list(self, data_type): for level in level_input: level = level.strip('(').strip(')') - level = f'{util.remove_quotes(level)}' + level = f'{remove_quotes(level)}' level_list.append(level) return level_list @@ -1778,7 +1778,7 @@ def run_stat_analysis_job(self, runtime_settings_dict_list): for mp_item in mp_items: if not runtime_settings_dict.get(mp_item, ''): continue - value = util.remove_quotes(runtime_settings_dict.get(mp_item, + value = remove_quotes(runtime_settings_dict.get(mp_item, '')) value = (f"{mp_item.lower()} = \"{value}\";") self.env_var_dict[f'METPLUS_{mp_item}'] = value diff --git a/metplus/wrappers/tcmpr_plotter_wrapper.py b/metplus/wrappers/tcmpr_plotter_wrapper.py index 3a3b6fb6e7..a1186c43f8 100755 --- a/metplus/wrappers/tcmpr_plotter_wrapper.py +++ b/metplus/wrappers/tcmpr_plotter_wrapper.py @@ -11,6 +11,7 @@ from ..util import time_util from ..util import do_string_sub from ..util import time_generator +from ..util import remove_quotes from . import CommandBuilder class TCMPRPlotterWrapper(CommandBuilder): @@ -160,7 +161,7 @@ def read_optional_args(self): if value: # add quotes around value if they are not already there if 'quotes' in data_type: - value = f'"{util.remove_quotes(value)}"' + value = f'"{remove_quotes(value)}"' # don't add value for boolean if data_type == 'bool': From 99e56cd0744256dd24839e081651fc1b530b92dc Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 1 Feb 2022 15:41:54 -0700 Subject: [PATCH 278/770] feature 1356 isolate config (#1386) --- docs/Contributors_Guide/basic_components.rst | 6 ++-- docs/Contributors_Guide/create_wrapper.rst | 6 ++-- .../pytests/ascii2nc/test_ascii2nc_wrapper.py | 8 ++++- .../command_builder/test_command_builder.py | 33 ------------------- .../pytests/tc_stat/test_tc_stat_wrapper.py | 9 +++-- metplus/wrappers/ascii2nc_wrapper.py | 6 ++-- metplus/wrappers/command_builder.py | 14 +------- metplus/wrappers/compare_gridded_wrapper.py | 6 ++-- metplus/wrappers/cyclone_plotter_wrapper.py | 18 +++++----- metplus/wrappers/ensemble_stat_wrapper.py | 6 ++-- metplus/wrappers/example_wrapper.py | 6 ++-- metplus/wrappers/extract_tiles_wrapper.py | 16 ++++++--- metplus/wrappers/gempak_to_cf_wrapper.py | 6 ++-- metplus/wrappers/gen_ens_prod_wrapper.py | 6 ++-- metplus/wrappers/gen_vx_mask_wrapper.py | 6 ++-- metplus/wrappers/gfdl_tracker_wrapper.py | 6 ++-- metplus/wrappers/grid_diag_wrapper.py | 6 ++-- metplus/wrappers/grid_stat_wrapper.py | 6 ++-- metplus/wrappers/ioda2nc_wrapper.py | 6 ++-- metplus/wrappers/loop_times_wrapper.py | 6 ++-- metplus/wrappers/make_plots_wrapper.py | 6 ++-- metplus/wrappers/met_db_load_wrapper.py | 6 ++-- metplus/wrappers/mode_wrapper.py | 6 ++-- metplus/wrappers/mtd_wrapper.py | 6 ++-- metplus/wrappers/pb2nc_wrapper.py | 6 ++-- metplus/wrappers/pcp_combine_wrapper.py | 6 ++-- metplus/wrappers/plot_data_plane_wrapper.py | 6 ++-- metplus/wrappers/point2grid_wrapper.py | 6 ++-- metplus/wrappers/point_stat_wrapper.py | 6 ++-- metplus/wrappers/py_embed_ingest_wrapper.py | 17 ++++++---- metplus/wrappers/reformat_gridded_wrapper.py | 6 ++-- metplus/wrappers/regrid_data_plane_wrapper.py | 6 ++-- metplus/wrappers/runtime_freq_wrapper.py | 6 ++-- metplus/wrappers/series_analysis_wrapper.py | 14 +++++--- metplus/wrappers/stat_analysis_wrapper.py | 6 ++-- metplus/wrappers/tc_gen_wrapper.py | 6 ++-- metplus/wrappers/tc_pairs_wrapper.py | 6 ++-- metplus/wrappers/tc_stat_wrapper.py | 6 ++-- metplus/wrappers/tcmpr_plotter_wrapper.py | 6 ++-- metplus/wrappers/tcrmw_wrapper.py | 7 ++-- metplus/wrappers/usage_wrapper.py | 6 ++-- metplus/wrappers/user_script_wrapper.py | 6 ++-- 42 files changed, 123 insertions(+), 211 deletions(-) diff --git a/docs/Contributors_Guide/basic_components.rst b/docs/Contributors_Guide/basic_components.rst index 7de9af11d8..487d89a66f 100644 --- a/docs/Contributors_Guide/basic_components.rst +++ b/docs/Contributors_Guide/basic_components.rst @@ -33,13 +33,11 @@ executable, relative to MET_BIN_DIR. The init function also calls the parent's initialization function using super() function:: - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "ascii2nc" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) The above code block is an excerpt from the ASCII2NCWrapper, found in metplus/wrappers/ascii2nc_wrapper.py. diff --git a/docs/Contributors_Guide/create_wrapper.rst b/docs/Contributors_Guide/create_wrapper.rst index e051a04211..bf016d3aac 100644 --- a/docs/Contributors_Guide/create_wrapper.rst +++ b/docs/Contributors_Guide/create_wrapper.rst @@ -96,13 +96,11 @@ If the application is a MET tool, then set self.app_path to the full path of the tool under **MET_BIN_DIR**. See the Basic Components :ref:`bc_init_function` section for more information:: - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'new_tool' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) Read Configuration Variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py index b7c3b72767..e746de6be9 100644 --- a/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py +++ b/internal_tests/pytests/ascii2nc/test_ascii2nc_wrapper.py @@ -28,8 +28,14 @@ def ascii2nc_wrapper(metplus_config, config_path=None, config_overrides=None): for key, value in config_overrides.items(): overrides[key] = value + instance = 'overrides' + if not config.has_section(instance): + config.add_section(instance) + for key, value in overrides.items(): + config.set(instance, key, value) + return ASCII2NCWrapper(config, - config_overrides=overrides) + instance=instance) @pytest.mark.parametrize( 'config_overrides, env_var_values', [ diff --git a/internal_tests/pytests/command_builder/test_command_builder.py b/internal_tests/pytests/command_builder/test_command_builder.py index 355cb66b87..98aed81ffd 100644 --- a/internal_tests/pytests/command_builder/test_command_builder.py +++ b/internal_tests/pytests/command_builder/test_command_builder.py @@ -236,39 +236,6 @@ def test_find_obs_dated_next_day(metplus_config): obs_file = pcw.find_obs(time_info, v) assert obs_file == pcw.c_dict['OBS_INPUT_DIR']+'/20180202/20180202_0013' -@pytest.mark.parametrize( - 'overrides, c_dict', [ - ({'LOG_MET_VERBOSITY': '5', }, # string - {'VERBOSITY': '5', }), - ({'CUSTOM_LOOP_LIST': 'a,b,c', }, # list - {'CUSTOM_LOOP_LIST': ['a', 'b', 'c'], }), - ({'SKIP_TIMES': '"%H:12,18", "%Y%m%d:20200201"', }, # dict - {'SKIP_TIMES': {'%H': ['12', '18'], - '%Y%m%d': ['20200201'], }}), - ] -) -def test_override_config_in_c_dict(metplus_config, overrides, c_dict): - config = metplus_config() - - pcw = CommandBuilder(config, config_overrides=overrides) - for key, expected_value in c_dict.items(): - assert pcw.c_dict.get(key) == expected_value - -@pytest.mark.parametrize( - 'overrides', [ - ({'LOG_MET_VERBOSITY': '5', }), - ({'CUSTOM_LOOP_LIST': 'a,b,c', }), - ({'SKIP_TIMES': '"%H:12,18", "%Y%m%d:20200201"', }), - ({'FAKE_TEMPLATE': '{valid?fmt=%Y%m%d%H}', }), - ] -) -def test_override_config(metplus_config, overrides): - config = metplus_config() - - pcw = CommandBuilder(config, config_overrides=overrides) - for key, expected_value in overrides.items(): - assert pcw.config.getraw('config', key) == expected_value - # dictionary items with values will be set in [test_section] # items with value None will not be set, so it should use # the value in [config], which is always 'default' diff --git a/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py b/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py index b464c2aa85..e84b90b09a 100644 --- a/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py +++ b/internal_tests/pytests/tc_stat/test_tc_stat_wrapper.py @@ -108,8 +108,13 @@ def tc_stat_wrapper(metplus_config): ] ) def test_override_config_in_c_dict(metplus_config, overrides, c_dict): - wrapper = TCStatWrapper(get_config(metplus_config), - config_overrides=overrides) + config = get_config(metplus_config) + instance = 'tc_stat_overrides' + if not config.has_section(instance): + config.add_section(instance) + for key, value in overrides.items(): + config.set(instance, key, value) + wrapper = TCStatWrapper(config, instance=instance) for key, expected_value in c_dict.items(): assert (wrapper.env_var_dict.get(f'METPLUS_{key}') == expected_value or wrapper.c_dict.get(key) == expected_value) diff --git a/metplus/wrappers/ascii2nc_wrapper.py b/metplus/wrappers/ascii2nc_wrapper.py index e68d78ac23..3e110e01a6 100755 --- a/metplus/wrappers/ascii2nc_wrapper.py +++ b/metplus/wrappers/ascii2nc_wrapper.py @@ -29,13 +29,11 @@ class ASCII2NCWrapper(CommandBuilder): 'METPLUS_TIME_SUMMARY_DICT', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "ascii2nc" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 05e498016c..46ebd54b35 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -51,7 +51,7 @@ class CommandBuilder: # name of variable to hold any MET config overrides MET_OVERRIDES_KEY = 'METPLUS_MET_CONFIG_OVERRIDES' - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.isOK = True self.errors = 0 self.config = config @@ -90,9 +90,6 @@ def __init__(self, config, instance=None, config_overrides=None): self.instance = instance - # override config if any were supplied - self.override_config(config_overrides) - self.env = os.environ.copy() if hasattr(config, 'env'): self.env = config.env @@ -140,15 +137,6 @@ def __init__(self, config, instance=None, config_overrides=None): self.clear() - def override_config(self, config_overrides): - if not config_overrides: - return - - self.logger.debug("Overriding config with explicit values:") - for key, value in config_overrides.items(): - self.logger.debug(f"Setting [config] {key} = {value}") - self.config.set('config', key, value) - def check_for_unused_env_vars(self): config_file = self.c_dict.get('CONFIG_FILE') if not config_file: diff --git a/metplus/wrappers/compare_gridded_wrapper.py b/metplus/wrappers/compare_gridded_wrapper.py index e9c4e2bafe..66caa20ec0 100755 --- a/metplus/wrappers/compare_gridded_wrapper.py +++ b/metplus/wrappers/compare_gridded_wrapper.py @@ -31,14 +31,12 @@ class CompareGriddedWrapper(CommandBuilder): that reformat gridded data """ - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): # set app_name if not set by child class to allow tests to run on this wrapper if not hasattr(self, 'app_name'): self.app_name = 'compare_gridded' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) # check to make sure all necessary probabilistic settings are set correctly # this relies on the subclass to finish creating the c_dict, so it has to # be checked after that happens diff --git a/metplus/wrappers/cyclone_plotter_wrapper.py b/metplus/wrappers/cyclone_plotter_wrapper.py index e401c9bd88..5edc6333e5 100644 --- a/metplus/wrappers/cyclone_plotter_wrapper.py +++ b/metplus/wrappers/cyclone_plotter_wrapper.py @@ -1,5 +1,5 @@ -"""!@namespace ExtraTropicalCyclonePlotter +"""!@namespace CyclonePlotter A Python class that generates plots of extra tropical cyclone forecast data, replicating the NCEP tropical and extra tropical cyclone tracks and verification plots http://www.emc.ncep.noaa.gov/mmb/gplou/emchurr/glblgen/ @@ -25,11 +25,13 @@ import cartopy from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER - ##If the script is run on a limited-internet access machine, the CARTOPY_DIR environment setting - ##will need to be set in the user-specific system configuration file. Review the Installation section - ##of the User's Guide for more details. + # If the script is run on a limited-internet access machine, + # the CARTOPY_DIR environment setting + # will need to be set in the user-specific system configuration file. + # Review the Installation section of the User's Guide for more details. if os.getenv('CARTOPY_DIR'): - cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', cartopy.config.get('data_dir')) + cartopy.config['data_dir'] = os.getenv('CARTOPY_DIR', + cartopy.config.get('data_dir')) except Exception as err_msg: WRAPPER_CANNOT_RUN = True @@ -48,12 +50,10 @@ class CyclonePlotterWrapper(CommandBuilder): Reads input from ATCF files generated from MET TC-Pairs """ - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'cyclone_plotter' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) if WRAPPER_CANNOT_RUN: self.log_error("There was a problem importing modules: " diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index b3762465b6..453946a3b5 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -100,13 +100,11 @@ class EnsembleStatWrapper(CompareGriddedWrapper): 'weight', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'ensemble_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """!Create a dictionary containing the values set in the config file diff --git a/metplus/wrappers/example_wrapper.py b/metplus/wrappers/example_wrapper.py index 821365b72d..04f8ddcd07 100755 --- a/metplus/wrappers/example_wrapper.py +++ b/metplus/wrappers/example_wrapper.py @@ -18,11 +18,9 @@ class ExampleWrapper(CommandBuilder): """!Wrapper can be used as a base to develop a new wrapper""" - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'example' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/extract_tiles_wrapper.py b/metplus/wrappers/extract_tiles_wrapper.py index c04d995806..b0e3f99824 100755 --- a/metplus/wrappers/extract_tiles_wrapper.py +++ b/metplus/wrappers/extract_tiles_wrapper.py @@ -50,11 +50,9 @@ class ExtractTilesWrapper(CommandBuilder): 'MTD': 'OBJECT_CAT', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'extract_tiles' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.regrid_data_plane = self.regrid_data_plane_init() def create_c_dict(self): @@ -187,8 +185,16 @@ def regrid_data_plane_init(self): ) overrides[f'{rdp}_ONCE_PER_FIELD'] = False overrides[f'{rdp}_MANDATORY'] = False + + # set all config variables in a new section + instance = 'extract_tiles_rdp' + if not self.config.has_section(instance): + self.config.add_section(instance) + for key, value in overrides.items(): + self.config.set(instance, key, value) + rdp_wrapper = RegridDataPlaneWrapper(self.config, - config_overrides=overrides) + instance=instance) rdp_wrapper.c_dict['SHOW_WARNINGS'] = False return rdp_wrapper diff --git a/metplus/wrappers/gempak_to_cf_wrapper.py b/metplus/wrappers/gempak_to_cf_wrapper.py index 6813282c9e..6f618427c9 100755 --- a/metplus/wrappers/gempak_to_cf_wrapper.py +++ b/metplus/wrappers/gempak_to_cf_wrapper.py @@ -24,12 +24,10 @@ class GempakToCFWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "GempakToCF" self.app_path = config.getstr('exe', 'GEMPAKTOCF_JAR', '') - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """!Create dictionary from config items to be used in the wrapper diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index 5df8bbb1c3..aeb6e706fd 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -51,13 +51,11 @@ class GenEnsProdWrapper(LoopTimesWrapper): 'weight', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'gen_ens_prod' self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/gen_vx_mask_wrapper.py b/metplus/wrappers/gen_vx_mask_wrapper.py index ac5459ec1d..b3ad0eeb2c 100755 --- a/metplus/wrappers/gen_vx_mask_wrapper.py +++ b/metplus/wrappers/gen_vx_mask_wrapper.py @@ -26,13 +26,11 @@ class GenVxMaskWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "gen_vx_mask" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/gfdl_tracker_wrapper.py b/metplus/wrappers/gfdl_tracker_wrapper.py index 4cf8f9176b..ff192bb179 100755 --- a/metplus/wrappers/gfdl_tracker_wrapper.py +++ b/metplus/wrappers/gfdl_tracker_wrapper.py @@ -117,11 +117,9 @@ class GFDLTrackerWrapper(CommandBuilder): "VERBOSE_VERB_G2": "int", } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'gfdl_tracker' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/grid_diag_wrapper.py b/metplus/wrappers/grid_diag_wrapper.py index d6d306f758..dcdd3501b8 100755 --- a/metplus/wrappers/grid_diag_wrapper.py +++ b/metplus/wrappers/grid_diag_wrapper.py @@ -35,13 +35,11 @@ class GridDiagWrapper(RuntimeFreqWrapper): 'METPLUS_MASK_DICT', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "grid_diag" self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/grid_stat_wrapper.py b/metplus/wrappers/grid_stat_wrapper.py index 35451c9ff2..86ea6dfbfa 100755 --- a/metplus/wrappers/grid_stat_wrapper.py +++ b/metplus/wrappers/grid_stat_wrapper.py @@ -96,13 +96,11 @@ class GridStatWrapper(CompareGriddedWrapper): 'apply_mask', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'grid_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/ioda2nc_wrapper.py b/metplus/wrappers/ioda2nc_wrapper.py index bfdd2e42df..4ff71addea 100755 --- a/metplus/wrappers/ioda2nc_wrapper.py +++ b/metplus/wrappers/ioda2nc_wrapper.py @@ -34,13 +34,11 @@ class IODA2NCWrapper(LoopTimesWrapper): 'METPLUS_TIME_SUMMARY_DICT', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "ioda2nc" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Read METplusConfig object and sets values in dictionary to be diff --git a/metplus/wrappers/loop_times_wrapper.py b/metplus/wrappers/loop_times_wrapper.py index 87781e76a2..9b7b4fae9d 100755 --- a/metplus/wrappers/loop_times_wrapper.py +++ b/metplus/wrappers/loop_times_wrapper.py @@ -15,14 +15,12 @@ class LoopTimesWrapper(RuntimeFreqWrapper): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): # set app_name if not set by child class to allow tests to run if not hasattr(self, 'app_name'): self.app_name = 'loop_times' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/make_plots_wrapper.py b/metplus/wrappers/make_plots_wrapper.py index 43d3183633..29bbcc8aac 100755 --- a/metplus/wrappers/make_plots_wrapper.py +++ b/metplus/wrappers/make_plots_wrapper.py @@ -70,12 +70,10 @@ class MakePlotsWrapper(CommandBuilder): 'VERIF_GRID', 'EVENT_EQUALIZATION', 'LOG_METPLUS', 'LOG_LEVEL' ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_path = 'python' self.app_name = 'make_plots' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) if WRAPPER_CANNOT_RUN: self.log_error(f"There was a problem importing modules: {EXCEPTION_ERR}\n") diff --git a/metplus/wrappers/met_db_load_wrapper.py b/metplus/wrappers/met_db_load_wrapper.py index 8437229c94..65f075792b 100755 --- a/metplus/wrappers/met_db_load_wrapper.py +++ b/metplus/wrappers/met_db_load_wrapper.py @@ -44,16 +44,14 @@ class METDbLoadWrapper(RuntimeFreqWrapper): 'LOAD_MPR': 'bool', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): met_data_db_dir = config.getdir('MET_DATA_DB_DIR') self.app_path = os.path.join(met_data_db_dir, 'METdbLoad', 'ush', 'met_db_load') self.app_name = os.path.basename(self.app_path) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/mode_wrapper.py b/metplus/wrappers/mode_wrapper.py index 23f958ac10..edd59aeb4c 100755 --- a/metplus/wrappers/mode_wrapper.py +++ b/metplus/wrappers/mode_wrapper.py @@ -96,15 +96,13 @@ class MODEWrapper(CompareGriddedWrapper): '(400.0/grid_res,0.0))'), } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): # only set app variables if not already set by MTD (subclass) if not hasattr(self, 'app_name'): self.app_name = 'mode' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def add_merge_config_file(self, time_info): """!If merge config file is defined, add it to the command""" diff --git a/metplus/wrappers/mtd_wrapper.py b/metplus/wrappers/mtd_wrapper.py index 6bf2bc19be..beb5eddd5c 100755 --- a/metplus/wrappers/mtd_wrapper.py +++ b/metplus/wrappers/mtd_wrapper.py @@ -37,13 +37,11 @@ class MTDWrapper(CompareGriddedWrapper): 'METPLUS_OUTPUT_PREFIX', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'mtd' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.fcst_file = None self.obs_file = None diff --git a/metplus/wrappers/pb2nc_wrapper.py b/metplus/wrappers/pb2nc_wrapper.py index 31828f145b..9fb28f4a3c 100755 --- a/metplus/wrappers/pb2nc_wrapper.py +++ b/metplus/wrappers/pb2nc_wrapper.py @@ -39,13 +39,11 @@ class PB2NCWrapper(CommandBuilder): 'METPLUS_OBS_PREPBUFR_MAP', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'pb2nc' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Create a data structure (dictionary) that contains all the diff --git a/metplus/wrappers/pcp_combine_wrapper.py b/metplus/wrappers/pcp_combine_wrapper.py index c23993e401..8afbc171a6 100755 --- a/metplus/wrappers/pcp_combine_wrapper.py +++ b/metplus/wrappers/pcp_combine_wrapper.py @@ -26,13 +26,11 @@ class PCPCombineWrapper(ReformatGriddedWrapper): # valid values for [FCST/OBS]_PCP_COMBINE_METHOD valid_run_methods = ['ADD', 'SUM', 'SUBTRACT', 'DERIVE', 'USER_DEFINED'] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'pcp_combine' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Create dictionary from config items to be used in the wrapper diff --git a/metplus/wrappers/plot_data_plane_wrapper.py b/metplus/wrappers/plot_data_plane_wrapper.py index 3eb642037f..71d1b61eda 100755 --- a/metplus/wrappers/plot_data_plane_wrapper.py +++ b/metplus/wrappers/plot_data_plane_wrapper.py @@ -24,13 +24,11 @@ class PlotDataPlaneWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "plot_data_plane" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/point2grid_wrapper.py b/metplus/wrappers/point2grid_wrapper.py index 5c71eebba7..f9ba942df1 100755 --- a/metplus/wrappers/point2grid_wrapper.py +++ b/metplus/wrappers/point2grid_wrapper.py @@ -25,13 +25,11 @@ class Point2GridWrapper(CommandBuilder): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "point2grid" self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/point_stat_wrapper.py b/metplus/wrappers/point_stat_wrapper.py index 669ab8252c..2ea826e33b 100755 --- a/metplus/wrappers/point_stat_wrapper.py +++ b/metplus/wrappers/point_stat_wrapper.py @@ -74,13 +74,11 @@ class PointStatWrapper(CompareGriddedWrapper): 'orank', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'point_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): """! Create a dictionary that holds all the values set in the diff --git a/metplus/wrappers/py_embed_ingest_wrapper.py b/metplus/wrappers/py_embed_ingest_wrapper.py index 5cbb4b229a..c59847deda 100755 --- a/metplus/wrappers/py_embed_ingest_wrapper.py +++ b/metplus/wrappers/py_embed_ingest_wrapper.py @@ -24,11 +24,9 @@ class PyEmbedIngestWrapper(CommandBuilder): """!Wrapper to utilize Python Embedding in the MET tools to read in data using a python script""" - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'py_embed_ingest' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() @@ -100,12 +98,17 @@ def create_c_dict(self): c_dict['INGESTERS'].append(ingester_dict) - skip = c_dict['SKIP_IF_OUTPUT_EXISTS'] - config_overrides = {'REGRID_DATA_PLANE_SKIP_IF_OUTPUT_EXISTS': skip} + # set config values for RegridDataPlane instance + instance = 'py_embed_ingest_rdp' + if not self.config.has_section(instance): + self.config.add_section(instance) + self.config.set(instance, + 'REGRID_DATA_PLANE_SKIP_IF_OUTPUT_EXISTS', + c_dict['SKIP_IF_OUTPUT_EXISTS']) c_dict['regrid_data_plane'] = ( RegridDataPlaneWrapper(self.config, - config_overrides=config_overrides) + instance=instance) ) return c_dict diff --git a/metplus/wrappers/reformat_gridded_wrapper.py b/metplus/wrappers/reformat_gridded_wrapper.py index feafd54384..da38e4f1de 100755 --- a/metplus/wrappers/reformat_gridded_wrapper.py +++ b/metplus/wrappers/reformat_gridded_wrapper.py @@ -31,10 +31,8 @@ class ReformatGriddedWrapper(CommandBuilder): """! Common functionality to wrap similar MET applications that reformat gridded data """ - def __init__(self, config, instance=None, config_overrides=None): - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + def __init__(self, config, instance=None): + super().__init__(config, instance=instance) # this class should not be called directly # pylint:disable=unused-argument diff --git a/metplus/wrappers/regrid_data_plane_wrapper.py b/metplus/wrappers/regrid_data_plane_wrapper.py index 58588c4ceb..b673a9d128 100755 --- a/metplus/wrappers/regrid_data_plane_wrapper.py +++ b/metplus/wrappers/regrid_data_plane_wrapper.py @@ -28,13 +28,11 @@ class RegridDataPlaneWrapper(ReformatGriddedWrapper): '''! Wraps the MET tool regrid_data_plane to reformat gridded datasets ''' - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'regrid_data_plane' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/runtime_freq_wrapper.py b/metplus/wrappers/runtime_freq_wrapper.py index 105bc552e9..adfaec06a9 100755 --- a/metplus/wrappers/runtime_freq_wrapper.py +++ b/metplus/wrappers/runtime_freq_wrapper.py @@ -34,10 +34,8 @@ class RuntimeFreqWrapper(CommandBuilder): 'RUN_ONCE_FOR_EACH' ] - def __init__(self, config, instance=None, config_overrides=None): - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + def __init__(self, config, instance=None): + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index 578dad2736..d4e48f95f9 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -81,14 +81,12 @@ class SeriesAnalysisWrapper(RuntimeFreqWrapper): 'prc', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'series_analysis' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) if self.c_dict['GENERATE_PLOTS']: self.plot_data_plane = self._plot_data_plane_init() @@ -371,8 +369,14 @@ def _plot_data_plane_init(self): "map_data={ source=[];}" ) + instance = 'plot_data_plane_sa' + if not self.config.has_section(instance): + self.config.add_section(instance) + for key, value in plot_overrides.items(): + self.config.set(instance, key, value) + pdp_wrapper = PlotDataPlaneWrapper(self.config, - config_overrides=plot_overrides) + instance=instance) return pdp_wrapper def clear(self): diff --git a/metplus/wrappers/stat_analysis_wrapper.py b/metplus/wrappers/stat_analysis_wrapper.py index 5c8fc82256..61d41c4cf3 100755 --- a/metplus/wrappers/stat_analysis_wrapper.py +++ b/metplus/wrappers/stat_analysis_wrapper.py @@ -113,13 +113,11 @@ class StatAnalysisWrapper(CommandBuilder): 'FCST_INIT_HOUR_LIST', 'OBS_INIT_HOUR_LIST' ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), 'stat_analysis') self.app_name = os.path.basename(self.app_path) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def get_command(self): diff --git a/metplus/wrappers/tc_gen_wrapper.py b/metplus/wrappers/tc_gen_wrapper.py index 3f6393b841..70b31df854 100755 --- a/metplus/wrappers/tc_gen_wrapper.py +++ b/metplus/wrappers/tc_gen_wrapper.py @@ -88,13 +88,11 @@ class TCGenWrapper(CommandBuilder): ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "tc_gen" self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/tc_pairs_wrapper.py b/metplus/wrappers/tc_pairs_wrapper.py index 369779ed43..424b25a4af 100755 --- a/metplus/wrappers/tc_pairs_wrapper.py +++ b/metplus/wrappers/tc_pairs_wrapper.py @@ -76,13 +76,11 @@ class TCPairsWrapper(CommandBuilder): 'cyclone': r'[0-9]{2,4}', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'tc_pairs' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.adeck = [] self.bdeck = [] self.edeck = [] diff --git a/metplus/wrappers/tc_stat_wrapper.py b/metplus/wrappers/tc_stat_wrapper.py index bb3c33e62c..cbe3379144 100755 --- a/metplus/wrappers/tc_stat_wrapper.py +++ b/metplus/wrappers/tc_stat_wrapper.py @@ -79,14 +79,12 @@ class TCStatWrapper(CommandBuilder): 'METPLUS_INIT_STR_EXC_VAL', ] - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'tc_stat' self.app_path = os.path.join(config.getdir('MET_BIN_DIR', ''), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) self.logger.debug("Initialized TCStatWrapper") def create_c_dict(self): diff --git a/metplus/wrappers/tcmpr_plotter_wrapper.py b/metplus/wrappers/tcmpr_plotter_wrapper.py index a1186c43f8..b60b697706 100755 --- a/metplus/wrappers/tcmpr_plotter_wrapper.py +++ b/metplus/wrappers/tcmpr_plotter_wrapper.py @@ -59,12 +59,10 @@ class TCMPRPlotterWrapper(CommandBuilder): 'save': 'bool', } - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'tcmpr_plotter' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/tcrmw_wrapper.py b/metplus/wrappers/tcrmw_wrapper.py index a5ce97553d..7c0e438d70 100755 --- a/metplus/wrappers/tcrmw_wrapper.py +++ b/metplus/wrappers/tcrmw_wrapper.py @@ -47,13 +47,12 @@ class TCRMWWrapper(CommandBuilder): 'METPLUS_DELTA_RANGE_KM', 'METPLUS_RMW_SCALE', ] - def __init__(self, config, instance=None, config_overrides=None): + + def __init__(self, config, instance=None): self.app_name = "tc_rmw" self.app_path = os.path.join(config.getdir('MET_BIN_DIR'), self.app_name) - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() diff --git a/metplus/wrappers/usage_wrapper.py b/metplus/wrappers/usage_wrapper.py index 11a2ef8ba4..d3fb8cf852 100644 --- a/metplus/wrappers/usage_wrapper.py +++ b/metplus/wrappers/usage_wrapper.py @@ -10,11 +10,9 @@ class UsageWrapper(CommandBuilder): """! A default process, prints out usage when nothing is defined in the PROCESS_LIST """ - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = 'Usage' - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) # get unique list of processes from met_util self.available_processes = list(set(val for val in LOWER_TO_WRAPPER_NAME.values())) self.available_processes.sort() diff --git a/metplus/wrappers/user_script_wrapper.py b/metplus/wrappers/user_script_wrapper.py index 89934edb64..de420fd861 100755 --- a/metplus/wrappers/user_script_wrapper.py +++ b/metplus/wrappers/user_script_wrapper.py @@ -24,11 +24,9 @@ ''' class UserScriptWrapper(RuntimeFreqWrapper): - def __init__(self, config, instance=None, config_overrides=None): + def __init__(self, config, instance=None): self.app_name = "user_script" - super().__init__(config, - instance=instance, - config_overrides=config_overrides) + super().__init__(config, instance=instance) def create_c_dict(self): c_dict = super().create_c_dict() From 4a0afcf9c8b89e338b9e73e9239bad92f05f9b9e Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 2 Feb 2022 13:35:32 -0700 Subject: [PATCH 279/770] Per #1402, added additional comments to config file including links to documentation. Added old format version of config file that contain many comments with the _DIR/_TEMPLATE variables grouped together for comparison --- .../met_tool_wrapper/GridStat/GridStat.conf | 17 ++ .../GridStat/GridStat_comments.conf | 250 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 parm/use_cases/met_tool_wrapper/GridStat/GridStat_comments.conf diff --git a/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf b/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf index e4316e8641..66c69c9f18 100644 --- a/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf +++ b/parm/use_cases/met_tool_wrapper/GridStat/GridStat.conf @@ -1,9 +1,23 @@ [config] +# For additional information, please see the METplus Users Guide. + +### +# Processes to run +# https://metplus.readthedocs.io/en/latest/Users_Guide/systemconfiguration.html#process-list +### + PROCESS_LIST = GridStat ### # Time Info +# LOOP_BY options are INIT, VALID, RETRO, and REALTIME +# If set to INIT or RETRO: +# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set +# If set to VALID or REALTIME: +# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set +# LEAD_SEQ is the list of forecast leads to process +# https://metplus.readthedocs.io/en/latest/Users_Guide/systemconfiguration.html#timing-control ### LOOP_BY = INIT @@ -18,6 +32,7 @@ LOOP_ORDER = times ### # File I/O +# https://metplus.readthedocs.io/en/latest/Users_Guide/systemconfiguration.html#directory-and-filename-template-info ### FCST_GRID_STAT_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst @@ -38,6 +53,7 @@ GRID_STAT_OUTPUT_TEMPLATE = {init?fmt=%Y%m%d%H} ### # Field Info +# https://metplus.readthedocs.io/en/latest/Users_Guide/systemconfiguration.html#field-info ### MODEL = WRF @@ -55,6 +71,7 @@ OBS_VAR1_THRESH = gt12.7, gt25.4, gt50.8, gt76.2 ### # GridStat +# https://metplus.readthedocs.io/en/latest/Users_Guide/wrappers.html#gridstat ### #LOG_GRID_STAT_VERBOSITY = 2 diff --git a/parm/use_cases/met_tool_wrapper/GridStat/GridStat_comments.conf b/parm/use_cases/met_tool_wrapper/GridStat/GridStat_comments.conf new file mode 100644 index 0000000000..0f2b4825bd --- /dev/null +++ b/parm/use_cases/met_tool_wrapper/GridStat/GridStat_comments.conf @@ -0,0 +1,250 @@ +# GridStat METplus Configuration + +# section heading for [config] variables - all items below this line and +# before the next section heading correspond to the [config] section +[config] + +# List of applications to run - only GridStat for this case +PROCESS_LIST = GridStat + +# time looping - options are INIT, VALID, RETRO, and REALTIME +# If set to INIT or RETRO: +# INIT_TIME_FMT, INIT_BEG, INIT_END, and INIT_INCREMENT must also be set +# If set to VALID or REALTIME: +# VALID_TIME_FMT, VALID_BEG, VALID_END, and VALID_INCREMENT must also be set +LOOP_BY = INIT + +# Format of INIT_BEG and INT_END using % items +# %Y = 4 digit year, %m = 2 digit month, %d = 2 digit day, etc. +# see www.strftime.org for more information +# %Y%m%d%H expands to YYYYMMDDHH +INIT_TIME_FMT = %Y%m%d%H + +# Start time for METplus run - must match INIT_TIME_FMT +INIT_BEG=2005080700 + +# End time for METplus run - must match INIT_TIME_FMT +INIT_END=2005080700 + +# Increment between METplus runs (in seconds if no units are specified) +# Must be >= 60 seconds +INIT_INCREMENT = 12H + +# List of forecast leads to process for each run time (init or valid) +# In hours if units are not specified +# If unset, defaults to 0 (don't loop through forecast leads) +LEAD_SEQ = 12 + +# Order of loops to process data - Options are times, processes +# Not relevant if only one item is in the PROCESS_LIST +# times = run all wrappers in the PROCESS_LIST for a single run time, then +# increment the run time and run all wrappers again until all times have +# been evaluated. +# processes = run the first wrapper in the PROCESS_LIST for all times +# specified, then repeat for the next item in the PROCESS_LIST until all +# wrappers have been run +LOOP_ORDER = times + +# directory containing forecast input to GridStat +FCST_GRID_STAT_INPUT_DIR = {INPUT_BASE}/met_test/data/sample_fcst + +# Template to look for forecast input to GridStat relative to FCST_GRID_STAT_INPUT_DIR +FCST_GRID_STAT_INPUT_TEMPLATE = {init?fmt=%Y%m%d%H}/wrfprs_ruc13_{lead?fmt=%HH}.tm00_G212 + +# directory containing observation input to GridStat +OBS_GRID_STAT_INPUT_DIR = {INPUT_BASE}/met_test/new + +# Template to look for observation input to GridStat relative to OBS_GRID_STAT_INPUT_DIR +OBS_GRID_STAT_INPUT_TEMPLATE = ST2ml{valid?fmt=%Y%m%d%H}_A03h.nc + +# directory containing climatology mean input to GridStat +# Not used in this example +GRID_STAT_CLIMO_MEAN_INPUT_DIR = + +# Template to look for climatology input to GridStat relative to GRID_STAT_CLIMO_MEAN_INPUT_DIR +# Not used in this example +GRID_STAT_CLIMO_MEAN_INPUT_TEMPLATE = + +# directory containing climatology mean input to GridStat +# Not used in this example +GRID_STAT_CLIMO_STDEV_INPUT_DIR = + +# Template to look for climatology input to GridStat relative to GRID_STAT_CLIMO_STDEV_INPUT_DIR +# Not used in this example +GRID_STAT_CLIMO_STDEV_INPUT_TEMPLATE = + +# directory to write output from GridStat +GRID_STAT_OUTPUT_DIR = {OUTPUT_BASE}/met_tool_wrapper/GridStat/GridStat + +# Optional subdirectories relative to GRID_STAT_OUTPUT_DIR to write output from GridStat +GRID_STAT_OUTPUT_TEMPLATE = {init?fmt=%Y%m%d%H} + +# Used to specify one or more verification mask files for GridStat +# Not used for this example +GRID_STAT_VERIFICATION_MASK_TEMPLATE = + + +# Verbosity of MET output - overrides LOG_VERBOSITY for GridStat only +#LOG_GRID_STAT_VERBOSITY = 2 + +# Location of MET config file to pass to GridStat +GRID_STAT_CONFIG_FILE = {PARM_BASE}/met_config/GridStatConfig_wrapped + +# grid to remap data. Value is set as the 'to_grid' variable in the 'regrid' dictionary +# See MET User's Guide for more information +GRID_STAT_REGRID_TO_GRID = NONE + +#GRID_STAT_INTERP_FIELD = +#GRID_STAT_INTERP_VLD_THRESH = +#GRID_STAT_INTERP_SHAPE = +#GRID_STAT_INTERP_TYPE_METHOD = +#GRID_STAT_INTERP_TYPE_WIDTH = + +#GRID_STAT_NC_PAIRS_VAR_NAME = + +#GRID_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = + +#GRID_STAT_GRID_WEIGHT_FLAG = + +# Name to identify model (forecast) data in output +MODEL = WRF + +# Name to identify observation data in output +OBTYPE = MC_PCP + +# set the desc value in the GridStat MET config file +GRID_STAT_DESC = NA + +# List of variables to compare in GridStat - FCST_VAR1 variables correspond +# to OBS_VAR1 variables +# Note [FCST/OBS/BOTH]_GRID_STAT_VAR_NAME can be used instead if different evaluations +# are needed for different tools + +# Name of forecast variable 1 +FCST_VAR1_NAME = APCP + +# List of levels to evaluate for forecast variable 1 +# A03 = 3 hour accumulation in GRIB file +FCST_VAR1_LEVELS = A03 + +# List of thresholds to evaluate for each name/level combination for +# forecast variable 1 +FCST_VAR1_THRESH = gt12.7, gt25.4, gt50.8, gt76.2 + +#FCST_GRID_STAT_FILE_TYPE = + +# Name of observation variable 1 +OBS_VAR1_NAME = APCP_03 + +# List of levels to evaluate for observation variable 1 +# (*,*) is NetCDF notation - must include quotes around these values! +# must be the same length as FCST_VAR1_LEVELS +OBS_VAR1_LEVELS = "(*,*)" + +# List of thresholds to evaluate for each name/level combination for +# observation variable 1 +OBS_VAR1_THRESH = gt12.7, gt25.4, gt50.8, gt76.2 + +#OBS_GRID_STAT_FILE_TYPE = + +# Time relative to valid time (in seconds) to allow files to be considered +# valid. Set both BEGIN and END to 0 to require the exact time in the filename +# Not used in this example. +FCST_GRID_STAT_FILE_WINDOW_BEGIN = 0 +FCST_GRID_STAT_FILE_WINDOW_END = 0 +OBS_GRID_STAT_FILE_WINDOW_BEGIN = 0 +OBS_GRID_STAT_FILE_WINDOW_END = 0 + +# MET GridStat neighborhood values +# See the MET User's Guide GridStat section for more information + +# width value passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_WIDTH = 1 + +# shape value passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_SHAPE = SQUARE + +# cov thresh list passed to nbrhd dictionary in the MET config file +GRID_STAT_NEIGHBORHOOD_COV_THRESH = >=0.5 + +# Set to true to run GridStat separately for each field specified +# Set to false to create one run of GridStat per run time that +# includes all fields specified. +GRID_STAT_ONCE_PER_FIELD = False + +# Set to true if forecast data is probabilistic +FCST_IS_PROB = false + +# Only used if FCST_IS_PROB is true - sets probabilistic threshold +FCST_GRID_STAT_PROB_THRESH = ==0.1 + +# Set to true if observation data is probabilistic +# Only used if configuring forecast data as the 'OBS' input +OBS_IS_PROB = false + +# Only used if OBS_IS_PROB is true - sets probabilistic threshold +OBS_GRID_STAT_PROB_THRESH = ==0.1 + +GRID_STAT_OUTPUT_PREFIX = {MODEL}_{CURRENT_FCST_NAME}_vs_{OBTYPE}_{CURRENT_OBS_NAME} + +#GRID_STAT_CLIMO_MEAN_FILE_NAME = +#GRID_STAT_CLIMO_MEAN_FIELD = +#GRID_STAT_CLIMO_MEAN_REGRID_METHOD = +#GRID_STAT_CLIMO_MEAN_REGRID_WIDTH = +#GRID_STAT_CLIMO_MEAN_REGRID_VLD_THRESH = +#GRID_STAT_CLIMO_MEAN_REGRID_SHAPE = +#GRID_STAT_CLIMO_MEAN_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_MEAN_MATCH_MONTH = +#GRID_STAT_CLIMO_MEAN_DAY_INTERVAL = +#GRID_STAT_CLIMO_MEAN_HOUR_INTERVAL = + +#GRID_STAT_CLIMO_STDEV_FILE_NAME = +#GRID_STAT_CLIMO_STDEV_FIELD = +#GRID_STAT_CLIMO_STDEV_REGRID_METHOD = +#GRID_STAT_CLIMO_STDEV_REGRID_WIDTH = +#GRID_STAT_CLIMO_STDEV_REGRID_VLD_THRESH = +#GRID_STAT_CLIMO_STDEV_REGRID_SHAPE = +#GRID_STAT_CLIMO_STDEV_TIME_INTERP_METHOD = +#GRID_STAT_CLIMO_STDEV_MATCH_MONTH = +#GRID_STAT_CLIMO_STDEV_DAY_INTERVAL = +#GRID_STAT_CLIMO_STDEV_HOUR_INTERVAL = + + +#GRID_STAT_CLIMO_CDF_BINS = 1 +#GRID_STAT_CLIMO_CDF_CENTER_BINS = False +#GRID_STAT_CLIMO_CDF_WRITE_BINS = True + +#GRID_STAT_OUTPUT_FLAG_FHO = NONE +GRID_STAT_OUTPUT_FLAG_CTC = STAT +GRID_STAT_OUTPUT_FLAG_CTS = STAT +#GRID_STAT_OUTPUT_FLAG_MCTC = NONE +#GRID_STAT_OUTPUT_FLAG_MCTS = NONE +#GRID_STAT_OUTPUT_FLAG_CNT = NONE +#GRID_STAT_OUTPUT_FLAG_SL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_SAL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VAL1L2 = NONE +#GRID_STAT_OUTPUT_FLAG_VCNT = NONE +#GRID_STAT_OUTPUT_FLAG_PCT = NONE +#GRID_STAT_OUTPUT_FLAG_PSTD = NONE +#GRID_STAT_OUTPUT_FLAG_PJC = NONE +#GRID_STAT_OUTPUT_FLAG_PRC = NONE +GRID_STAT_OUTPUT_FLAG_ECLV = BOTH +#GRID_STAT_OUTPUT_FLAG_NBRCTC = NONE +#GRID_STAT_OUTPUT_FLAG_NBRCTS = NONE +#GRID_STAT_OUTPUT_FLAG_NBRCNT = NONE +GRID_STAT_OUTPUT_FLAG_GRAD = BOTH +#GRID_STAT_OUTPUT_FLAG_DMAP = NONE + +GRID_STAT_NC_PAIRS_FLAG_LATLON = FALSE +GRID_STAT_NC_PAIRS_FLAG_RAW = FALSE +GRID_STAT_NC_PAIRS_FLAG_DIFF = FALSE +GRID_STAT_NC_PAIRS_FLAG_CLIMO = FALSE +#GRID_STAT_NC_PAIRS_FLAG_CLIMO_CDP = FALSE +#GRID_STAT_NC_PAIRS_FLAG_WEIGHT = FALSE +#GRID_STAT_NC_PAIRS_FLAG_NBRHD = FALSE +#GRID_STAT_NC_PAIRS_FLAG_FOURIER = FALSE +#GRID_STAT_NC_PAIRS_FLAG_GRADIENT = FALSE +#GRID_STAT_NC_PAIRS_FLAG_DISTANCE_MAP = FALSE +GRID_STAT_NC_PAIRS_FLAG_APPLY_MASK = FALSE From 774a3e9c2fddf2651e4a7ee58361d3173742d41d Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 4 Feb 2022 12:32:26 -0700 Subject: [PATCH 280/770] Feature 675 Continuous Integration Documentation (#1409) Co-authored-by: johnhg --- .github/jobs/build_documentation.sh | 1 + .github/jobs/copy_output_to_artifact.sh | 9 + .github/jobs/create_dirs_for_database.sh | 16 + .github/jobs/create_output_data_volumes.sh | 2 +- .github/jobs/docker_setup.sh | 4 +- .github/jobs/run_difference_tests.sh | 23 + .github/jobs/save_error_logs.sh | 14 + .github/jobs/set_job_controls.sh | 35 +- .github/workflows/testing.yml | 105 +- docs/Contributors_Guide/add_use_case.rst | 286 +----- .../continuous_integration.rst | 919 +++++++++++++++++- .../figure/ci-doc-artifacts.png | Bin 0 -> 6294 bytes .../figure/ci-doc-error.png | Bin 0 -> 17765 bytes .../figure/gha-workflow-name.png | Bin 13030 -> 17475 bytes docs/Users_Guide/installation.rst | 8 +- internal_tests/use_cases/all_use_cases.txt | 4 +- {ci => scripts}/docker/Dockerfile | 0 {ci => scripts}/docker/docker_data/Dockerfile | 0 .../docker/docker_data/build_docker_images.sh | 0 .../docker/docker_data/hooks/build | 0 .../docker/docker_data_output/Dockerfile | 0 {ci => scripts}/docker/docker_env/Dockerfile | 0 .../docker/docker_env/Dockerfile.gempak_env | 0 .../docker/docker_env/Dockerfile.gfdl-tracker | 0 .../docker/docker_env/Dockerfile.metplus_base | 0 .../docker_env/Dockerfile.py_embed_base | 0 {ci => scripts}/docker/docker_env/README.md | 2 +- .../docker/docker_env/scripts/cfgrib_env.sh | 0 .../docker_env/scripts/cycloneplotter_env.sh | 0 .../docker/docker_env/scripts/diff_env.sh | 0 .../docker/docker_env/scripts/gempak_env.sh | 0 .../docker/docker_env/scripts/h5py_env.sh | 0 .../docker/docker_env/scripts/icecover_env.sh | 0 .../docker_env/scripts/metdatadb_env.sh | 0 .../docker_env/scripts/metplotpy_env.sh | 0 .../docker_env/scripts/metplus_base_env.sh | 0 .../docker/docker_env/scripts/netcdf4_env.sh | 0 .../docker_env/scripts/py_embed_base_env.sh | 0 .../docker/docker_env/scripts/pygrib_env.sh | 0 .../docker/docker_env/scripts/pytest_env.sh | 0 .../docker_env/scripts/spacetime_env.sh | 0 .../docker_env/scripts/weatherregime_env.sh | 0 .../docker/docker_env/scripts/xesmf_env.sh | 0 {ci => scripts}/docker/hooks/build | 0 {ci => scripts}/docker/hooks/get_met_version | 0 45 files changed, 1003 insertions(+), 425 deletions(-) create mode 100755 .github/jobs/copy_output_to_artifact.sh create mode 100755 .github/jobs/create_dirs_for_database.sh create mode 100755 .github/jobs/run_difference_tests.sh create mode 100755 .github/jobs/save_error_logs.sh create mode 100644 docs/Contributors_Guide/figure/ci-doc-artifacts.png create mode 100644 docs/Contributors_Guide/figure/ci-doc-error.png mode change 100755 => 100644 docs/Contributors_Guide/figure/gha-workflow-name.png rename {ci => scripts}/docker/Dockerfile (100%) rename {ci => scripts}/docker/docker_data/Dockerfile (100%) rename {ci => scripts}/docker/docker_data/build_docker_images.sh (100%) rename {ci => scripts}/docker/docker_data/hooks/build (100%) rename {ci => scripts}/docker/docker_data_output/Dockerfile (100%) rename {ci => scripts}/docker/docker_env/Dockerfile (100%) rename {ci => scripts}/docker/docker_env/Dockerfile.gempak_env (100%) rename {ci => scripts}/docker/docker_env/Dockerfile.gfdl-tracker (100%) rename {ci => scripts}/docker/docker_env/Dockerfile.metplus_base (100%) rename {ci => scripts}/docker/docker_env/Dockerfile.py_embed_base (100%) rename {ci => scripts}/docker/docker_env/README.md (99%) rename {ci => scripts}/docker/docker_env/scripts/cfgrib_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/cycloneplotter_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/diff_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/gempak_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/h5py_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/icecover_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/metdatadb_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/metplotpy_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/metplus_base_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/netcdf4_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/py_embed_base_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/pygrib_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/pytest_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/spacetime_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/weatherregime_env.sh (100%) rename {ci => scripts}/docker/docker_env/scripts/xesmf_env.sh (100%) rename {ci => scripts}/docker/hooks/build (100%) rename {ci => scripts}/docker/hooks/get_met_version (100%) diff --git a/.github/jobs/build_documentation.sh b/.github/jobs/build_documentation.sh index 3306facee0..5bd00ea174 100755 --- a/.github/jobs/build_documentation.sh +++ b/.github/jobs/build_documentation.sh @@ -25,6 +25,7 @@ if [ -s $warning_file ]; then echo Summary: grep WARNING ${DOCS_DIR}/_build/warnings.log grep ERROR ${DOCS_DIR}/_build/warnings.log + grep CRITICAL ${DOCS_DIR}/_build/warnings.log echo Review this log file or download documentation_warnings.log artifact exit 1 fi diff --git a/.github/jobs/copy_output_to_artifact.sh b/.github/jobs/copy_output_to_artifact.sh new file mode 100755 index 0000000000..ca5847bdb1 --- /dev/null +++ b/.github/jobs/copy_output_to_artifact.sh @@ -0,0 +1,9 @@ +#! /bin/bash + +# Called from .github/workflows/testing.yml +# Creates directory for output data artifact and +# copies output data into directory + +artifact_name=$1 +mkdir -p artifact/${artifact_name} +cp -r ${RUNNER_WORKSPACE}/output/* artifact/${artifact_name}/ diff --git a/.github/jobs/create_dirs_for_database.sh b/.github/jobs/create_dirs_for_database.sh new file mode 100755 index 0000000000..49f3e6f37e --- /dev/null +++ b/.github/jobs/create_dirs_for_database.sh @@ -0,0 +1,16 @@ +#! /bin/bash + +# Create directories used for use case that uses METviewer and METdatadb +# Open up write permissions for these directories so that files can +# be written by METviewer Docker container. +# Called by .github/workflows/testing.yml + +if [ -z "${RUNNER_WORKSPACE}" ]; then + echo "ERROR: RUNNER_WORKSPACE env var must be set" + exit 1 +fi + +mkdir -p $RUNNER_WORKSPACE/mysql +mkdir -p $RUNNER_WORKSPACE/output/metviewer +chmod a+w $RUNNER_WORKSPACE/mysql +chmod a+w $RUNNER_WORKSPACE/output/metviewer diff --git a/.github/jobs/create_output_data_volumes.sh b/.github/jobs/create_output_data_volumes.sh index 6cf11f57d2..38f55b7dea 100755 --- a/.github/jobs/create_output_data_volumes.sh +++ b/.github/jobs/create_output_data_volumes.sh @@ -28,7 +28,7 @@ if [ $? != 0 ]; then exit 1 fi -docker_data_output_dir=ci/docker/docker_data_output +docker_data_output_dir=scripts/docker/docker_data_output success=1 for vol_name in use_cases_*; do diff --git a/.github/jobs/docker_setup.sh b/.github/jobs/docker_setup.sh index 79d0514175..0e67e2b324 100755 --- a/.github/jobs/docker_setup.sh +++ b/.github/jobs/docker_setup.sh @@ -29,9 +29,9 @@ duration=$(( SECONDS - start_seconds )) echo "TIMING: docker pull ${DOCKERHUB_TAG} took `printf '%02d' $(($duration / 60))`:`printf '%02d' $(($duration % 60))` (MM:SS)" # set DOCKERFILE_PATH that is used by docker hook script get_met_version -export DOCKERFILE_PATH=${GITHUB_WORKSPACE}/ci/docker/Dockerfile +export DOCKERFILE_PATH=${GITHUB_WORKSPACE}/scripts/docker/Dockerfile -MET_TAG=`${GITHUB_WORKSPACE}/ci/docker/hooks/get_met_version` +MET_TAG=`${GITHUB_WORKSPACE}/scripts/docker/hooks/get_met_version` # if MET_FORCE_TAG variable is set and not empty, use that version instead if [ ! -z "$MET_FORCE_TAG" ]; then diff --git a/.github/jobs/run_difference_tests.sh b/.github/jobs/run_difference_tests.sh new file mode 100755 index 0000000000..d363b673d5 --- /dev/null +++ b/.github/jobs/run_difference_tests.sh @@ -0,0 +1,23 @@ +#! /bin/bash + +# Called by .github/workflows/testing.yml +# Runs Python script to perform difference testing on use case output +# to compare output to truth data. +# If any differences were reported, set GHA output var upload_diff +# to true, copy difference files to artifact directory, and return +# non-zero status. If no differences were found, set GHA output var +# upload_diff to false + +matrix_categories=$1 +artifact_name=$2 + +.github/jobs/setup_and_run_diff.py ${matrix_categories} $artifact_name + +if [ "$( ls -A ${RUNNER_WORKSPACE}/diff)" ]; then + echo ::set-output name=upload_diff::true + mkdir -p artifact/diff-${artifact_name} + cp -r ${RUNNER_WORKSPACE}/diff/* artifact/diff-${artifact_name} + exit 1 +fi + +echo ::set-output name=upload_diff::false diff --git a/.github/jobs/save_error_logs.sh b/.github/jobs/save_error_logs.sh new file mode 100755 index 0000000000..0662056729 --- /dev/null +++ b/.github/jobs/save_error_logs.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +# Called from .github/workflows/testing.yml +# Call Python script to copy logs from any use case that contains "ERROR:" +# into directory to create GitHub Actions artifact. +# Sets output variable upload_error_logs to 'true' if errors occurred or +# 'false' if no errors occurred + +.github/jobs/copy_error_logs.py ${RUNNER_WORKSPACE}/output artifact/error_logs +if [ -d "artifact/error_logs" ]; then + echo ::set-output name=upload_error_logs::true +else + echo ::set-output name=upload_error_logs::false +fi diff --git a/.github/jobs/set_job_controls.sh b/.github/jobs/set_job_controls.sh index b8e05e27da..93ed12e970 100755 --- a/.github/jobs/set_job_controls.sh +++ b/.github/jobs/set_job_controls.sh @@ -5,7 +5,6 @@ # a push to determine which jobs to run and which to skip. # set default status for jobs -run_docs=true run_get_image=true run_get_input_data=true run_unit_tests=true @@ -49,7 +48,6 @@ else # check commit messages for skip or force keywords if grep -q "ci-skip-all" <<< "$commit_msg"; then - run_docs=false run_get_image=false run_get_input_data=false run_unit_tests=false @@ -66,18 +64,9 @@ else run_unit_tests=false fi - if grep -q "ci-only-docs" <<< "$commit_msg"; then - run_docs=true - run_get_image=false - run_get_input_data=false - run_unit_tests=false - run_use_cases=false - run_save_truth_data=false - run_diff=false - fi - - if grep -q "ci-only-new-cases" <<< "$commit_msg"; then - run_all_use_cases=false + if grep -q "ci-run-all-cases" <<< "$commit_msg"; then + run_use_cases=true + run_all_use_cases=true fi if grep -q "ci-run-all-diff" <<< "$commit_msg"; then @@ -89,26 +78,8 @@ else run_diff=true fi - if grep -q "ci-run-all-cases" <<< "$commit_msg"; then - run_use_cases=true - run_all_use_cases=true - fi - fi -touch job_control_status -echo run_docs=${run_docs} >> job_control_status -echo run_get_image=${run_get_image} >> job_control_status -echo run_get_input_data=${run_get_input_data} >> job_control_status -echo run_unit_tests=${run_unit_tests} >> job_control_status -echo run_use_cases=${run_use_cases} >> job_control_status -echo run_save_truth_data=${run_save_truth_data} >> job_control_status -echo run_all_use_cases=${run_all_use_cases} >> job_control_status -echo run_diff=${run_diff} >> job_control_status -echo external_trigger=${external_trigger} >> job_control_status -echo Job Control Settings: -cat job_control_status - echo ::set-output name=run_get_image::$run_get_image echo ::set-output name=run_get_input_data::$run_get_input_data echo ::set-output name=run_diff::$run_diff diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 79c0b51e32..7b2080704e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,18 +1,22 @@ name: Testing + on: + push: branches: - develop - develop-ref - - feature_* - - main_* - - bugfix_* + - 'feature_*' + - 'main_*' + - 'bugfix_*' paths-ignore: - - docs/** + - 'docs/**' + pull_request: - types: [opened, reopened, synchronize] + types: [opened, synchronize, reopened] paths-ignore: - - docs/** + - 'docs/**' + workflow_dispatch: inputs: repository: @@ -23,14 +27,16 @@ on: required: true ref: description: 'Branch that triggered event' + required: true actor: description: 'User that triggered the event' pusher_email: description: 'Email address of user who triggered push event' jobs: + event_info: - name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repository }} ${{ github.event_name != 'workflow_dispatch' && 'local' || github.event.inputs.pusher_email }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.sha }}" + name: "Trigger: ${{ github.event_name != 'workflow_dispatch' && github.event_name || github.event.inputs.repository }} ${{ github.event_name != 'workflow_dispatch' && 'local' || github.event.inputs.actor }} ${{ github.event_name != 'workflow_dispatch' && 'event' || github.event.inputs.sha }}" runs-on: ubuntu-latest steps: - name: Print GitHub values for reference @@ -41,6 +47,15 @@ jobs: job_control: name: Determine which jobs to run runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set job controls + id: job_status + run: .github/jobs/set_job_controls.sh + env: + commit_msg: ${{ github.event.head_commit.message }} + outputs: matrix: ${{ steps.job_status.outputs.matrix }} run_some_tests: ${{ steps.job_status.outputs.run_some_tests }} @@ -49,17 +64,7 @@ jobs: run_diff: ${{ steps.job_status.outputs.run_diff }} run_save_truth_data: ${{ steps.job_status.outputs.run_save_truth_data }} external_trigger: ${{ steps.job_status.outputs.external_trigger }} - steps: - - uses: actions/checkout@v2 - - name: Set job controls - id: job_status - run: .github/jobs/set_job_controls.sh - env: - commit_msg: ${{ github.event.head_commit.message }} - - uses: actions/upload-artifact@v2 - with: - name: job_control_status - path: job_control_status + get_image: name: Docker Setup - Get METplus Image runs-on: ubuntu-latest @@ -76,6 +81,7 @@ jobs: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} #MET_FORCE_TAG: 10.0.0 + update_data_volumes: name: Docker Setup - Update Data Volumes runs-on: ubuntu-latest @@ -83,16 +89,20 @@ jobs: if: ${{ needs.job_control.outputs.run_get_input_data == 'true' }} steps: - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 with: python-version: '3.6' + - name: Install dependencies run: python -m pip install --upgrade pip python-dateutil requests bs4 + - name: Update Data Volumes run: .github/jobs/docker_update_data_volumes.py env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + use_case_tests: name: Use Case Tests runs-on: ubuntu-latest @@ -102,21 +112,24 @@ jobs: fail-fast: false matrix: ${{fromJson(needs.job_control.outputs.matrix)}} steps: + + - uses: actions/checkout@v2 + - name: Create directories for database - run: | - mkdir -p $RUNNER_WORKSPACE/mysql - mkdir -p $RUNNER_WORKSPACE/output/metviewer - chmod a+w $RUNNER_WORKSPACE/mysql - chmod a+w $RUNNER_WORKSPACE/output/metviewer + run: .github/jobs/create_dirs_for_database.sh + - name: Create directory for artifacts run: mkdir -p artifact - - uses: actions/checkout@v2 + - name: Get artifact name id: get-artifact-name run: | artifact_name=`.github/jobs/get_artifact_name.sh ${{ matrix.categories }}` echo ::set-output name=artifact_name::${artifact_name} - - uses: ./.github/actions/run_tests + + # run use case tests + - name: Run Use Cases + uses: ./.github/actions/run_tests id: run_tests with: categories: ${{ matrix.categories }} @@ -125,61 +138,43 @@ jobs: - name: Save error logs id: save-errors if: ${{ always() && steps.run_tests.conclusion == 'failure' && matrix.categories != 'pytests' }} - run: | - .github/jobs/copy_error_logs.py \ - ${RUNNER_WORKSPACE}/output \ - artifact/error_logs - if [ -d "artifact/error_logs" ]; then - echo ::set-output name=upload_error_logs::true - else - echo ::set-output name=upload_error_logs::false - fi + run: .github/jobs/save_error_logs.sh # run difference testing - name: Run difference tests id: run-diff if: ${{ needs.job_control.outputs.run_diff == 'true' && steps.run_tests.conclusion == 'success' && matrix.categories != 'pytests' }} - run: | - artifact_name=${{ steps.get-artifact-name.outputs.artifact_name }} - .github/jobs/setup_and_run_diff.py ${{ matrix.categories }} $artifact_name - if [ "$( ls -A ${RUNNER_WORKSPACE}/diff)" ]; then - echo ::set-output name=upload_diff::true - mkdir -p artifact/diff-${artifact_name} - cp -r ${RUNNER_WORKSPACE}/diff/* artifact/diff-${artifact_name} - exit 1 - else - echo ::set-output name=upload_diff::false - fi + run: .github/jobs/run_difference_tests.sh ${{ matrix.categories }} ${{ steps.get-artifact-name.outputs.artifact_name }} # copy output data to save as artifact - name: Save output data id: save-output if: ${{ always() && steps.run_tests.conclusion != 'skipped' && matrix.categories != 'pytests' }} - run: | - artifact_name=${{ steps.get-artifact-name.outputs.artifact_name }} - mkdir -p artifact/${artifact_name} - cp -r ${RUNNER_WORKSPACE}/output/* artifact/${artifact_name}/ + run: .github/jobs/copy_output_to_artifact.sh ${{ steps.get-artifact-name.outputs.artifact_name }} - - uses: actions/upload-artifact@v2 - name: Upload output data artifact + - name: Upload output data artifact + uses: actions/upload-artifact@v2 if: ${{ always() && steps.run_tests.conclusion != 'skipped' && matrix.categories != 'pytests' }} with: name: ${{ steps.get-artifact-name.outputs.artifact_name }} path: artifact/${{ steps.get-artifact-name.outputs.artifact_name }} - - uses: actions/upload-artifact@v2 - name: Upload error logs artifact + + - name: Upload error logs artifact + uses: actions/upload-artifact@v2 if: ${{ always() && steps.save-errors.outputs.upload_error_logs }} with: name: error_logs path: artifact/error_logs if-no-files-found: ignore - - uses: actions/upload-artifact@v2 - name: Upload difference data artifact + + - name: Upload difference data artifact + uses: actions/upload-artifact@v2 if: ${{ always() && steps.run-diff.outputs.upload_diff == 'true' }} with: name: diff-${{ steps.get-artifact-name.outputs.artifact_name }} path: artifact/diff-${{ steps.get-artifact-name.outputs.artifact_name }} if-no-files-found: ignore + create_output_data_volumes: name: Create Output Docker Data Volumes runs-on: ubuntu-latest diff --git a/docs/Contributors_Guide/add_use_case.rst b/docs/Contributors_Guide/add_use_case.rst index fe01f46ca0..9b881b61ea 100644 --- a/docs/Contributors_Guide/add_use_case.rst +++ b/docs/Contributors_Guide/add_use_case.rst @@ -665,220 +665,10 @@ will be used in the final pull request. Add use case to the test suite ------------------------------ -In the METplus repository, there is a text file that contains the list of -all use cases:: - - internal_tests/use_cases/all_use_cases.txt - +The **internal_tests/use_cases/all_use_cases.txt** file in the METplus +repository contains the list of all use cases. Add the new use case to this file so it will be available in -the tests. The file is organized by use case category. Each category starts -a line that following the format:: - - Category: - -where is the name of the use case category. If you are adding a -use case that will go into a new category, you will have to add a new category -definition line to this file and add your new use case under it. Each use case -in that category will be found on its own line after this line. -The use cases can be defined using 3 different formats:: - - :: - :::: - :::::: - -**** - -The index is the number associated with the use case so it can be referenced -easily. The first index number in a new category should be 0. -Each use case added should have an index that is one greater than the previous. - -**::** - -This format should only be used if the use case has only 1 configuration file -and no additional Python package dependencies besides the ones that are -required by the METplus wrappers. is the path of the conf file -used for the use case relative to METplus/parm/use_cases. The filename of the -config file without the .conf extension will be used as the name of the use -case. Example:: - - 6::model_applications/medium_range/PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf - -The above example will be named -'PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr' and will run using the -configuration file listed. - -**::::** - -This format is required if the use case contains multiple configuration files. -Instead of forcing the script to guess which conf file should be used as the -name of the use case, you must explicitly define it. The name of the use case -must be separated from the with '::' and each conf file path or -conf variable override must be separated by a comma. Example:: - - 44::GridStat_multiple_config:: met_tool_wrapper/GridStat/GridStat.conf,met_tool_wrapper/GridStat/GridStat_forecast.conf,met_tool_wrapper/GridStat/GridStat_observation.conf - -The above example is named 'GridStat_multiple_config' and uses 3 .conf files. -Use cases with only one configuration file can also use this format is desired. - -**::::::** - -This format is used if there are additional dependencies required to run -the use case such as a different Python environment. - is a list of keywords separated by commas. - -Example:: - - 0::CyclonePlotter::met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf,user_env_vars.MET_PYTHON_EXE=python3:: cycloneplotter_env - -See the next section for more information on valid values to supply as -dependencies. - -Dependencies -^^^^^^^^^^^^ - -Conda Environments -"""""""""""""""""" - -The keywords that end with "_env" are Python environments created in Docker -images using Conda that can be used to run use cases. These images are stored -on DockerHub in dtcenter/metplus-envs and are named with a tag that corresponds -to the keyword without the "_env" suffix. -The environments were created using Docker commands via scripts that are found -in ci/docker/docker_env. Existing keywords that set up Conda environments used -for use cases are: - -* metplotpy_env -* spacetime_env -* xesmf_env -* netcdf4_env -* pygrib_env -* metdatadb_env -* h5py_env -* gempak_env - -Example:: - - spacetime_env - -The above example uses the Conda environment -in dtcenter/metplus-envs:**spacetime** to run a user script. -Note that only one dependency that contains the "_env" suffix can be supplied -to a given use case. - -The **gempak_env** is handled a little differently. It is used if -GempakToCF.jar is needed for a use case to convert GEMPAK data to NetCDF -format so it can be read by the MET tools. Instead of creating a Python -environment to use for the use case, this Docker image installs Java and -obtains the GempakToCF.jar file. When creating the Docker container to run -the use cases, the necessary Java files are copied over into the container -that runs the use cases so that the JAR file can be run by METplus wrappers. - -Other Keywords -"""""""""""""" - -Besides specifying Python environments, -there are additional keywords that can be used to set up the environment -to run a use case: - -* **py_embed** - Used if a different Python environment is required to - run a Python Embedding script. If this keyword is included with a Python - environment, then the MET_PYTHON_EXE environment variable will be set to - specify the version of Python3 that is included in that environment - -Example:: - - pygrib_env,py_embed - -In this example, the dtcenter/metplus-envs:**pygrib** environment is used to -run the use case. Since **py_embed** is also included, then the following will -be added to the call to run_metplus.py so that the Python embedding script -will use the **pygrib** environment to run:: - - user_env_vars.MET_PYTHON_EXE=/usr/local/envs/pygrib/bin/python3 - -Please see the MET User's Guide for more information on how to use Python -Embedding. - -* **metviewer** - Used if METviewer should be made available to the use case. - This is typically added for a METdbLoad use case that needs to populate a - database with MET output. - -* **metplus** - Used if a user script needs to call utility functions from the - metplus Python package. This keyword simply adds the METplus source code - directory to the PYTHONPATH so that the metplus.util functions can be - imported. Note that this keyword is not needed unless a different Python - environment is specified with a "_env" keyword. The version of Python that - is used to run typical use cases has already installed the METplus Python - package in its environment, so the package can be imported easily. - - -Creating New Python Environments -"""""""""""""""""""""""""""""""" - -In METplus v4.0.0 and earlier, a list of Python packages were added to use -cases that required additional packages. These packages were either installed -with pip3 or using a script. This approach was very time consuming as some -packages take a very long time to install in Docker. The new approach involves -creating Docker images that use Conda to create a Python environment that can -run the use case. To see what is available in each of the existing Python -environments, refer to the comments in the scripts found in -**ci/docker/docker_env/scripts**. New environments must be added by a METplus -developer, so please contact MET Help if none of these environments contain the -package requirements needed to run a new use case. - -A README file can be found in the ci/docker/docker_env directory that -provides commands that can be run to recreate a Docker image if the -conda environment needs to be updated. Please note that Docker must -be installed on the workstation used to create new Docker images and -a DockerHub account with access to the dtcenter repositories must -be used to push Docker images to DockerHub. - -The README file also contains commands to create a conda environment -that is used for the tests locally. Any base conda environments, -such as metplus_base and py_embed_base, must be created locally first -before creating an environment that builds upon these environments. -Please note that some commands in the scripts are specific to -the Docker environment and may need to be rerun to successfully -build the environment locally. - -**Installing METplus Components** - -These scripts -do not install any METplus components, -such as metplotpy/metcalcpy/metplus, in the Python environment that -may be needed for a use case. This is done because the automated tests -will install and use the latest version (develop) of the packages to -ensure that any changes to those components do not break any existing -use cases. These packages will need to be installed by the user -and need to be updated manually. To install these packages, -activate the Conda environment, obtain the source code from GitHub, -and run "pip3 install ." in the top level directory of the repository. - -Example:: - - conda activate weatherregime - git clone git@github.com:dtcenter/METplotpy - cd METplotpy - git checkout develop - git pull - pip3 install . - -**Cartopy Shapefiles** - -The cartopy python package automatically attempts to download -shapefiles as needed. -The URL that is used in cartopy version 0.18.0 and earlier no longer -exists, so use cases that needs these files will fail if they are -not found locally. If a conda environment uses cartopy, these -shapefiles may need to be downloaded by the user running the use case -even if the conda environment was created by another user. -Cartopy provides a script that can be used to obtain these shapefiles -from the updated URL:: - - wget https://raw.githubusercontent.com/SciTools/cartopy/master/tools/cartopy_feature_download.py - python3 cartopy_feature_download.py cultural physical cultural-extra - - +the tests. See the :ref:`cg-ci-all-use-cases` section for details. .. _add_new_category_to_test_runs: @@ -887,11 +677,12 @@ Add new category to test runs The **.github/parm/use_case_groups.json** file in the METplus repository contains a list of the use case groups to run together. -In METplus version 4.0.0 and earlier, this list was -found in the .github/workflows/testing.yml file. Add a new entry to the list that includes the category of the new use case, the list of indices that correspond to the index number described in the :ref:`add_use_case_to_test_suite` section. + +See the :ref:`cg-ci-use-case-groups` section for details. + Set the "run" variable to true so that the new use case group will run in the automated test suite whenever a new change is pushed to GitHub. This allows users to test that the new use case runs successfully. @@ -910,6 +701,7 @@ Example:: This example adds a new use case group that contains the climate use case with index 2 and is marked to "run" for every push. + New use cases are added as a separate item to make reviewing the test results easier. A new use case will produce new output data that is not found in the "truth" data set which is compared the output of the use case runs to check @@ -919,66 +711,6 @@ It also makes it easier to check the size of the output data and length of time the use case takes to run to determine if it can be added to an existing group or if it should remain in its own group. - -.. _subset_category: - -Subset Category into Multiple Tests -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use cases can be separated into multiple test jobs. -In the "index_list" value, define the cases to run for the job. -Use cases are numbered starting with 0 and are in order of how they are -found in the all_use_cases.txt file. - -The argument supports a comma-separated list of numbers. Example:: - - { - "category": "data_assimilation", - "index_list": "0,2,4", - "run": false - }, - { - "category": "data_assimilation", - "index_list": "1,3", - "run": false - }, - -The above example will run a job with data_assimilation use cases 0, 2, and -4, then another job with data_assimilation use cases 1 and 3. - -It also supports a range of numbers separated with a dash. Example:: - - { - "category": "data_assimilation", - "index_list": "0-3", - "run": false - }, - { - "category": "data_assimilation", - "index_list": "4-5", - "run": false - }, - -The above example will run a job with data_assimilation 0, 1, 2, and 3, then -another job with data_assimilation 4 and 5. - -You can also use a combination of commas and dashes to define the list of cases -to run. Example:: - - { - "category": "data_assimilation", - "index_list": "0-2,4", - "run": false - }, - { - "category": "data_assimilation", - "index_list": "3", - "run": false - }, - -The above example will run data_assimilation 0, 1, 2, and 4 in one -job, then data_assimilation 3 in another job. - Monitoring Automated Tests -------------------------- @@ -1077,8 +809,8 @@ so that it runs in a reasonable time frame. If the new use case runs in a reasonable amount of time but the total time to run the set of use cases is now above 20 minutes or so, consider creating a -new job for the new use case. See the :ref:`subset_category` section and the -multiple medium_range jobs for an example. +new job for the new use case. See the :ref:`cg-ci-subset_category` section +and the multiple medium_range jobs for an example. .. _exceeded-Github-Actions: diff --git a/docs/Contributors_Guide/continuous_integration.rst b/docs/Contributors_Guide/continuous_integration.rst index f10f62abeb..16a43b1934 100644 --- a/docs/Contributors_Guide/continuous_integration.rst +++ b/docs/Contributors_Guide/continuous_integration.rst @@ -1,12 +1,11 @@ +********************** Continuous Integration -====================== - -More information on Continuous Integration (CI) coming soon! +********************** METplus utilizes GitHub Actions to run processes automatically when changes are pushed to GitHub. These tasks include: -* Building documentation +* Building documentation to catch warnings/errors * Building a Docker image to run tests * Creating/Updating Docker data volumes with new input data used for tests * Running unit tests @@ -14,102 +13,378 @@ are pushed to GitHub. These tasks include: * Comparing use case output to truth data * Creating/Updating Docker data volumes with truth data to use in comparisons -Workflow Control ----------------- - -GitHub Actions is controlled by a file in the .github/workflow directory called -testing.yml. If this file exists and is valid (no errors), GitHub Actions will -read this file and trigger a workflow run if the triggering criteria is met. -It can run multiple jobs in parallel or serially depending on dependency rules -that can be set. Each job can run a series of commands or scripts called steps. -Job steps can include "actions" with can be used to perform tasks. Many useful -actions are provided by GitHub and external collaborators. Developers can also -write their own custom actions to perform complex tasks to simplify a workflow. +GitHub Actions Workflows +======================== + +GitHub Actions runs workflows defined by files in the **.github/workflows** +directory of a GitHub repository. +Files with the .yml suffix are parsed and GitHub Actions will +trigger a workflow run if the triggering criteria is met. +Multiple workflows may be triggered by a single event. +All workflow runs can be seen on the Actions tab of the repository. +Each workflow run is identified by the branch for which it was invoked +as well as the corresponding commit message on that branch. +In general, a green check mark indicates that all checks for +that workflow run passed. +A red X indicates that at least one of the jobs failed. + +Workflows can run multiple jobs in parallel or serially depending on +dependency rules that can be set. +Each job can run a series of commands or scripts called steps. +Steps can include actions which can be used to perform common tasks. +Many useful actions are provided by GitHub and external collaborators. +Developers can also write their own custom actions to perform complex tasks +to simplify a workflow. + +**TODO Add screenshots** + +Testing (testing.yml) +--------------------- + +This workflow performs a variety of tasks to ensure that changes do not break +any existing functionality. +See the :ref:`cg-ci-testing-workflow` for more information. + +Documentation (documentation.yml) +--------------------------------- + +METplus documentation is written using Sphinx. +The METplus components utilize ReadTheDocs to build and display documentation. +However, ReadTheDocs will render the documentation when warnings occur. +This GitHub Actions workflow is run to catch/report warnings and errors. + +This workflow is only triggered when changes are made to files under the +**docs** directory of the METplus repository. +It builds the documentation by running "make clean html" and +makes the files available to download at the end of the workflow +as a GitHub Actions artifact. This step is no longer mandatory because +ReadTheDocs is configured to automatically generate the documentation for each +branch/tag and publish it `online `_. + +The Makefile that runs sphinx-build was modified to write warnings and errors +to a file called warnings.log using the -w argument. This file will be empty +if no errors or warnings have occurred in the building of the documentation. +If it is not empty, the script called by this workflow will exit with a +non-zero value so that the workflow reports a failure. + +.. figure:: figure/ci-doc-error.png + +A summary of the lines that contain WARNING or ERROR are output in the +GitHub Actions log for easy access. +The warnings.log file is also made available as a GitHub Actions +artifact so it can be downloaded and reviewed. Artifacts can be found +at the bottom of the workflow summary page when the workflow has completed. + +.. figure:: figure/ci-doc-artifacts.png + + +Release Published (release_published.yml) - DEPRECATED +------------------------------------------------------ + +**This workflow is no longer be required, as Slack now has GitHub integration +to automatically create posts on certain events.** The workflow YAML file +is still found in the repository for reference, but the workflow has been +disabled via the Actions tab of the METplus GitHub webpage. + +This workflow is triggered when a release is published on GitHub. +It uses cURL to trigger a Slack message on the DTC-METplus announcements +channel that lists information about the release. A Slack bot was created +through the Slack API and the webhook that generated for the Slack channel +was saved as a GitHub Secret. + +.. _cg-ci-testing-workflow: + +Testing Workflow +================ Name -^^^^ +---- The name of a workflow can be specified to describe an overview of what is run. -Currently METplus only has 1 workflow, but others can be added. The following -line in the testing.yml file:: +The following line in the testing.yml file:: - name: METplus CI/CD Workflow + name: Testing -defines the workflow that runs all of the jobs. +defines the workflow identifier that can be seen from the Actions tab on the +METplus GitHub page. .. figure:: figure/gha-workflow-name.png Event Control -^^^^^^^^^^^^^ +------------- + +The **on** keyword defines which events trigger the workflow +to run. There are currently 3 types of events that trigger this workflow: +**push**, **pull_request**, and **workflow_dispatch**. +The jobs that are run in this workflow depend on which event has triggered it. +Many jobs are common to multiple events. +To avoid creating multiple workflow .yml files that contain redundant jobs, +an additional layer of control is added within this workflow. +See :ref:`cg-ci-job-control` for more information. + +Push +^^^^ -The "on" keyword is used to determine which events will trigger the workflow -to run:: +:: on: + push: branches: - develop - develop-ref - - feature_* - - main_* - - bugfix_* + - 'feature_*' + - 'main_*' + - 'bugfix_*' + paths-ignore: + - 'docs/**' + +This configuration tells GitHub Actions to trigger the workflow when changes +are pushed to the repository and the following criteria are met: + +* The branch is named **develop** or **develop-ref** +* The branch starts with **feature\_**, **main\_**, or **bugfix\_** +* Changes were made to at least one file that is not in the **docs** directory. + +Pull Request +^^^^^^^^^^^^ + +:: + pull_request: types: [opened, reopened, synchronize] + paths-ignore: + - 'docs/**' + +This configuration tells GitHub Actions to trigger the workflow for +pull requests in the repository and the following criteria are met: -This configuration tells GitHub Actions to trigger the workflow when: +* The pull request was opened, reopened, or synchronized. +* Changes were made to at least one file that is not in the **docs** directory. -* A push event occurs on the develop or develop-ref branch -* A push event occurs on a branch that starts with - feature\_, main\_, or bugfix\_ -* A pull request is opened, reopened, or synchronized (when new changes are - pushed to the source branch of the pull request. +The **synchronize** type triggers a workflow for every push to a branch +that is included in an open pull request. +If changes were requested in the pull request review, +a new workflow will be triggered for each push. +To prevent many workflows from being triggered, +developers are encouraged to limit the number of pushes for open pull requests. +Note that pull requests can be closed until the necessary changes are +completed, or :ref:`cg-ci-commit-message-keywords` can be used +to suppress the testing workflow. + + +Workflow Dispatch +^^^^^^^^^^^^^^^^^ + +:: + + workflow_dispatch: + inputs: + repository: + description: 'Repository that triggered workflow' + required: true + sha: + description: 'Commit hash that triggered the event' + required: true + ref: + description: 'Branch that triggered event' + required: true + actor: + description: 'User that triggered the event' + + +This configuration enables manual triggering of this workflow. +It allows other GitHub repositories such as MET, METplotpy, and METcalcpy +to trigger this workflow. +It lists the input values that are passed from the external repository. +The inputs include: + +* The repository that triggered the workflow, such as dtcenter/MET +* The commit hash in the external repository that triggered the event +* The reference (or branch) that triggered the event, such as + refs/heads/develop +* The GitHub username that triggered the event in the external repository + (optional) + +The MET, METcalcpy, and METplotpy repositories are configured to +trigger this workflow since they are used in 1 or more METplus use cases. +Currently all 3 repositories only trigger when changes are pushed to their +develop branch. + +Future work is planned to support main_v* branches, which +will involve using the 'ref' input to determine what to obtain in the workflow. +For example, changes pushed to dtcenter/MET main_v10.1 should trigger a +testing workflow that runs on the METplus main_v4.1 branch. Jobs -^^^^ +---- + +The **jobs** keyword is used to define the jobs that are run in the workflow. +Each item under **jobs** is a string that defines the ID of the job. +This value can be referenced within the workflow as needed. +Each job in the testing workflow is described in its own section. + +* :ref:`cg-ci-event-info` +* :ref:`cg-ci-job-control` +* :ref:`cg-ci-get-image` +* :ref:`cg-ci-update-data-volumes` +* :ref:`cg-ci-use-case-tests` +* :ref:`cg-ci-create-output-data-volumes` + +.. _cg-ci-event-info: + +Event Info +---------- + +This job contains information on what triggered the workflow. +The name of the job contains complex logic to cleanly display information +about an event triggered by an external repository when that occurs. +Otherwise, it simply lists the type of local event (push or pull_request) +that triggered the workflow. + +**Insert images of examples of the Trigger job name for local and external** + +It also logs all of the information contained in the 'github' object that +includes all of the available information from the event that triggered +the workflow. This is useful to see what information is available to use +in the workflow based on the event. + +**Insert image of screenshot of the github.event info** -The "jobs" keyword is used to define the jobs that are run in the workflow. -Each item under "jobs" is a string that defines the ID of the job. This value -can be referenced within the workflow as needed. +.. _cg-ci-job-control: Job Control ----------- +:: + + job_control: + name: Determine which jobs to run + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set job controls + id: job_status + run: .github/jobs/set_job_controls.sh + env: + commit_msg: ${{ github.event.head_commit.message }} + + outputs: + matrix: ${{ steps.job_status.outputs.matrix }} + run_some_tests: ${{ steps.job_status.outputs.run_some_tests }} + run_get_image: ${{ steps.job_status.outputs.run_get_image }} + run_get_input_data: ${{ steps.job_status.outputs.run_get_input_data }} + run_diff: ${{ steps.job_status.outputs.run_diff }} + run_save_truth_data: ${{ steps.job_status.outputs.run_save_truth_data }} + external_trigger: ${{ steps.job_status.outputs.external_trigger }} + +This job runs a script called **set_job_controls.sh** +that parses environment variables set by GitHub Actions to determine which +jobs to run. There is :ref:`cg-ci-default-behavior` based on the event that +triggered the workflow and the branch name. +The last commit message before a push event is also parsed to look for +:ref:`cg-ci-commit-message-keywords` that can override the default behavior. + +The script also calls another script called **get_use_cases_to_run.sh** that +reads a JSON file that contains the use case test groups. +The job control settings determine which of the use case groups to run. +See :ref:`cg-ci-use-case-groups` for more information. + +Output Variables +^^^^^^^^^^^^^^^^ + +The step that calls the job control script is given an identifier using the +**id** keyword:: + + id: job_status + run: .github/jobs/set_job_controls.sh + +Values from the script are set as output variables using the following syntax:: + + echo ::set-output name=run_get_image::$run_get_image + +In this example, an output variable named *run_get_image* +(set with **name=run_get_image**) is created with the value of a +variable from the script with the same name (set after the :: characters). +The variable can be referenced elsewhere within the job using the following +syntax:: + + ${{ steps.job_status.outputs.run_get_image }} + +The ID of the step is needed to reference the outputs for that step. +**Note that this notation should be referenced directly in the workflow YAML +file and not inside a script that is called by the workflow.** + +To make the variable available to other jobs in the workflow, it will need +to be set in the **outputs** section of the job:: + + outputs: + run_get_image: ${{ steps.job_status.outputs.run_get_image }} + +The variable **run_get_image** can be referenced by other jobs that include +**job_status** as a job that must complete before starting using the **needs** +keyword:: + + get_image: + name: Docker Setup - Get METplus Image + runs-on: ubuntu-latest + needs: job_control + if: ${{ needs.job_control.outputs.run_get_image == 'true' }} + +Setting **needs: job_control** tells the **get_image** job to wait until the +**job_control** job has completed before running. Since this is the case, this +job can reference output from that job in the **if** value to determine if the +job should be run or not. + +.. _cg-ci-default-behavior: + Default Behavior ^^^^^^^^^^^^^^^^ On Push """"""" -When a push to a feature\_\*, bugfix\_\*, main_v\*, or develop\* branch occurs -the default behavior is to run the following: +When a push event occurs the default behavior is to run the following: -* Build documentation -* Update Docker image +* Create/Update the METplus Docker image * Look for new input data * Run unit tests -* Run any **new** use cases +* Run any use cases marked to run (see :ref:`cg-ci-use-case-tests`) + +If the push is on the **develop** or a **main_vX.Y** branch, then all +of the use cases are run. + +Default behavior for push events can be overridden using +:ref:`cg-ci-commit-message-keywords`. On Pull Request """"""""""""""" -When a pull request is created into the develop branch or a main_v\* branch, -additional jobs are run in automation. In addition to the jobs run for a push, -the scripts will: +When a pull request is created into the **develop** branch or +a **main_vX.Y** branch, additional jobs are run in automation. +In addition to the jobs run for a push, the scripts will: * Run all use cases * Compare use case output to truth data +.. _cg-ci-push-reference-branch: + On Push to Reference Branch """"""""""""""""""""""""""" -Branches with a name that ends with "-ref" contain the state of the repository -that will generate output that is considered "truth" data. -In addition to the jobs run for a normal push, the scripts will: +Branches with a name that ends with **-ref** contain the state of the +repository that will generate output that is considered "truth" data. +In addition to the jobs run for a push, the scripts will: * Run all use cases * Create/Update Docker data volumes that store truth data with the use case output +See :ref:`cg-ci-create-output-data-volumes` for more information. + +.. _cg-ci-commit-message-keywords: + Commit Message Keywords ^^^^^^^^^^^^^^^^^^^^^^^ @@ -119,12 +394,33 @@ Here is a list of the currently supported keywords and what they control: * **ci-skip-all**: Don't run anything - skip all automation jobs * **ci-skip-use-cases**: Don't run any use cases +* **ci-skip-unit-tests**: Don't run the Pytest unit tests * **ci-run-all-cases**: Run all use cases * **ci-run-diff**: Obtain truth data and run diffing logic for use cases that are marked to run * **ci-run-all-diff**: Obtain truth data and run diffing logic for all use cases -* **ci-only-docs**: Only run build documentation job - skip the rest + +.. _cg-ci-get-image: + +Create/Update METplus Docker Image +---------------------------------- + +This job calls the **docker_setup.sh** script. +This script builds a METplus Docker image and pushes it to DockerHub. +The image is pulled instead of built in each test job to save execution time. +The script attempts to pull the appropriate Docker image from DockerHub +(dtcenter/metplus-dev:*BRANCH_NAME*) if it already exists so that unchanged +components of the Docker image do not need to be rebuilt. +This reduces the time it takes to rebuild the image for a given branch on +a subsequent workflow run. + +DockerHub Credentials +^^^^^^^^^^^^^^^^^^^^^ + +The credentials needed to push images to DockerHub are stored in Secret +Environment Variables for the repository. These variables are passed +into the script that needs them using the **env** keyword. Force MET Version Used for Tests ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -137,10 +433,10 @@ i.e. METplotpy or METviewer, until a corresponding change is made to that component. If this occurs then some of the METplus use cases may break. To allow the tests to run successfully in the meantime, an option was added to force the version of the MET tag that is used to build the METplus Docker image -that is used for testing. In the testing.yml GitHub Actions workflow file -(found in .github/workflows), there is a commented variable called +that is used for testing. In the testing.yml workflow file, +there is a commented variable called MET_FORCE_TAG that can be uncommented and set to force the version of MET to -use. This variable is found in the "get_image" job under the "env" section +use. This variable is found in the **get_image** job under the **env** section for the step named "Get METplus Image." :: @@ -151,3 +447,524 @@ for the step named "Get METplus Image." DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} #MET_FORCE_TAG: 10.0.0 + + +.. _cg-ci-update-data-volumes: + +Create/Update Docker Data Volumes +--------------------------------- + +The METplus use case tests obtain input data from Docker data volumes. +Each use case category that corresponds to a directory in +**parm/use_cases/model_applications** has its own data volume that contains +all of the data needed to run those use cases. The MET Tool Wrapper use cases +found under **parm/use_cases/met_tool_wrapper** also have a data volume. +These data are made available on the DTC web server. + +The logic in this +job checks if the tarfile that contains the data for a use case category has +changed since the corresponding Docker data volume has been last updated. +If it has, then the Docker data volume is regenerated with the new data. + +When new data is needed for a new METplus use case, a directory that is named +after a feature branch is populated with the existing data for the use case +category and the new data is added there. This data is used for testing the +new use case in the automated tests. When the pull request for the new use +case is approved, the new data is moved into the version of the +data that corresponds to the upcoming release (i.e. v4.1) +so that it will be available for future tests. More details on this +process can be found in the :ref:`use_case_input_data` section of the +Add Use Cases chapter of the Contributor's Guide. + + +.. _cg-ci-use-case-tests: + +Use Case Tests +-------------- + +.. _cg-ci-all-use-cases: + +All Use Cases +^^^^^^^^^^^^^ + +All of the existing use cases are listed in **all_use_cases.txt**, +found in internal_tests/use_cases. + +The file is organized by use case category. Each category starts +a line that following the format:: + + Category: + +where ** is the name of the use case category. +See :ref:`use_case_categories` for more information. If you are adding a +use case that will go into a new category, you will have to add a new category +definition line to this file and add your new use case under it. Each use case +in that category will be found on its own line after this line. +The use cases can be defined using the following formats:: + + :::: + :::::: + +index +""""" + +The index is the number associated with the use case so it can be referenced +easily. The first index number in a new category should be 0. +Each use case added should have an index that is one greater than the previous. +If it has been determined that a use case cannot run in the automated tests, +then the index number should be replaced with "#X" so that is it included +in the list for reference but not run by the tests. + +name +"""" + +This is the string identifier of the use case. The name typically matches +the use case configuration filename without the **.conf** extension. + +Example:: + + PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr + + +config_args +""""""""""" + +This is the path of the config file used for the use case relative to +**parm/use_cases**. + +Example:: + + model_applications/medium_range/PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf + +If the use case contains multiple configuration files, +they can be listed separated by commas. + +Example:: + + met_tool_wrapper/GridStat/GridStat.conf,met_tool_wrapper/GridStat/GridStat_forecast.conf,met_tool_wrapper/GridStat/GridStat_observation.conf + + +dependencies +"""""""""""" + +If there are additional dependencies required to run the use case, +such as a different Python environment, a list of keywords separated by commas +can be provided. +The :ref:`cg-ci-use-case-dependencies` section contains information +on the keywords that can be used. + +Example:: + + cycloneplotter_env + + +.. _cg-ci-use-case-dependencies: + +Use Case Dependencies +^^^^^^^^^^^^^^^^^^^^^ + +Conda Environments +"""""""""""""""""" + +The keywords that end with **_env** are Python environments created in Docker +images using Conda that can be used to run use cases. These images are stored +on DockerHub in *dtcenter/metplus-envs* and are named with a tag that +corresponds to the keyword without the **_env** suffix. +The environments were created using Docker commands via scripts that are found +in **scripts/docker/docker_env**. +Existing keywords that set up Conda environments used for use cases are: + +* cfgrib_env +* h5py_env +* icecover_env +* metdatadb_env +* metplotpy_env +* netcdf4_env +* pygrib_env +* spacetime_env +* weatherregime_env +* xesmf_env + +Example:: + + spacetime_env + +The above example uses the Conda environment +in dtcenter/metplus-envs:**spacetime** to run a user script. +Note that only one dependency that contains the **_env** suffix can be supplied +to a given use case. + +Other Environments +"""""""""""""""""" + +A few of the environments do not contain Conda environments and +are handled a little differently. + +* **gempak_env** - Used if GempakToCF.jar is needed for a use case to convert + GEMPAK data to NetCDF format so it can be read by the MET tools. + Instead of creating a Python environment to use for the use case, + this Docker image installs Java and obtains the GempakToCF.jar file. + When creating the Docker container to run the use cases, + the necessary Java files are copied over into the container + that runs the use cases so that the JAR file can be run by METplus wrappers. +* **gfdl-tracker_env** - Contains the GFDL Tracker application that is used by + the GFDLTracker wrapper use cases. + + +Other Keywords +"""""""""""""" + +Besides specifying Python environments, +there are additional keywords that can be used to set up the environment +to run a use case: + +* **py_embed** - Used if a different Python environment is required to + run a Python Embedding script. If this keyword is included with a Python + environment, then the MET_PYTHON_EXE environment variable will be set to + specify the version of Python3 that is included in that environment + +Example:: + + pygrib_env,py_embed + +In this example, the dtcenter/metplus-envs:**pygrib** environment is used to +run the use case. Since **py_embed** is also included, then the following will +be added to the call to run_metplus.py so that the Python embedding script +will use the **pygrib** environment to run:: + + user_env_vars.MET_PYTHON_EXE=/usr/local/envs/pygrib/bin/python3 + +Please see the +`MET User's Guide `_ +for more information on how to use Python Embedding. + +* **metviewer** - Used if METviewer should be made available to the use case. + This is typically added for a METdbLoad use case that needs to populate a + database with MET output. + +* **metplus** - Used if a user script needs to call utility functions from the + metplus Python package. This keyword simply adds the METplus source code + directory to the PYTHONPATH so that the metplus.util functions can be + imported. Note that this keyword is not needed unless a different Python + environment is specified with a "_env" keyword. The version of Python that + is used to run typical use cases has already installed the METplus Python + package in its environment, so the package can be imported easily. + +* **metdatadb** - Used if the METdatadb repository is needed to run. Note that + this is only needed if using a Conda environment other than metdatadb_env. + The repository Python code will be installed in the Python environment. + +* **cartopy** - Used if cartopy 0.18.0 is needed in the Conda environment. + Cartopy uses shapefiles that are downloaded as needed. The URL that is used + to download the files has changed since cartopy 0.18.0 and we have run into + issues where the files cannot be obtained. To remedy this issue, we modified + the scripts that generate the Docker images that contain the Conda + environments that use cartopy to download these shape files so they will + always be available. These files need to be copied from the Docker + environment image into the testing image. When this keyword is found in the + dependency list, a different Dockerfile (Dockerfile.run_cartopy found in + .github/actions/run_tests) is used to create the testing environment and + copy the required shapefiles into place. + + +Creating New Python Environments +"""""""""""""""""""""""""""""""" + +In METplus v4.0.0 and earlier, a list of Python packages were added to use +cases that required additional packages. These packages were either installed +with pip3 or using a script. This approach was very time consuming as some +packages take a very long time to install in Docker. The new approach involves +creating Docker images that use Conda to create a Python environment that can +run the use case. To see what is available in each of the existing Python +environments, refer to the comments in the scripts found in +**scripts/docker/docker_env/scripts**. +New environments must be added by a METplus developer, +so please create a discussion on the +`METplus GitHub Discussions `_ +forum if none of these environments contain the package requirements +needed to run a new use case. + +A **README.md** file can be found in **scripts/docker/docker_env** that +provides commands that can be run to recreate a Docker image if the +conda environment needs to be updated. Please note that Docker must +be installed on the workstation used to create new Docker images and +a DockerHub account with access to the dtcenter repositories must +be used to push Docker images to DockerHub. + +The **README.md** file also contains commands to create a conda environment +that is used for the tests locally. Any base conda environments, +such as metplus_base and py_embed_base, must be created locally first +before creating an environment that builds upon these environments. +Please note that some commands in the scripts are specific to +the Docker environment and may need to be rerun to successfully +build the environment locally. + +**Installing METplus Components** + +The scripts used to create the Python environment Docker images +do not install any METplus components, +such as METplotpy, METcalcpy, METdatadb, and METplus, +in the Python environment that may be needed for a use case. +This is done because the automated tests +will install and use the latest version (develop) of the packages to +ensure that any changes to those components do not break any existing +use cases. These packages will need to be installed by the user +and need to be updated manually. To install these packages, +activate the Conda environment, obtain the source code from GitHub, +and run "pip3 install ." in the top level directory of the repository. + +Example:: + + conda activate weatherregime + git clone git@github.com:dtcenter/METplotpy + cd METplotpy + git checkout develop + git pull + pip3 install . + +**Cartopy Shapefiles** + +The cartopy python package automatically attempts to download +shapefiles as needed. +The URL that is used in cartopy version 0.18.0 and earlier no longer +exists, so use cases that needs these files will fail if they are +not found locally. If a conda environment uses cartopy, these +shapefiles may need to be downloaded by the user running the use case +even if the conda environment was created by another user. +Cartopy provides a script that can be used to obtain these shapefiles +from the updated URL:: + + wget https://raw.githubusercontent.com/SciTools/cartopy/master/tools/cartopy_feature_download.py + python3 cartopy_feature_download.py cultural physical cultural-extra + + +.. _cg-ci-use-case-groups: + +Use Case Groups +^^^^^^^^^^^^^^^ + +The use cases that are run in the automated test suite are divided into +groups that can be run concurrently. + +The **use_case_groups.json** file (found in **.github/parm**) +contains a list of the use case groups to run together. +In METplus version 4.0.0 and earlier, this list was +found in the .github/workflows/testing.yml file. + +Each use case group is defined with the following format:: + + { + "category": "", + "index_list": "", + "run": + } + +* **** is the category group that the use case is found under in the + **all_use_cases.txt** file (see :ref:`cg-ci-all-use-cases`). +* **** is a list of indices of the use cases from + **all_use_cases.txt** to run in the group. + This can be a single integer, a comma-separated list of + integers, and a range of values with a dash, i.e. 0-3. +* **** is a boolean (true/false) value that determines if the use + case group should be run. If the workflow job controls are not set to run + all of the use cases, then only use case groups that are set to true are + run. + +Example:: + + { + "category": "climate", + "index_list": "2", + "run": true + } + +This example defines a use case group that contains the climate use case +with index 2 and is marked to run for every push. + + +.. _cg-ci-subset_category: + +Subset Category into Multiple Tests +""""""""""""""""""""""""""""""""""" + +Use cases can be separated into multiple test jobs. +In the *index_list* value, define the cases to run for the job. +Use cases are numbered starting with 0 and correspond to the number set in +the **all_use_cases.txt** file. + +The argument supports a comma-separated list of numbers. Example:: + + { + "category": "data_assimilation", + "index_list": "0,2,4", + "run": false + }, + { + "category": "data_assimilation", + "index_list": "1,3", + "run": false + }, + +The above example will run a job with data_assimilation use cases 0, 2, and +4, then another job with data_assimilation use cases 1 and 3. + +It also supports a range of numbers separated with a dash. Example:: + + { + "category": "data_assimilation", + "index_list": "0-3", + "run": false + }, + { + "category": "data_assimilation", + "index_list": "4-5", + "run": false + }, + +The above example will run a job with data_assimilation 0, 1, 2, and 3, then +another job with data_assimilation 4 and 5. + +You can also use a combination of commas and dashes to define the list of cases +to run. Example:: + + { + "category": "data_assimilation", + "index_list": "0-2,4", + "run": false + }, + { + "category": "data_assimilation", + "index_list": "3", + "run": false + }, + +The above example will run data_assimilation 0, 1, 2, and 4 in one +job, then data_assimilation 3 in another job. + +Run Use Cases +^^^^^^^^^^^^^ + +The **use_case_tests** job is duplicated for each use case group using the +strategy -> matrix syntax:: + + strategy: + fail-fast: false + matrix: ${{fromJson(needs.job_control.outputs.matrix)}} + +**fail-fast** is set to false so that the rest of the use case test jobs will +run even when one of them fails. The **matrix** value is a list of use +case categories and indices that is created in the :ref:`cg-ci-job-control` +job. Each value in the list is referenced in the job steps with +**${{ matrix.categories }}**:: + + - name: Run Use Cases + uses: ./.github/actions/run_tests + id: run_tests + with: + categories: ${{ matrix.categories }} + +The logic that runs the use cases is contained in a custom GitHub Action +that is found in the METplus repository. + +Obtaining Input Data +"""""""""""""""""""" + +Each use case category has a corresponding Docker data volume that contains +the input data needed to run all of the use cases. The data volume is obtained +from DockerHub and mounted into the container that will run the use cases +using the **\-\-volumes-from** argument to the **docker run** command. + +Build Docker Test Environment +""""""""""""""""""""""""""""" + +A `Docker multi-stage build `_ +is used to create the Docker environment to run the use cases. +The Docker images that contain the :ref:`cg-ci-use-case-dependencies` are +built and the relevant files (such as the Conda environment files) are +copied into the METplus image so that they will be available when running +the use cases. + +Setup Use Case Commands +""""""""""""""""""""""" + +Before **run_metplus.py** is called to run the use case, +some other commands are run in the Docker container. +For example, if another METplus Python component such as +METcalcpy, METplotpy, or METdatadb are required for the use case, +the **develop** branch of those repositories are obtained the Python code +is installed in the Python (Conda) environment that will be used to +run the use case. + +Run the Use Cases +""""""""""""""""" + +The **run_metplus.py** script is called to run each use case. +The **OUTPUT_BASE** METplus configuration variable is overridden to +include the use case name identifier defined in +the :ref:`cg-ci-all-use-cases` file to isolate all of the output for each +use case. If any of the use cases contain an error, then the job for the +use case group will fail and display a red X next to the job on the +GitHub Actions webpage. + +Difference Tests +^^^^^^^^^^^^^^^^ + +After all of the use cases in a group have finished running, the output +that was generated is compared to the truth data to determine if any of +the output was changed. The truth data for each use case group is stored +in a Docker data volume on DockerHub. The **diff_util.py** script +(found in **metplus/util**) is run to compare all of the output files in +different ways depending on the file type. + +The logic in this script could be improved to provide more robust testing. +For example, the logic to compare images has been disabled because the +existing logic was reporting false differences. + +If any differences were found, then the files that contained the differences +are copied into a directory so they can be made available in an artifact. +The files are renamed to include an identifier just before the extension +so that it is easy to tell which file came from the truth data and which came +from the new output. + +.. _cg-ci-create-output-data-volumes: + +Create/Update Output Data Volumes +--------------------------------- + +Differences in the use case output may be expected. +The most common difference is new data from a newly added use case that is +not found in the truth data. If all of the differences are determined to be +expected, then the truth data must be updated so that the changes are included +in future difference tests. +All of the artifacts with a name that starts with **use_cases_** are downloaded +in this job. Data from each group is copied into a Docker image and pushed +up to DockerHub, replacing the images that were used for the difference tests. +See :ref:`cg-ci-push-reference-branch` for information on which events +trigger this job. + +Output (Artifacts) +------------------ + +Error Logs +^^^^^^^^^^ + +If there are errors in any of the use cases, then the log file from the run +is copied into a directory that will be made available at the end of the +workflow run as a downloadable artifact. This makes it easier to review all +of the log files that contain errors. + +Output Data +^^^^^^^^^^^ + +All of the output data that is generated by the use case groups are saved as +downloadable artifacts. Each output artifact name starts with **use_cases_** +and contains the use case category and indices. This makes it easy to obtain +the output from a given use case to review. + +Diff Data +^^^^^^^^^ + +When differences are found when comparing the new output from a use case to +the truth data, an artifact is created for the use case group. It contains +files that differ so that the user can download and examine them. Files that +are only found in one or the other are also included. diff --git a/docs/Contributors_Guide/figure/ci-doc-artifacts.png b/docs/Contributors_Guide/figure/ci-doc-artifacts.png new file mode 100644 index 0000000000000000000000000000000000000000..37ac0b6f35c9cb71c241020b8a60dfc0edc7d78e GIT binary patch literal 6294 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 M&FU)p3OM1v0Ai?4@&Et; literal 0 HcmV?d00001 diff --git a/docs/Contributors_Guide/figure/ci-doc-error.png b/docs/Contributors_Guide/figure/ci-doc-error.png new file mode 100644 index 0000000000000000000000000000000000000000..6e9a149f36505f67b48dea16826e365911c65ae7 GIT binary patch literal 17765 zcmeIZX*gSL-#3~LI-pclX-%a{i&E4)wT99vS}mbwp=yYECbVj34M7LAXlf`m#u#(7 zrcgs@NmL9qhlp8%AbYv4Yw!Dh?)$l)ee4hWdfyN42U$nf;mBI+JkRz2|EBZReFL3S zj29R|Ake8hx>`mc&|zK>=+NScW55}|+x+do;gGkHjs~ctmuDXMa@6sb{w)xwEP-j) zmI3&F(nHtG8w6r$Iruw-asOfu0%`5s(Yp00z=}K?)_oaoIqsR5c={NVFN4^%v3TBd zI#(Wa-0ftqnmzgIxqW6&#Yw>g{Y$i0mjhhSz7ruO=Y7w8{Mi!BWUK$Qv+mA=a=|O7 z%D0kGcOHLD;&}LhKQSVv^>l~ii&vf(loR>yUby;WJ_OtSe*2@0<7{`M|59xQ6-^It zTF#L1UV2~ajiq|e^+Lja`t#R+Q`vOF-WXra7>A;_&C#boAe%?8rG#8UewMM1>-+|u zMenlNRGLs=>xDxgP`efpIAeBN2?V-%E`kLFN@U190s`6J`WsF^J4vG~?@D-26Hj_? z1b=jfTTvT9IPKN<$yiiKI|Q-kt4%b$ghkICnJq(xj)tuHTX8@!gHz_{P37;UY-vTH zB985%E=M&v&Z4w?nNyD41hw^3>#Wv;=qG%%dYx`f>bQ$rV2~?!_rfn~16E~c2Khc= zuN|vJ@Z7xU0ZP+WTQ94%8MSBOMac6968C0RC-)H75do7f4xsHx$8IEjBR+|Y8)u^L z9ho(x@951ww~nj!fA6n+4`JaJuYVVWBM9#fyjJwteN~Gw-xv4pO55*N5&kw+(d2Fw z_`;3AEZIb_kPB$^Z(q5-?3b!m;sr``VlVM4Qt)}n-BWwK$*2<6wwr*z(S@L_b4@&>UxW08uhy$A}~5|aQk=>mNDT<nQ!)dYwGFMP=jV*&bwCQw`m*uo(@tqK!3G z+fJ<~gmmvx%*So4sJP=jh|n$ho~~HRdkqXH+MW+KePp%=8CDDPTzh=Owjwh9Y^(gfs0ir$=zfb+{Xt+CaXuR5*I zrhH$w&1~WLZgMNW$XpU}*(U(FRy}@y+&v^;aSwY~vZ2%J8rS0v2&u8*1e0OtCTaE| ze@OK9AfEnuo0Gmfo;town%%5NUGgc49?}|^iw@iUJUfTAzL6qfT{gYSN*~h8CLK;i z9n6ynx6-sQ#{bUqy{N!NR%`I6@-W?svw-EZ=+k&?Mk zK0%**dai;of#0cWjV#>JZB57;QqkoE%dJFTJHp}A+@bXAQ8M+_c{V#!fz77sg1^m^o zscsXGcc+bSS*??(HGP}1>t*z-@~_dODl7RS-4HMJSVmqlIcBCqwn{-sJ=bGuKgvE*_<~lA)a; zlM~eU)s_t{D%Z+;SoaI7)1vnm^k&!n_K;~9I)!~UW5Ljse@ta} z36Bf2T`QoeJM)!MHA8w-1jD>`ay>(>%DO)8d(W=X_??!jOKIy8-diK(Fb{#I3CEr^ zwOy@Rgmp;sZm%s{s$W=71J%7t{YdBb2`m<`Xv;!kAC@Mx@OQ80WwPY z^9AJdlCGhz17ESQ7$*iq9lhY>EpRJY9`>%wlGum*$tl|0$F}in$LR_e-8)7q-}mEM zx=ar$;=KL2H1;PGp?8x`m-+tek#p@ZPVi7Q6NZ+7;!K$<%N(u`U&S>njszuzXInK1hNgtUf@M^TG#w z=yEgF)^+%k-ETF`g(}zpzf$^FueR9;Gu@Yo+XBoN&WQK!)$8G6t#+l6 z`nY#cd-KMS^Cx(}wzdjq_4Oh2?Hds^JTe}2$~G3prnVqRW_a}^W6LZ7+;UIyv`XqkKXyn;(u9lTz4VV zEy>s3^4D$DAne+azXtg6Qhd%K^OuERfA59-_@b#(<@PgIQ|Es7r9-)uH;FGP`8%;y z(Xc|;ipH75TdoGF@9ND47-E@HmMUyps=6SxlL842!0OQXeL8~0#?FrYL7YWf&D_{W z_4n6?wT9%BuMQiR5}6qpxOF#-BUbL}FDINMAhyUPoE~ob)dVyCIX!(MM0!Mz`$cOzsCcpng9%H#;lxx^yLq zeCr!BS-t7R=|-@U9qiX6znQ-W(F5C}Hn=0FRZE%lotAVSQL-}6T%R?zQ9QV2otx0R zL;bZzN_Hlfsrt!ddl}g!QF`P{FlI3d4&~cbxPxxLfz(_t%d)*=@XFEu)}ALvfR4^ z%ulpMrw7%ibzg=Elq>s;p?8T6ot#Q2mhW1=p?IFys#x~=*_AbI@0|#hak=2lsmwEk zAt|2JhnwYZW;eEl=gH7~-?@qSu=t)@L>?s#0;}muKny9ypIJr6uv z)`+3R&TS)3U9YaQNQ{z+i9>REBdI_Sffh<*L#-KNrh@B2={iH0 z-u>yaV18sTcsDC`o>7_>jn*?Q0TK14^ppA+Jf4u?OuL1t*86EKWQMiDtv@MSWRm^NvnpZ4-XCDd zjCagT?E@d){H`&$sic9UYU)^yWIfk>i+nI1Ycwi(pHq&ea^=%zs&P{W zsSQPP7OR1!>?>h-7-`E&PeRSl-`p@w_X}Iyi+gi@;OL8k>&hEY@#mGEC|MP5lE=3# z`%j+y8uSJm>S|yrQpm(>?0I$m@VP*q5E?4yS%=qh`#=XNAg3|=*{{`B)G69x@YrU+ za}*jT$rsEZcUHL_hUuov^YdRNk_!)OQZGNUg05iFheJ{ON}8C&DOmvvGDxmV*ka~% z;YnUA&xcKzU{XFN$Z;AYcR?M%EoFq{zq zJ7n#leTi8h1LICR@QN8tE+31(t_jXTJ4tD#W5jJ(s7S&bfz9fOSccJHE0Ph6dsYHu$@ zfWxo%=|a^ZVPmONgxd*ttzDen~>GO?a z0){R4nrbLCYOkn9jQO$I)BMdm8wL*PdOqB z0;mDN7oG@se0HecMBXvb&D(z~B^~Mm7;&kq#eo3uH&9}3HYGTZsvCgsp7egKw~??! z+jT;KmRbe{fxgz@)O^x^H8!$buoMbuMFASblo_edv%0Q4Nf4}N2Lpkgc^2L9v8eGu z)H=AU52L;afE6^#^@}8dN2qxh1vjTIRknpL{uZD*MLf6rvc0@IBo|zVT(6%s^=afF zC|zmy?@w1_b8<<7DP7OYf`XpSD>)e;z_Ly&4NVqe^gxI6-ylR|cyXGW9Nsc4yV@09 z$m{T1VMNhPQW@oaeeqQC>Cz@%cXLIJ$=}k?J$m|i%R|@zBnd9%jYd8X?iZ! z^DnFF_o=AwGhSzic;7Ap;aKE2lW2)$)9rDG%F?FaXVR$^eMPV%pj^$K(XkPU^?m2r zwfGafc#GiH*T0h(c?1{T6P0A6z{kF4jU7Fscyb^uqu~czRD_0@&rzfpx!6;ou1VD6 z3%>f+7d1skGB@b@9cll<-xKJK*}CS5dkb?M+wwTI!WH1ysihI^vdqL&UcBUDl1J$Z zU67X!$R1U)qBUn;x>w|os%xeAYF|8uedUiEx+;e#6kBTaeX8=NHp!>xtAO3XQ}%Nz zv~qzTj~KI>r;^`2r;y^fm4?B54K`B`NP6nGE;>iU&32A~?iu-@k-9w#&fLH5YGR}h zN1OH_x}9eRLFTEnv8PgZiK**%qc?ssasVszSs|i(yZ{~Od*x}Y#UZogcTp_cS0RgE zCt6GVa=KyfvUOht@1!*~{>WtV$G&@RPrQHVwklOKI+SlLh3)mGW#0)}0V<>39R#Y= zpS}IV;lwhTSTWSdW9!U}JQ_jXSNRCGkd?{3ZRF8Od<|myYh=Zz>*d}0myO}nSd1q>InZkPsakw2Y|8i zj(+YkjsX7qgBO;Gzd;Sj0RozZ3%?TZnpz!?mpG#a&@<4p)goVFjk)D7-6S#oGBx0R zRWP}rBwb*VzNjE9k^RkxY*f^SGt7P>BCKS6x1Yi#*-&lrHknOr6W)ZyNX(Vvj7QGM)QH5 zium9rKFD&?THNDbKov!?ilYx`wt> zgv*DUuODJUtx^1TWV8Ok_r3bIh$MW9{; zN@f4o8X;LMZqdX2_}m40p#Q2fqjd#5{i1#_A)nVcF$Y<9(-@WyP2eC6hc@ER23eGm zeJ1qcq;}`+UFri}5v=DS3kq+^`rqEI*0UN|BLUq!)J8Qee@+^ay{lhZ(2=Mf{DXpj(IEt{J@6((ZYjs zCiWqnu#fUSj|ikProbiK=CD1tZ%8wlM|rH|`e6{X>}$VsO5IEkq)X;K_Z zz6Yi16*b{bOi96wSs*fTGK&fFS<q#_4HWO1t&cv^qT4^itqA&5$#x*+ z7xDdVg8u*DA^g2qXK7UdfHJH*N=+_sZG1*z@e8{m&YpSpB-CZas<5_2Xd3~SlFnZX z_nx*$pHoH6Mg3V^0SHv)!ovCfc(TomI#T>n0CVH_1G!_ij^w_P+~Z%BolZ=%d#u0v zFzw6d#3HYIlp`KC25rMprkh*Q&hau2iC=_MTQc6XB>TT!U7%i7X*zUo9Q8{~jk-DK z*T3yA^rj{cviMtS+KS~QPkG61VG7pYUB})ixml*FkXKA%7@%vt_qPO3@T!KNd@@Uk zD4ZEOa)m)wch)-JGxUU$^SFlhI`jWjT~Dg(rj7|M#Tc1!efJbPlkaMcQS#id+#gl;$n0Zz8w!aZ(_yhSNX<5>LU z7SC5E6p(W``UFi+j44Vi(d`EnLFLc^dwlR+ykk^doiY5NEmJgLX1zeNid<%ANkCFR zXe5ZCG9)x!NkyeBp-^cUO6>*6T-}VzjW+%EJTP;Kw)>?9E5<*GsM_OdQP+g_pxh<0VetDWIE3J$T%9AIf>`kY0kA z)a`nhzwHl^;JLcGns$fG52KI=>h?4T_kLu|(|@$aWs{X0CAf4wuVpor*t zs$w!PfPIuzOAbLELDmcxJN7ZSVl|=6HVvIVtEb^{m4)~M>g6iS&0ED*dAqkW(bp>5 z-z)er@dpp`p+$nkZ>5bLmmHa=ba;Y1WD^7L^>FtG=AP;0)~o!{R~GkGz+_XTLO=6b z?0b3x^5D`pQJXst_ox}BV=gf^^(N2Xe_nCg-zQDta&cttdLnn253dtz`$!PdF3NSz zO1a$aU;^Zu%aEEc#E~>n6hZRv1KD$C7{YSUD8+Lz3=6X0Sacj2l;1m_f^8^)O@vgd zh@IkHd)PkoyycZ8S@%6X^mXY)js^`Ql>E3**IZA|M+s5BdGo66aow&rcfwpOi67ya z5b+&1O4DLt2;Z*~?@`K-YVm3c!H*xm@PcFMA>On26ruOr>Ym5FNTLF5Y}{i+hA)`f z@Im4GdL|L8hNN5`;uk@)?lb_n?cVcEqJceV=LiWUzQs1oyis&_YbtZFUF&rFG;33pLl_0z85Rx4UiAg>!UTUD1A#LRKEY2uQ z>peQJvfC%n_2GkuGd)IeL@|FD3^_yfiQ`p?8tD>vu8DgWwz(F#Oupz7cfe?5Vj@|r z3vbUCUW~MOZvP+1E_ZX7rsdAti@$d^k36?jf4ZKM&)%^w^gEe^IQHZuZ;-h5xU#J$ z>oDmJezTC!f)J@g>GMZ#V`Oud*Ip1&*9Z=dTf)sk=fteBs=~Vx9zpaz7Pcb=&%l z83SGL+s%N>M{DO`1YvlK#?%E|wOTSJW{MaM|G0mEg;L*oNst(MQEGwKvmYdlgZ6uD zjnt7HO=T3bNZ@EFd`8J4*!p)O)Yn?GN>e9Ma1(nB^@)LOur|kGd83~1c=O?fdgF6mg%dx5k@Fc>j83%3TNj&qwfWc(U!KMLJ;&{(eCX?6 zs+Q<&B(R(~=AGaIM;Bl8@u65(7EXpb{dY+v-*L&IAiD-KJ_vu3Vn_q`A zvxT+qXNVh}@FkW;KH%Id`NYP0d{wQ$q4V&yobeUzCZ%t|)x4*fXZi5Dnb{R#`Y3?_ zQ0fOS2?{B-UIEDlZ0k~ zyAC37dB)=jBk!7N6Pg7Wz_&9FNrjA_Rg0xp(pNua^6La3&ElYXoKO23+^!PA&4z{7 zs-U7=kFX^2lk@mz=oTo{yh`1YRD&7=hWL8IUno<%#G|OGRQCwzI--h|S7YgCqD>|; zqyu6mx2(#O_5xVkTo^!^6F*?+T*v=6ftPMI6#n;m*r43)->mA`-6q7ArygIIv{Nh; z0Kx55l+39yLDQN~{o2hsuI3=n^^d^AhV5SJc^7}Sc8!e${8Jp{bLLd@XIl5QU|K&) z`&4Z++iR#D>=5W#Tk#E_12eMLXZt(xo95Eb6GyZc3o!h`3SIlJ7^?D;R#Qd@G4vxWC1n!@N@3Pc4%0=Q%cZ- zYJEtF`8HOp>jd9~@c}XnS&quXbxoIvGrhnxfR%HG_?OZ*K3XrFJU)7kl{biIa;R53 zIF5?i^+g7qxv+6u+yS?F$mWNTk5A;QPsu{gIud^ZO}6@i35}@TLjGt{Pq4dA;gH_* zG+p9phZWNbF+QNMaTkmDRL<7hST2j|Hn@qm0B@D=x2f(WazDi^;#RY#t!C@t=xo}B z?~;_6%gEV4A>9Cx2sc*zySbiMWz{m)#4EIZ5l0WRl0nBlH_Y4NgoT_jBasT|Pugi$ z{v~UeY&-1cB%!WQzL;#UvbpvoY>E_3`{{pg2Xh3p9l2ilVG8=?zW3VKYym3n z1TR}1GBamL-aX`-n5nI&7&9a9%*oH15wMI}4V_5$ypK zOr2u%xxf2jbI&7J{cJgOJ;-kyAg%Zk+|_n7er_i`rl2a6W=f(ErRKR=K}D~-J)Ple zXjk&`Id9YJoSa=#n|6W1o9i*V+%AYgL3LyeP5sHBC}k`SA&RTBRAqTnV2*l9e7WgD zC}fvb@Z`?lyAOLr_Oc6}>Yqx1zJNKkk1rKgEswcij!r>~5em35u48e>K_;ToIwHdh zHm~QI2L7s6E4Phm%^oF_5+}kj_GL)c^{6LEwos)>;Q%GNN#++WanmKJ- zCMjuFD`3Gs+BP2sf_~42}SwN0R5%6=@Nja!x-YcT;x`YlB;;%P5L)KMnbj3!@KdEoNF*@|B;4`7o?!olbN zQ6c|Dg=~SQHFh1JeN+j(1?Y~paB7L~VwyM6V=kv@>5AYSQDwQ2xB4&gd|npO-!`Yq z7CO7$yJ`8K)Jgej#RmoQ!OraEXXA|FcNllB*iMKt^S*+Fxz0^aF6-l6hNk#V%2;+O zcLo+G1P6>ANlgez7CI}wDpR$v-6Z`gSgDDMR=GDUjqX zp~TC`=K(6X5vNLf4YHY6YYvEH1l`IJ^H8u2bIR{yyZU;}o^0f11_C&BOu+%CwvrRW z{H)|pKzxQ7ExdMsDR|o;DTxSIQE)rUq6$$U9XVYQ54+8p~xrkPmoLGM)n% z0qd+@J#t*hckxA8J>|jJ_zGjpFy8JA!JC#3Z`|GGFi=A!z#-CiO%LC{s!k|=oHeVt z-EfLNl8+7XQNxpF%h)of4x%DwRE(Z$M(ll#6uG;7n}(&u6p0D;C`oV2lc9MrlctE9 zKfo}Jf0HoF?!@)yKB-DDC-gvQ770?mDkI0Wso+XH*73amj_Eura#W-WngS%EXe$)M zEzHGqt@gmMF5j;8>qFqtJapZqu14x@D%)$AX-_LdQC`Pf@xFz;ce{n{c$!W@V0o&e zpa`|(v+(cM7pQ0b(;iDBB|XxBp8gNfb+Fjzg0@IiDz+jHz{!zL1qDgCrThbgl6+$x7iQv-ZVGE6w;e0@7ymQ;&g$ zvAK75A=#q-g7UMn4oAN`nc413C+4CL^vXXF=FXd6vszbU-_cc4a~cn`U3~--dG1Qu z%-dO3E^EM_{pL5p)#%wHeLIZq%jPmngGX0JN1F3boo^p?Hgpy1GU7+0)gYKV9dfFe zuW4FF5We00g1(E(@A#%dSAz!Y_Km-2G?u8}?hlnbBwms}z8jRIwoZ}3hww#5vY1(K z&h>ELGRGP(lmXzT!|*)hIE#p1)g_00+8v);MFEl7uI>h}7mgN+xCg3Le&P&ohA`dQ zdpy&bW7rW17zIl0S<6r8UFh#~N#p{`($oXk#pbdBno3KJqmqfSwIybBo-BiEiX4Mj zQywHha;5Zw@ig<&Ls>kAzYNR5D1p9lFQyiUE|!n0KLMo<0v*yaYGn+rwm#Z5@wV+d z6EDO28q%b#NEkJ!EvG44WJ5P|fr+<7!DNs3oHj=CSiz^4b%-Z{in!93K2VV}kPQm* zH>KD`Jt#p(R^n>brqYB0u%v(DluRZRrjZ7AQ=1!`=M@c1WZH+ys2Z53%kH0*;1k=w zy4=_Un?_xNHikf-2#=##q^4sqyI_UHGxZ`Ddn7x!G*3Sgt!R=5U2pmqC_Jvp!}Vlr z8k$UEb2nqUSoh*iyh&>H7u^cCB&2l*Xo*v%PQ)6Ic{T5@vCvBB1I-eT2>Ucs zYrqKe1Ykz>(x_AH-q4>gxb6P>=#=M=bT$5sKXNi9$^roXHI@AEQ$!G|=LIn3Uxb+I zQ`Jz41b^zoeiTE-kZiQ#SUs-Hy*Cq3O84usU#4fTH&FdX3KI6nTR|Wb!`TXLwPn51 zH3T@BO(n`|W~grdiTCKHd=Q{+4?i#<>Pr<1Z{_QE7+p6}6s8^Gx+HE9nR&@`F@bDh zvD-Kw6z@Fg6#2enCS$q*hf!5oK6Ed-`_00-;_z;L>c4x`_0^Z8x9=WG9Ug>pM6+Bi znhJ@v#`cx1l)XaVzP?(SGt6!hVsMCfdf+0h?RFTh@StB@n`3k#`~j9BaWv79TRuxb zc#~7B?r=0aB_)}WH%V|_A6MRJu_MZKQxUH9;2oLd+tT%9Cg20*$a?rfgg4*?OfjO? zW88z{mU7@#|DPMi-jRwq%0u4Z?QOC25LbR)jQ!ivqc%P}6_N;-ZDvGC*cfiz(+UvX zOKK@EFMqqlp`bNn{n+g2h}N>OKRwVo$yYna4D(UQ7qGjs6mZ%%cM4)P22+M9re!5$ ze6W5dhJ#?hnbykHqUZmQUSZ)mu*X5(TsLdaP&27}TF2_x7#jVLULig<)zCCk*CUio zF{b7tmw9iG>i*2SkJ zy1j}TV6X8H2)>Jjc*jMBS7?a!(@6OL&>OD4R=nM#4fEDfP#5q2%l98dbjwAe!~qOt z^P0lg`YgPp6J-@vKu5E81?#i9;zfmmEGpvq$HOdJKTyz90_1NlftY|O&+1`z!uRrl zXe1IlQa)bkTyjf8j zkM2mX68cwd_Kf9eF{_Cygf#cKne``Y@UsPvi1(rU^4K-|AL9o9YNizq*C*&~K0kpm zT3%Mbs z*24wCj4|7myl&sgE-t$Aw_d||6bNh=mtXogqyu0}E*^Q=)5awI=^Sk%Q zKwo$NTV=Kdh=qc1{eUQ90RI2MBh~{ByjazOGzA-~en^4m-TB>{Rlax#S9Tw<$A1-$aRr>!^=+ zrR5vn?_zw_I)p=@uah`wAHa!RV$>-b-Ol9S&fDZT?cW9&3sx~8r>GC!kN|qZBndv3 zqX<(A82uxMGK$;$~R$7_NF<_SUt;!21gy`87t26B%3vnGZ(g z(Bh8MXEBcxRCc|Zke4NW576IcAk^8Y%&!^DpQT^zbkzxyfoKF^-l6_7?>0zyQnWoS zXteYb1I=t>Gn^P2F}0wq1_b%w2p1QWVR0eN9qzOX)8z%@OK3bsyR>Q-1SE1{FZ78| zy5`t88@#c4d@zW1{0VmRGhE-5-<5xiv;(ab*0jkWVsk^5re*xa_6bLS z6=-MXbtrp%vhKqG>6K!teT$xzIboqp!d}W@yyRxxo$)Zfv*(e9f0%fkzt%tG`91Rd z!f3UvfiBWwO}*e6eLcH+ z6aJYR6185ox3$cCV<;GDtyYqd8K=$fPgSU5{rDROx}F-6%<7M7#C{G59d#@&dO(&u z)P<5#M*@~&BGjH>Y95mj2mR(z@Jd$PC;49GpuICeSr})+cCt3>cf?*!O!kFn7d(UD z9iVFn-*r(R@XMcia{)5GDn?{Ao2!#X+aMhN$--M}jNt@}R9W`v1S~$D;_+HMr$DJ2 zHMdgrhd4pm7d1T=Ni%JVqhSpopV!-8Y0M0W#sd>?q)dg|^HTnZxG0u(7(X}qlGT<;wI1@QBPvwLfC3hWrS2?%nyg{qZs`6(c+(MI0Bv7sq| zQX{)X7S_4<8o1(eHkar8zkyslPuZVyHJp#2vlxzPq@bE3mCk zOi*B6hjZA5X3Dm{bC2WQO&9A&beM>bi+VUYC;=-D*`Qi;R?AhnjSp#$Z)KBogSYmh zmW{g;$s;a0IB2|#=i+Sz%4VQs7YQ9hro`TS6t9h2rPNzh!+2=pG9Z~Uge;XoCk!`T zk%!)@oXwaq>*^C28H8KVJxhE09&nseO$uUnky@2!_o{W=$y zQ-SWH>~li)P%JK|c=+)fkJ=HYb5-R}BcRgBJF*S?v38}|B0Gn)+a7Y{7c{j9KHAdG za#3&DI}xjgUA+@uM%rqlWV=|3acN?<79iwJ4nDjyUFhfliTP^ok?O)>!km^uufVc< z>39WCaGtk{>Y`i{dehWd6VCgr@EbrhCQ1Tcf=9MRxuqji{JJ=6o1Z%^us-iHiHv?~ zj(ASE^Nodx{+(gsb9o&V^z|$N+W(ZU{AV=(65>IXF28V#`c8_0FFUn%;bv#e$P@qSO|qYRseVl16|rsaQZ5qPf1;Zl*zb~?E~ z?}zU_K=Yil8(%Y!4uy3$d!PDRS*!T;F=F)JX-H#rHiuHdouDltekpGN^T<7{A1<5h z^K_lFn}m7r_nHzKfL}bLnoD?sREVw@NqH3995b#1S|^_19pq#K)6Nwb%cOQ6_+fFL z){)9DdwKmcHp6wnzdf4wqW}eXhTBrD#QM?+2BWiSc*!P9py@DbPus6)M4x^Cn;Q*Q zu&@=6O$yx$+DrT+G%{$T9*ed*J(}QQ0mF!>whyg(LwhJYwdVL@MCd7S>8Rg6tsn1L zW#BvV!CPpkoiMkub#{P;^K~1V*$gghuMe}KSkMa)6h-q*wsb_@HGJz)8}S*7pC2ol zx0MtAYZTA@rN?1OrWayr+D!}GS@LDkDf`u^K?v_@yXH6f;y3o!mnH|#qMz{oNB5bqqeYKpqE*C0r5A9m!}ZxB>} zuS@hG0}(l3-_I$B*tdAF?E7f#{?~6l6<)kr+>c+CpGXde2<{EDx)flGJGNfaRNRSQP<$ z)ib8|?-f;2E}al-#5jKCs)t+G&@&+S8i1e%**l+wHv?9%>0|DN&|%>XI?$&SjLjE$ zBt&v-#6cy3GZq81&jRT({~X*?*3@zN0Y9s#7Zb0f_&DH-h0#r!P zTH{qz8MX^wjctK4%Dr5qHtGOXrgKEMj(MVy7dG3N(lXD&tD(Q6!Rc#$<&n9|q-09) zw=9!$*`@mJMom%pum{ild_4Oj%O*Jp_l`OeYSrQ=b z6?x?60RLgUC0F9zv|ts`y|&?K79hp1TiOR!13W>%Z)Cam{;VZB>B!ds*FW~}7wY^C z+x7p4bl)GlG}>-Qz%S_}qyg3ZeZ`zas$L4x6xfD#$J~`0DB>l6dC=r{ptIT;iJ3@h-uXA1M4h0QuyU7agTD+c4!Z zIZ;5&n|^@HsoNg{mG5l(|95E*g(_=T#AgpyZ9oT%zr0?zb7rxmnI@2v4+A*K9y#Sd zB|r4a-NzF3z#={K>kCy4g@Qj!Hp)I59OXXxz4EbJHWNzhcb#Q|fcQ#K<7_Ka7>jes z_g1_+lXadu|7cy^vXjNyk!QYkbCiXyoIN9p?zX(xu&&yM0(r62V0l3w3vV);Ubv2^ z$IH^DoQn_IjoVBuAC!t{H(Cx_sryIPDoSnK8K^7_mz?^7AYH#t85J^fdMIK+x~pRc zGo<;}QKe*`fWC6sFR70FqRe`oNRS3{TI z>JaOTn`RqbZ|+7u@7S82E~K$&>Nvv^f-Gk81=#5bgGQO+5dceI4pKP9f<{inFRKDS&VzY^me4ad)qVg^zV zfwIf2Us}R`dRKI})lnkX*Vce7HUFfAXVUJBq;Xqii86Yc-S3(#Adnr3ceYhBFa1E^ zmki3iMtkp{je-yC@a-w(!|uhO2bK2zL%$$d!IKM!3+PL24Q}f(WR=Y9D-_B?I7#MS zIjmD|3}@jt}!wYvAIzLvlPo+)o?FB$B2Hnv%&??ce H3ID$U^nijJ literal 0 HcmV?d00001 diff --git a/docs/Contributors_Guide/figure/gha-workflow-name.png b/docs/Contributors_Guide/figure/gha-workflow-name.png old mode 100755 new mode 100644 index bfcd5cb8f37ac83e636c79bd2d29b4edaf9ea812..4d62965c7a63edde5d276f1c11ca660c4ffbd62b GIT binary patch literal 17475 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 F{|#`^iHZOK literal 13030 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 P3_$C#-lGzA+qeG*OZS0? diff --git a/docs/Users_Guide/installation.rst b/docs/Users_Guide/installation.rst index be724cc5a3..ab33891e3d 100644 --- a/docs/Users_Guide/installation.rst +++ b/docs/Users_Guide/installation.rst @@ -202,7 +202,6 @@ The METplus Wrappers source code contains the following directory structure:: METplus/ build_components/ - ci/ docs/ environment.yml internal_tests/ @@ -212,6 +211,7 @@ The METplus Wrappers source code contains the following directory structure:: produtil/ README.md requirements.txt + scripts/ setup.py ush/ @@ -222,9 +222,6 @@ The **build_components/** directory contains scripts that use manage_externals and files available on dtcenter.org to download MET and start the build process. -The **ci/** directory contains scripts that are used for creating -Docker images and scripts that are used internally for automation. - The **docs/** directory contains documentation for users and contributors (HTML) and Doxygen files that are used to create the METplus wrapper API documentation. The @@ -247,6 +244,9 @@ METplus Wrappers. The **produtil/** directory contains part of the external utility produtil. +The **scripts/** directory contains scripts that are used for creating +Docker images. + The **ush/** directory contains the run_metplus.py script that is executed to run use cases. diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index fc4c3c0cd5..051b1e961e 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -1,5 +1,5 @@ Category: met_tool_wrapper -0::CyclonePlotter::met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf,user_env_vars.MET_PYTHON_EXE=python3:: cycloneplotter_env,cartopy +0::CyclonePlotter::met_tool_wrapper/CyclonePlotter/CyclonePlotter.conf:: cycloneplotter_env,cartopy, py_embed 1::PCPCombine_python_embedding:: met_tool_wrapper/PCPCombine/PCPCombine_python_embedding.conf:: h5py_env,py_embed 2::ASCII2NC:: met_tool_wrapper/ASCII2NC/ASCII2NC.conf 3::met_tool_wrapper/ASCII2NC/ASCII2NC_python_embedding.conf @@ -130,7 +130,7 @@ Category: s2s 9:: UserScript_obsERA_obsOnly_OMI:: model_applications/s2s/UserScript_obsERA_obsOnly_OMI.conf:: spacetime_env, metdatadb 10:: UserScript_obsERA_obsOnly_RMM:: model_applications/s2s/UserScript_obsERA_obsOnly_RMM.conf:: spacetime_env, metdatadb 11:: UserScript_fcstGFS_obsERA_WeatherRegime:: model_applications/s2s/UserScript_fcstGFS_obsERA_WeatherRegime.conf:: weatherregime_env,cartopy,metplus -12:: UserScript_obsERA_obsOnly_Stratosphere:: model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf:: metplotpy_env,metdatadb,metplus +12:: UserScript_obsERA_obsOnly_Stratosphere:: model_applications/s2s/UserScript_obsERA_obsOnly_Stratosphere.conf:: metplotpy_env,metdatadb Category: space_weather 0::GridStat_fcstGloTEC_obsGloTEC_vx7:: model_applications/space_weather/GridStat_fcstGloTEC_obsGloTEC_vx7.conf diff --git a/ci/docker/Dockerfile b/scripts/docker/Dockerfile similarity index 100% rename from ci/docker/Dockerfile rename to scripts/docker/Dockerfile diff --git a/ci/docker/docker_data/Dockerfile b/scripts/docker/docker_data/Dockerfile similarity index 100% rename from ci/docker/docker_data/Dockerfile rename to scripts/docker/docker_data/Dockerfile diff --git a/ci/docker/docker_data/build_docker_images.sh b/scripts/docker/docker_data/build_docker_images.sh similarity index 100% rename from ci/docker/docker_data/build_docker_images.sh rename to scripts/docker/docker_data/build_docker_images.sh diff --git a/ci/docker/docker_data/hooks/build b/scripts/docker/docker_data/hooks/build similarity index 100% rename from ci/docker/docker_data/hooks/build rename to scripts/docker/docker_data/hooks/build diff --git a/ci/docker/docker_data_output/Dockerfile b/scripts/docker/docker_data_output/Dockerfile similarity index 100% rename from ci/docker/docker_data_output/Dockerfile rename to scripts/docker/docker_data_output/Dockerfile diff --git a/ci/docker/docker_env/Dockerfile b/scripts/docker/docker_env/Dockerfile similarity index 100% rename from ci/docker/docker_env/Dockerfile rename to scripts/docker/docker_env/Dockerfile diff --git a/ci/docker/docker_env/Dockerfile.gempak_env b/scripts/docker/docker_env/Dockerfile.gempak_env similarity index 100% rename from ci/docker/docker_env/Dockerfile.gempak_env rename to scripts/docker/docker_env/Dockerfile.gempak_env diff --git a/ci/docker/docker_env/Dockerfile.gfdl-tracker b/scripts/docker/docker_env/Dockerfile.gfdl-tracker similarity index 100% rename from ci/docker/docker_env/Dockerfile.gfdl-tracker rename to scripts/docker/docker_env/Dockerfile.gfdl-tracker diff --git a/ci/docker/docker_env/Dockerfile.metplus_base b/scripts/docker/docker_env/Dockerfile.metplus_base similarity index 100% rename from ci/docker/docker_env/Dockerfile.metplus_base rename to scripts/docker/docker_env/Dockerfile.metplus_base diff --git a/ci/docker/docker_env/Dockerfile.py_embed_base b/scripts/docker/docker_env/Dockerfile.py_embed_base similarity index 100% rename from ci/docker/docker_env/Dockerfile.py_embed_base rename to scripts/docker/docker_env/Dockerfile.py_embed_base diff --git a/ci/docker/docker_env/README.md b/scripts/docker/docker_env/README.md similarity index 99% rename from ci/docker/docker_env/README.md rename to scripts/docker/docker_env/README.md index dc40f5ad0c..9b8adc0a63 100644 --- a/ci/docker/docker_env/README.md +++ b/scripts/docker/docker_env/README.md @@ -1,6 +1,6 @@ # Docker Conda Environments -Run the commands from this directory (ci/docker/docker_env). +Run the commands from this directory (scripts/docker/docker_env). Instructions include how to create Docker images in dtcenter/metplus-envs so environments are available for the automated tests. Instructions to create these Conda environments on a local machine are also provided. diff --git a/ci/docker/docker_env/scripts/cfgrib_env.sh b/scripts/docker/docker_env/scripts/cfgrib_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/cfgrib_env.sh rename to scripts/docker/docker_env/scripts/cfgrib_env.sh diff --git a/ci/docker/docker_env/scripts/cycloneplotter_env.sh b/scripts/docker/docker_env/scripts/cycloneplotter_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/cycloneplotter_env.sh rename to scripts/docker/docker_env/scripts/cycloneplotter_env.sh diff --git a/ci/docker/docker_env/scripts/diff_env.sh b/scripts/docker/docker_env/scripts/diff_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/diff_env.sh rename to scripts/docker/docker_env/scripts/diff_env.sh diff --git a/ci/docker/docker_env/scripts/gempak_env.sh b/scripts/docker/docker_env/scripts/gempak_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/gempak_env.sh rename to scripts/docker/docker_env/scripts/gempak_env.sh diff --git a/ci/docker/docker_env/scripts/h5py_env.sh b/scripts/docker/docker_env/scripts/h5py_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/h5py_env.sh rename to scripts/docker/docker_env/scripts/h5py_env.sh diff --git a/ci/docker/docker_env/scripts/icecover_env.sh b/scripts/docker/docker_env/scripts/icecover_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/icecover_env.sh rename to scripts/docker/docker_env/scripts/icecover_env.sh diff --git a/ci/docker/docker_env/scripts/metdatadb_env.sh b/scripts/docker/docker_env/scripts/metdatadb_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/metdatadb_env.sh rename to scripts/docker/docker_env/scripts/metdatadb_env.sh diff --git a/ci/docker/docker_env/scripts/metplotpy_env.sh b/scripts/docker/docker_env/scripts/metplotpy_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/metplotpy_env.sh rename to scripts/docker/docker_env/scripts/metplotpy_env.sh diff --git a/ci/docker/docker_env/scripts/metplus_base_env.sh b/scripts/docker/docker_env/scripts/metplus_base_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/metplus_base_env.sh rename to scripts/docker/docker_env/scripts/metplus_base_env.sh diff --git a/ci/docker/docker_env/scripts/netcdf4_env.sh b/scripts/docker/docker_env/scripts/netcdf4_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/netcdf4_env.sh rename to scripts/docker/docker_env/scripts/netcdf4_env.sh diff --git a/ci/docker/docker_env/scripts/py_embed_base_env.sh b/scripts/docker/docker_env/scripts/py_embed_base_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/py_embed_base_env.sh rename to scripts/docker/docker_env/scripts/py_embed_base_env.sh diff --git a/ci/docker/docker_env/scripts/pygrib_env.sh b/scripts/docker/docker_env/scripts/pygrib_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/pygrib_env.sh rename to scripts/docker/docker_env/scripts/pygrib_env.sh diff --git a/ci/docker/docker_env/scripts/pytest_env.sh b/scripts/docker/docker_env/scripts/pytest_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/pytest_env.sh rename to scripts/docker/docker_env/scripts/pytest_env.sh diff --git a/ci/docker/docker_env/scripts/spacetime_env.sh b/scripts/docker/docker_env/scripts/spacetime_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/spacetime_env.sh rename to scripts/docker/docker_env/scripts/spacetime_env.sh diff --git a/ci/docker/docker_env/scripts/weatherregime_env.sh b/scripts/docker/docker_env/scripts/weatherregime_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/weatherregime_env.sh rename to scripts/docker/docker_env/scripts/weatherregime_env.sh diff --git a/ci/docker/docker_env/scripts/xesmf_env.sh b/scripts/docker/docker_env/scripts/xesmf_env.sh similarity index 100% rename from ci/docker/docker_env/scripts/xesmf_env.sh rename to scripts/docker/docker_env/scripts/xesmf_env.sh diff --git a/ci/docker/hooks/build b/scripts/docker/hooks/build similarity index 100% rename from ci/docker/hooks/build rename to scripts/docker/hooks/build diff --git a/ci/docker/hooks/get_met_version b/scripts/docker/hooks/get_met_version similarity index 100% rename from ci/docker/hooks/get_met_version rename to scripts/docker/hooks/get_met_version From b302a933cfd6c9b8361d9654b1fb69df7ed7fc68 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 4 Feb 2022 13:51:38 -0700 Subject: [PATCH 281/770] feature 1382 Filename templates in MODEL (#1414) --- metplus/wrappers/compare_gridded_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metplus/wrappers/compare_gridded_wrapper.py b/metplus/wrappers/compare_gridded_wrapper.py index 66caa20ec0..358a3a9c12 100755 --- a/metplus/wrappers/compare_gridded_wrapper.py +++ b/metplus/wrappers/compare_gridded_wrapper.py @@ -58,8 +58,8 @@ def create_c_dict(self): metplus_configs=['OBTYPE']) # set old MET config items for backwards compatibility - c_dict['MODEL_OLD'] = self.config.getstr('config', 'MODEL', 'FCST') - c_dict['OBTYPE_OLD'] = self.config.getstr('config', 'OBTYPE', 'OBS') + c_dict['MODEL_OLD'] = self.config.getraw('config', 'MODEL', 'FCST') + c_dict['OBTYPE_OLD'] = self.config.getraw('config', 'OBTYPE', 'OBS') # INPUT_BASE is not required unless it is referenced in a config file # it is used in the use case config files. Don't error if it is not set From b98212fa60eec4eda74a0e67c73e0bef25a74db3 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 7 Feb 2022 10:31:03 -0700 Subject: [PATCH 282/770] Feature 1289 explicit file list (#1387) --- docs/Users_Guide/glossary.rst | 95 +++++++++- docs/Users_Guide/wrappers.rst | 18 +- .../series_analysis/test_series_analysis.py | 12 +- metplus/wrappers/command_builder.py | 27 ++- metplus/wrappers/ensemble_stat_wrapper.py | 91 +-------- metplus/wrappers/gen_ens_prod_wrapper.py | 10 +- metplus/wrappers/mtd_wrapper.py | 103 ++++++++--- metplus/wrappers/series_analysis_wrapper.py | 173 ++++++++++++------ metplus/wrappers/tcrmw_wrapper.py | 64 ++++--- .../met_tool_wrapper/PB2NC/PB2NC.conf | 4 +- ...tat_fcstHRRRE_obsHRRRE_Sfc_MultiField.conf | 8 +- 11 files changed, 381 insertions(+), 224 deletions(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 80ee89c001..20085aa151 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -2695,7 +2695,7 @@ METplus Configuration Glossary | *Used by:* EnsembleStat, GridStat, MODE, MTD, PB2NC, PointStat OBS_FILE_WINDOW_END - Used to control the upper bound of the window around the valid time to determine if a file should be used for processing. See :ref:`Directory_and_Filename_Template_Info` subsection called 'Using Windows to Find Valid Files.' Units are seconds.This value will be used for all wrappers that look for an observation file unless it is overridden by a wrapper specific configuration variable. For example, if :term:`OBS_GRID_STAT_WINDOW_END` is set, the GridStat wrapper will use that value. If :term:`PB2NC_WINDOW_END` is not set, then the PB2NC wrapper will use :term:`OBS_WINDOW_END`. A corresponding variable exists for forecast data called :term:`FCST_FILE_WINDOW_END`. + Used to control the upper bound of the window around the valid time to determine if a file should be used for processing. See :ref:`Directory_and_Filename_Template_Info` subsection called 'Using Windows to Find Valid Files.' Units are seconds.This value will be used for all wrappers that look for an observation file unless it is overridden by a wrapper specific configuration variable. For example, if :term:`OBS_GRID_STAT_FILE_WINDOW_END` is set, the GridStat wrapper will use that value. If :term:`PB2NC_FILE_WINDOW_END` is not set, then the PB2NC wrapper will use :term:`OBS_FILE_WINDOW_END`. A corresponding variable exists for forecast data called :term:`FCST_FILE_WINDOW_END`. | *Used by:* EnsembleStat, GridStat, MODE, MTD, PB2NC, PointStat @@ -3173,12 +3173,12 @@ METplus Configuration Glossary .. warning:: **DEPRECATED:** Please use :term:`OBS_WINDOW_BEGIN`. OBS_WINDOW_BEGIN - Passed to the MET config file to determine the range of data within a file that should be used for processing.Units are seconds. This value will be used for all wrappers that look for an observation file unless it is overridden by a wrapper specific configuration variable. For example, if :term:`OBS_POINT_STAT_WINDOW_BEGIN` is set, the PointStat wrapper will use that value. If :term:`PB2NC_WINDOW_BEGIN` is not set, then the PB2NC wrapper will use :term:`OBS_WINDOW_BEGIN`. A corresponding variable exists for forecast data called :term:`FCST_WINDOW_BEGIN`. + Passed to the MET config file to determine the range of data within a file that should be used for processing.Units are seconds. This value will be used for all wrappers that look for an observation file unless it is overridden by a wrapper specific configuration variable. For example, if :term:`OBS_POINT_STAT_WINDOW_BEGIN` is set, the PointStat wrapper will use that value. If :term:`PB2NC_OBS_WINDOW_BEGIN` is not set, then the PB2NC wrapper will use :term:`OBS_WINDOW_BEGIN`. A corresponding variable exists for forecast data called :term:`FCST_WINDOW_BEGIN`. | *Used by:* PB2NC, PointStat OBS_WINDOW_END - Passed to the MET config file to determine the range of data within a file that should be used for processing.Units are seconds. This value will be used for all wrappers that look for an observation file unless it is overridden by a wrapper specific configuration variable. For example, if :term:`OBS_POINT_STAT_WINDOW_END` is set, the PointStat wrapper will use that value. If :term:`PB2NC_WINDOW_END` is not set, then the PB2NC wrapper will use :term:`OBS_WINDOW_END`. A corresponding variable exists for forecast data called :term:`FCST_WINDOW_END`. + Passed to the MET config file to determine the range of data within a file that should be used for processing.Units are seconds. This value will be used for all wrappers that look for an observation file unless it is overridden by a wrapper specific configuration variable. For example, if :term:`OBS_POINT_STAT_WINDOW_END` is set, the PointStat wrapper will use that value. If :term:`PB2NC_OBS_WINDOW_END` is not set, then the PB2NC wrapper will use :term:`OBS_WINDOW_END`. A corresponding variable exists for forecast data called :term:`FCST_WINDOW_END`. | *Used by:* PB2NC, PointStat @@ -3317,12 +3317,12 @@ METplus Configuration Glossary | *Used by:* PB2NC - PB2NC_WINDOW_BEGIN + PB2NC_OBS_WINDOW_BEGIN Passed to the pb2nc MET config file to determine the range of data within a file that should be used for processing.Units are seconds. If the variable is not set, pb2nc will use :term:`OBS_WINDOW_BEGIN`. | *Used by:* PB2NC - PB2NC_WINDOW_END + PB2NC_OBS_WINDOW_END Passed to the pb2nc MET config file to determine the range of data within a file that should be used for processing. Units are seconds. If the variable is not set, pb2nc will use :term:`OBS_WINDOW_END`. | *Used by:* PB2NC @@ -3649,6 +3649,20 @@ METplus Configuration Glossary | *Used by:* SeriesAnalysis + BOTH_SERIES_ANALYSIS_INPUT_DIR + Specify the directory to read forecast and observation input from the + same file in SeriesAnalysis. + See also :term:`BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE` + + | *Used by:* SeriesAnalysis + + BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE + Template to find forecast and observation input from the + same file in SeriesAnalysis. + See also :term:`BOTH_SERIES_ANALYSIS_INPUT_DIR` + + | *Used by:* SeriesAnalysis + SERIES_ANALYSIS_OUTPUT_DIR Specify the directory where files will be written from the MET series analysis tool. @@ -8960,3 +8974,74 @@ METplus Configuration Glossary See also :term:`SERIES_ANALYSIS_CLIMO_STDEV_USE_FCST`. | *Used by:* SeriesAnalysis + + FCST_SERIES_ANALYSIS_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass into + series_analysis with the -fcst argument. If set, + :term:`OBS_SERIES_ANALYSIS_INPUT_FILE_LIST` must also be set and + :term:`FCST_SERIES_ANALYSIS_INPUT_TEMPLATE` and + :term:`FCST_SERIES_ANALYSIS_INPUT_DIR` are ignored. + See also :term:`BOTH_SERIES_ANALYSIS_INPUT_FILE_LIST`. + + | *Used by:* SeriesAnalysis + + OBS_SERIES_ANALYSIS_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass into + series_analysis with the -fcst argument. If set, + :term:`FCST_SERIES_ANALYSIS_INPUT_FILE_LIST` must also be set and + :term:`OBS_SERIES_ANALYSIS_INPUT_TEMPLATE` and + :term:`OBS_SERIES_ANALYSIS_INPUT_DIR` are ignored. + See also :term:`BOTH_SERIES_ANALYSIS_INPUT_FILE_LIST`. + + | *Used by:* SeriesAnalysis + + BOTH_SERIES_ANALYSIS_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass into + series_analysis with the -both argument. If set, + :term:`BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE` and + :term:`BOTH_SERIES_ANALYSIS_INPUT_DIR` are ignored. + See also :term:`FCST_SERIES_ANALYSIS_INPUT_FILE_LIST` and + :term:`OBS_SERIES_ANALYSIS_INPUT_FILE_LIST`. + + | *Used by:* SeriesAnalysis + + FCST_MTD_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass into + mtd with the -fcst or -single argument. If set, + :term:`FCST_MTD_INPUT_TEMPLATE` and + :term:`FCST_MTD_INPUT_DIR` are ignored. + See also :term:`OBS_MTD_INPUT_FILE_LIST`. + + | *Used by:* MTD + + OBS_MTD_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass into + mtd with the -obs or -single argument. If set, + :term:`OBS_MTD_INPUT_TEMPLATE` and + :term:`OBS_MTD_INPUT_DIR` are ignored. + See also :term:`FCST_MTD_INPUT_FILE_LIST`. + + | *Used by:* MTD + + TC_RMW_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass into tc_rmw. + If set, :term:`TC_RMW_INPUT_TEMPLATE` and :term:`TC_RMW_INPUT_DIR` + are ignored. + + | *Used by:* TCRMW + + FCST_ENSEMBLE_STAT_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass ensembles into + ensemble_stat. If set, + :term:`FCST_ENSEMBLE_STAT_INPUT_TEMPLATE` and + :term:`FCST_ENSEMBLE_STAT_INPUT_DIR` are ignored. + + | *Used by:* EnsembleStat + + GEN_ENS_PROD_INPUT_FILE_LIST + Specifies an explicit path to a file list file to pass ensembles into + gen_ens_prod. If set, + :term:`GEN_ENS_PROD_INPUT_TEMPLATE` and + :term:`GEN_ENS_PROD_INPUT_DIR` are ignored. + + | *Used by:* GenEnsProd diff --git a/docs/Users_Guide/wrappers.rst b/docs/Users_Guide/wrappers.rst index f63cc7b0fa..f486675110 100644 --- a/docs/Users_Guide/wrappers.rst +++ b/docs/Users_Guide/wrappers.rst @@ -182,6 +182,7 @@ METplus Configuration | :term:`OBS_ENSEMBLE_STAT_POINT_INPUT_TEMPLATE` | :term:`OBS_ENSEMBLE_STAT_GRID_INPUT_TEMPLATE` | :term:`FCST_ENSEMBLE_STAT_INPUT_TEMPLATE` +| :term:`FCST_ENSEMBLE_STAT_INPUT_FILE_LIST` | :term:`ENSEMBLE_STAT_OUTPUT_TEMPLATE` | :term:`ENSEMBLE_STAT_CTRL_INPUT_DIR` | :term:`ENSEMBLE_STAT_CTRL_INPUT_TEMPLATE` @@ -1031,6 +1032,7 @@ METplus Configuration | :term:`GEN_ENS_PROD_INPUT_DIR` | :term:`GEN_ENS_PROD_INPUT_TEMPLATE` +| :term:`GEN_ENS_PROD_INPUT_FILE_LIST` | :term:`GEN_ENS_PROD_CTRL_INPUT_DIR` | :term:`GEN_ENS_PROD_CTRL_INPUT_TEMPLATE` | :term:`GEN_ENS_PROD_OUTPUT_DIR` @@ -4598,6 +4600,8 @@ METplus Configuration | :term:`MTD_OUTPUT_DIR` | :term:`FCST_MTD_INPUT_TEMPLATE` | :term:`OBS_MTD_INPUT_TEMPLATE` +| :term:`FCST_MTD_INPUT_FILE_LIST` +| :term:`OBS_MTD_INPUT_FILE_LIST` | :term:`MTD_OUTPUT_TEMPLATE` | :term:`MTD_CONFIG_FILE` | :term:`MTD_MIN_VOLUME` @@ -4876,8 +4880,8 @@ METplus Configuration | :term:`PB2NC_TIME_SUMMARY_END` | :term:`PB2NC_TIME_SUMMARY_VAR_NAMES` | :term:`PB2NC_TIME_SUMMARY_TYPES` -| :term:`PB2NC_WINDOW_BEGIN` -| :term:`PB2NC_WINDOW_END` +| :term:`PB2NC_OBS_WINDOW_BEGIN` +| :term:`PB2NC_OBS_WINDOW_END` | :term:`PB2NC_VALID_BEGIN` | :term:`PB2NC_VALID_END` | :term:`PB2NC_CUSTOM_LOOP_LIST` @@ -4958,9 +4962,9 @@ see :ref:`How METplus controls MET config file settings`. * - METplus Config(s) - MET Config File - * - :term:`PB2NC_WINDOW_BEGIN` + * - :term:`PB2NC_OBS_WINDOW_BEGIN` - obs_window.beg - * - :term:`PB2NC_WINDOW_END` + * - :term:`PB2NC_OBS_WINDOW_END` - obs_window.end **${METPLUS_MASK_DICT}** @@ -5936,10 +5940,15 @@ METplus Configuration | :term:`SERIES_ANALYSIS_MET_CONFIG_OVERRIDES` | :term:`FCST_SERIES_ANALYSIS_INPUT_DIR` | :term:`OBS_SERIES_ANALYSIS_INPUT_DIR` +| :term:`BOTH_SERIES_ANALYSIS_INPUT_DIR` | :term:`SERIES_ANALYSIS_TC_STAT_INPUT_DIR` | :term:`SERIES_ANALYSIS_OUTPUT_DIR` | :term:`FCST_SERIES_ANALYSIS_INPUT_TEMPLATE` | :term:`OBS_SERIES_ANALYSIS_INPUT_TEMPLATE` +| :term:`BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE` +| :term:`FCST_SERIES_ANALYSIS_INPUT_FILE_LIST` +| :term:`OBS_SERIES_ANALYSIS_INPUT_FILE_LIST` +| :term:`BOTH_SERIES_ANALYSIS_INPUT_FILE_LIST` | :term:`SERIES_ANALYSIS_TC_STAT_INPUT_TEMPLATE` | :term:`SERIES_ANALYSIS_OUTPUT_TEMPLATE` | :term:`SERIES_ANALYSIS_CLIMO_MEAN_FILE_NAME` @@ -7894,6 +7903,7 @@ METplus Configuration | :term:`TC_RMW_OUTPUT_DIR` | :term:`TC_RMW_DECK_TEMPLATE` | :term:`TC_RMW_INPUT_TEMPLATE` +| :term:`TC_RMW_INPUT_FILE_LIST` | :term:`TC_RMW_OUTPUT_TEMPLATE` | :term:`LOG_TC_RMW_VERBOSITY` | :term:`TC_RMW_CONFIG_FILE` diff --git a/internal_tests/pytests/series_analysis/test_series_analysis.py b/internal_tests/pytests/series_analysis/test_series_analysis.py index f8ec19e35f..eb80c34ee9 100644 --- a/internal_tests/pytests/series_analysis/test_series_analysis.py +++ b/internal_tests/pytests/series_analysis/test_series_analysis.py @@ -692,9 +692,9 @@ def test_get_all_files_and_subset(metplus_config, time_info, expect_fcst_subset, ]), ] ) -def test_create_ascii_storm_files_list(metplus_config, config_overrides, - time_info, storm_id, lead_group, - expect_fcst_subset, expect_obs_subset): +def test_get_fcst_and_obs_path(metplus_config, config_overrides, + time_info, storm_id, lead_group, + expect_fcst_subset, expect_obs_subset): all_config_overrides = { 'LOOP_BY': 'INIT', 'SERIES_ANALYSIS_RUNTIME_FREQ': 'RUN_ONCE', @@ -757,9 +757,9 @@ def test_create_ascii_storm_files_list(metplus_config, config_overrides, if os.path.exists(obs_file_path): os.remove(obs_file_path) - fcst_path, obs_path = wrapper._create_ascii_storm_files_list(time_info, - storm_id, - lead_group) + fcst_path, obs_path = wrapper._get_fcst_and_obs_path(time_info, + storm_id, + lead_group) assert(fcst_path == fcst_file_path and obs_path == obs_file_path) with open(fcst_file_path, 'r') as file_handle: diff --git a/metplus/wrappers/command_builder.py b/metplus/wrappers/command_builder.py index 46ebd54b35..61e151c790 100755 --- a/metplus/wrappers/command_builder.py +++ b/metplus/wrappers/command_builder.py @@ -831,11 +831,6 @@ def find_input_files_ensemble(self, time_info): @param time_info dictionary containing timing information @returns True on success """ - # get list of ensemble files to process - input_files = self.find_model(time_info, return_list=True) - if not input_files: - self.log_error("Could not find any input files") - return False # get control file if requested if self.c_dict.get('CTRL_INPUT_TEMPLATE'): @@ -847,6 +842,28 @@ def find_input_files_ensemble(self, time_info): self.args.append(f'-ctrl {ctrl_file}') + # if explicit file list file is specified, use it and + # bypass logic to error check model files + if self.c_dict.get('FCST_INPUT_FILE_LIST'): + file_list_path = do_string_sub(self.c_dict['FCST_INPUT_FILE_LIST'], + **time_info) + self.logger.debug(f"Explicit file list file: {file_list_path}") + if not os.path.exists(file_list_path): + self.log_error("Could not find file list file") + return False + + self.infiles.append(file_list_path) + return True + + # get list of ensemble files to process + input_files = self.find_model(time_info, return_list=True) + if not input_files: + self.log_error("Could not find any input files") + return False + + # if control file is requested, remove it from input list + if self.c_dict.get('CTRL_INPUT_TEMPLATE'): + # check if control file is found in ensemble list if ctrl_file in input_files: # warn and remove control file if found diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index 453946a3b5..a2f3165346 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -172,14 +172,14 @@ def create_c_dict(self): self.config.getdir('OBS_ENSEMBLE_STAT_POINT_INPUT_DIR', '') c_dict['OBS_POINT_INPUT_TEMPLATE'] = \ - self.config.getraw('filename_templates', + self.config.getraw('config', 'OBS_ENSEMBLE_STAT_POINT_INPUT_TEMPLATE') c_dict['OBS_GRID_INPUT_DIR'] = \ self.config.getdir('OBS_ENSEMBLE_STAT_GRID_INPUT_DIR', '') c_dict['OBS_GRID_INPUT_TEMPLATE'] = \ - self.config.getraw('filename_templates', + self.config.getraw('config', 'OBS_ENSEMBLE_STAT_GRID_INPUT_TEMPLATE') # The ensemble forecast files input directory and filename templates @@ -189,8 +189,13 @@ def create_c_dict(self): c_dict['FCST_INPUT_TEMPLATE'] = ( self.config.getraw('config', 'FCST_ENSEMBLE_STAT_INPUT_TEMPLATE') ) - if not c_dict['FCST_INPUT_TEMPLATE']: - self.log_error("Must set FCST_ENSEMBLE_STAT_INPUT_TEMPLATE") + c_dict['FCST_INPUT_FILE_LIST'] = ( + self.config.getraw('config', 'FCST_ENSEMBLE_STAT_INPUT_FILE_LIST') + ) + if (not c_dict['FCST_INPUT_TEMPLATE'] and + not c_dict['FCST_INPUT_FILE_LIST']): + self.log_error("Must set FCST_ENSEMBLE_STAT_INPUT_TEMPLATE or " + "FCST_ENSEMBLE_STAT_INPUT_FILE_LIST") c_dict['OUTPUT_DIR'] = self.config.getdir('ENSEMBLE_STAT_OUTPUT_DIR', '') @@ -475,84 +480,6 @@ def get_all_field_info(self, var_list, data_type): return ','.join(field_list) - - def find_model_members(self, time_info): - """! Finds the model member files to compare - Args: - @param time_info dictionary containing timing information - @rtype string - @return Returns a list of the paths to the ensemble model files - """ - model_dir = self.c_dict['FCST_INPUT_DIR'] - # used for filling in missing files to ensure ens_thresh check is accurate - fake_dir = '/ensemble/member/is/missing' - - # model_template is a list of 1 or more. - ens_members_path = [] - - # get all files that exist - for ens_member_template in self.c_dict['FCST_INPUT_TEMPLATE']: - member_file = do_string_sub(ens_member_template, - **time_info) - expected_path = os.path.join(model_dir, member_file) - - # if wildcard expression, get all files that match - if '?' in expected_path or '*' in expected_path: - wildcard_files = sorted(glob.glob(expected_path)) - self.logger.debug('Ensemble members file pattern: {}' - .format(expected_path)) - self.logger.debug('{} members match file pattern' - .format(str(len(wildcard_files)))) - - # add files to list of ensemble members - for wildcard_file in wildcard_files: - ens_members_path.append(wildcard_file) - else: - # otherwise check if file exists - expected_path = util.preprocess_file(expected_path, - self.c_dict['FCST_INPUT_DATATYPE'], - self.config) - - # if the file exists, add it to the list - if expected_path: - ens_members_path.append(expected_path) - else: - # add relative path to fake dir and add to list - ens_members_path.append(os.path.join(fake_dir, member_file)) - self.logger.warning('Expected ensemble file {} not found' - .format(member_file)) - - # if more files found than expected, error and exit - if len(ens_members_path) > self.c_dict['N_MEMBERS']: - msg = 'Found more files than expected! ' +\ - 'Found {} expected {}. '.format(len(ens_members_path), - self.c_dict['N_MEMBERS']) +\ - 'Adjust wildcard expression in [filename_templates] '+\ - 'FCST_ENSEMBLE_STAT_INPUT_TEMPLATE or adjust [config] '+\ - 'ENSEMBLE_STAT_N_MEMBERS. Files found: {}'.format(ens_members_path) - self.log_error(msg) - self.logger.error("Could not file files in {} for init {} f{} " - .format(model_dir, time_info['init_fmt'], - str(time_info['lead_hours']))) - return False - # if fewer files found than expected, warn and add fake files - elif len(ens_members_path) < self.c_dict['N_MEMBERS']: - msg = 'Found fewer files than expected. '+\ - 'Found {} expected {}.'.format(len(ens_members_path), - self.c_dict['N_MEMBERS']) - self.logger.warning(msg) - # add fake files to list to get correct number of files for ens_thresh - diff = self.c_dict['N_MEMBERS'] - len(ens_members_path) - self.logger.warning('Adding {} fake files to '.format(str(diff))+\ - 'ensure ens_thresh check is accurate') - for _ in range(0, diff, 1): - ens_members_path.append(fake_dir) - - # write file that contains list of ensemble files - list_filename = time_info['init_fmt'] + '_' + \ - str(time_info['lead_hours']) + '_ensemble.txt' - return self.write_list_file(list_filename, ens_members_path) - def set_environment_variables(self, time_info): self.add_env_var("MET_OBS_ERROR_TABLE", self.c_dict.get('MET_OBS_ERR_TABLE', '')) diff --git a/metplus/wrappers/gen_ens_prod_wrapper.py b/metplus/wrappers/gen_ens_prod_wrapper.py index aeb6e706fd..d4877cf847 100755 --- a/metplus/wrappers/gen_ens_prod_wrapper.py +++ b/metplus/wrappers/gen_ens_prod_wrapper.py @@ -73,8 +73,14 @@ def create_c_dict(self): c_dict['FCST_INPUT_DIR'] = self.config.getdir('GEN_ENS_PROD_INPUT_DIR', '') - if not c_dict['FCST_INPUT_TEMPLATE']: - self.log_error('GEN_ENS_PROD_INPUT_TEMPLATE must be set') + c_dict['FCST_INPUT_FILE_LIST'] = ( + self.config.getraw('config', 'GEN_ENS_PROD_INPUT_FILE_LIST') + ) + + if (not c_dict['FCST_INPUT_TEMPLATE'] and + not c_dict['FCST_INPUT_FILE_LIST']): + self.log_error('GEN_ENS_PROD_INPUT_TEMPLATE or ' + 'GEN_ENS_PROD_INPUT_FILE_LIST must be set') # not all input files are mandatory to be found c_dict['MANDATORY'] = False diff --git a/metplus/wrappers/mtd_wrapper.py b/metplus/wrappers/mtd_wrapper.py index beb5eddd5c..e970e13c9f 100755 --- a/metplus/wrappers/mtd_wrapper.py +++ b/metplus/wrappers/mtd_wrapper.py @@ -100,6 +100,17 @@ def create_c_dict(self): 'OBS_MTD_INPUT_TEMPLATE') ) + c_dict['FCST_FILE_LIST'] = ( + self.config.getraw('config', + 'FCST_MTD_INPUT_FILE_LIST') + ) + c_dict['OBS_FILE_LIST'] = ( + self.config.getraw('config', + 'OBS_MTD_INPUT_FILE_LIST') + ) + if c_dict['FCST_FILE_LIST'] or c_dict['OBS_FILE_LIST']: + c_dict['EXPLICIT_FILE_LIST'] = True + c_dict['FCST_IS_PROB'] = ( self.config.getbool('config', 'FCST_IS_PROB', @@ -197,10 +208,6 @@ def run_at_time_loop_string(self, input_dict): Args: @param input_dict dictionary containing timing information """ -# max_lookback = self.c_dict['MAX_LOOKBACK'] -# file_interval = self.c_dict['FILE_INTERVAL'] - lead_seq = util.get_lead_sequence(self.config, input_dict) - var_list = util.sub_var_list(self.c_dict['VAR_LIST_TEMP'], input_dict) @@ -222,9 +229,36 @@ def run_at_time_loop_string(self, input_dict): for var_info in var_list: + if self.c_dict.get('EXPLICIT_FILE_LIST', False): + time_info = time_util.ti_calculate(input_dict) + model_list_path = do_string_sub(self.c_dict['FCST_FILE_LIST'], + **time_info) + self.logger.debug(f"Explicit FCST file: {model_list_path}") + if not os.path.exists(model_list_path): + self.log_error('FCST file list file does not exist: ' + f'{model_list_path}') + return None + + obs_list_path = do_string_sub(self.c_dict['OBS_FILE_LIST'], + **time_info) + self.logger.debug(f"Explicit OBS file: {obs_list_path}") + if not os.path.exists(obs_list_path): + self.log_error('OBS file list file does not exist: ' + f'{obs_list_path}') + return None + + arg_dict = {'obs_path': obs_list_path, + 'model_path': model_list_path} + + self.process_fields_one_thresh(time_info, var_info, **arg_dict) + continue + model_list = [] obs_list = [] + # find files for each forecast lead time + lead_seq = util.get_lead_sequence(self.config, input_dict) + tasks = [] for lead in lead_seq: input_dict['lead'] = lead @@ -288,36 +322,51 @@ def run_single_mode(self, input_dict, var_info): single_list = [] data_src = self.c_dict.get('SINGLE_DATA_SRC') - if data_src == 'OBS': - find_method = self.find_obs + + if self.c_dict.get('EXPLICIT_FILE_LIST', False): + time_info = time_util.ti_calculate(input_dict) + single_list_path = do_string_sub( + self.c_dict[f'{data_src}_FILE_LIST'], + **time_info + ) + self.logger.debug(f"Explicit file list: {single_list_path}") + if not os.path.exists(single_list_path): + self.log_error(f'{data_src} file list file does not exist: ' + f'{single_list_path}') + return None + else: - find_method = self.find_model + if data_src == 'OBS': + find_method = self.find_obs + else: + find_method = self.find_model - lead_seq = util.get_lead_sequence(self.config, input_dict) - for lead in lead_seq: - input_dict['lead'] = lead - current_task = time_util.ti_calculate(input_dict) + lead_seq = util.get_lead_sequence(self.config, input_dict) + for lead in lead_seq: + input_dict['lead'] = lead + current_task = time_util.ti_calculate(input_dict) - single_file = find_method(current_task, var_info) - if single_file is None: - continue + single_file = find_method(current_task, var_info) + if single_file is None: + continue - single_list.append(single_file) + single_list.append(single_file) - if len(single_list) == 0: - return + if len(single_list) == 0: + return - # write ascii file with list of files to process - input_dict['lead'] = lead_seq[0] - time_info = time_util.ti_calculate(input_dict) - file_ext = self.check_for_python_embedding(data_src, var_info) - if not file_ext: - return + # write ascii file with list of files to process + input_dict['lead'] = lead_seq[0] + time_info = time_util.ti_calculate(input_dict) + file_ext = self.check_for_python_embedding(data_src, var_info) + if not file_ext: + return - single_outfile = ( - f"{time_info['valid_fmt']}_mtd_single_{file_ext}.txt" - ) - single_list_path = self.write_list_file(single_outfile, single_list) + single_outfile = ( + f"{time_info['valid_fmt']}_mtd_single_{file_ext}.txt" + ) + single_list_path = self.write_list_file(single_outfile, + single_list) arg_dict = {} if data_src == 'OBS': diff --git a/metplus/wrappers/series_analysis_wrapper.py b/metplus/wrappers/series_analysis_wrapper.py index d4e48f95f9..372ab6001e 100755 --- a/metplus/wrappers/series_analysis_wrapper.py +++ b/metplus/wrappers/series_analysis_wrapper.py @@ -179,11 +179,20 @@ def create_c_dict(self): # get input dir, template, and datatype for FCST, OBS, and BOTH for data_type in ('FCST', 'OBS', 'BOTH'): + + # check if {data_type}_{app}_FILE_LIST is set + c_dict[f'{data_type}_INPUT_FILE_LIST'] = ( + self.config.getraw( + 'config', + f'{data_type}_SERIES_ANALYSIS_INPUT_FILE_LIST' + ) + ) + c_dict[f'{data_type}_INPUT_DIR'] = ( self.config.getdir(f'{data_type}_SERIES_ANALYSIS_INPUT_DIR', '') ) c_dict[f'{data_type}_INPUT_TEMPLATE'] = ( - self.config.getraw('filename_templates', + self.config.getraw('config', f'{data_type}_SERIES_ANALYSIS_INPUT_TEMPLATE', '') ) @@ -233,43 +242,61 @@ def create_c_dict(self): False) ) - # if BOTH is set, neither FCST or OBS can be set - c_dict['USING_BOTH'] = False - if c_dict['BOTH_INPUT_TEMPLATE']: - if c_dict['FCST_INPUT_TEMPLATE'] or c_dict['OBS_INPUT_TEMPLATE']: - self.log_error("Cannot set FCST_SERIES_ANALYSIS_INPUT_TEMPLATE" - " or OBS_SERIES_ANALYSIS_INPUT_TEMPLATE if " - "BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE is set.") + c_dict['USING_BOTH'] = (c_dict['BOTH_INPUT_TEMPLATE'] or + c_dict['BOTH_INPUT_FILE_LIST']) - c_dict['USING_BOTH'] = True + if c_dict['USING_BOTH']: - # set *_WINDOW_* variables for BOTH - # used in CommandBuilder.find_data function) - self.handle_file_window_variables(c_dict, dtypes=['BOTH']) + # check if using explicit file list for BOTH + if c_dict['BOTH_INPUT_FILE_LIST']: + c_dict['EXPLICIT_FILE_LIST'] = True + else: + # set *_WINDOW_* variables for BOTH + # used in CommandBuilder.find_data function) + self.handle_file_window_variables(c_dict, dtypes=['BOTH']) - prob_thresh = self.config.getstr('config','BOTH_SERIES_ANALYSIS_PROB_THRESH','') + prob_thresh = self.config.getraw( + 'config', + 'BOTH_SERIES_ANALYSIS_PROB_THRESH' + ) c_dict['FCST_PROB_THRESH'] = prob_thresh c_dict['OBS_PROB_THRESH'] = prob_thresh # if BOTH is not set, both FCST or OBS must be set else: - if (not c_dict['FCST_INPUT_TEMPLATE'] or - not c_dict['OBS_INPUT_TEMPLATE']): - self.log_error("Must either set " - "BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE or both " - "FCST_SERIES_ANALYSIS_INPUT_TEMPLATE and " - "OBS_SERIES_ANALYSIS_INPUT_TEMPLATE to run " - "SeriesAnalysis wrapper.") - - # set *_WINDOW_* variables for FCST and OBS - self.handle_file_window_variables(c_dict, dtypes=['FCST', 'OBS']) + fcst_input_list = c_dict['FCST_INPUT_FILE_LIST'] + obs_input_list = c_dict['OBS_INPUT_FILE_LIST'] + if fcst_input_list and obs_input_list: + c_dict['EXPLICIT_FILE_LIST'] = True + elif not fcst_input_list and not obs_input_list: + if (not c_dict['FCST_INPUT_TEMPLATE'] or + not c_dict['OBS_INPUT_TEMPLATE']): + self.log_error( + "Must either set " + "BOTH_SERIES_ANALYSIS_INPUT_TEMPLATE or both " + "FCST_SERIES_ANALYSIS_INPUT_TEMPLATE and " + "OBS_SERIES_ANALYSIS_INPUT_TEMPLATE to run " + "SeriesAnalysis wrapper." + ) + + # set *_WINDOW_* variables for FCST and OBS + self.handle_file_window_variables(c_dict, + dtypes=['FCST', 'OBS']) + # if fcst input list or obs input list are not set + else: + self.log_error('Cannot set ' + 'FCST_SERIES_ANALYSIS_INPUT_FILE_LIST ' + 'without OBS_SERIES_ANALYSIS_INPUT_FILE_LIST ' + 'and vice versa') c_dict['FCST_PROB_THRESH'] = ( - self.config.getstr('config','FCST_SERIES_ANALYSIS_PROB_THRESH','') + self.config.getraw('config', + 'FCST_SERIES_ANALYSIS_PROB_THRESH') ) c_dict['OBS_PROB_THRESH'] = ( - self.config.getstr('config','OBS_SERIES_ANALYSIS_PROB_THRESH','') + self.config.getraw('config', + 'OBS_SERIES_ANALYSIS_PROB_THRESH') ) c_dict['TC_STAT_INPUT_DIR'] = ( @@ -464,9 +491,9 @@ def run_at_time_once(self, time_info, lead_group=None): for storm_id in storm_list: # Create FCST and OBS ASCII files fcst_path, obs_path = ( - self._create_ascii_storm_files_list(time_info, - storm_id, - lead_group) + self._get_fcst_and_obs_path(time_info, + storm_id, + lead_group) ) if not fcst_path or not obs_path: self.log_error('No ASCII file lists were created. Skipping.') @@ -611,7 +638,7 @@ def compare_time_info(self, runtime, filetime): return bool(filetime['storm_id'] == runtime['storm_id']) - def _create_ascii_storm_files_list(self, time_info, storm_id, lead_group): + def _get_fcst_and_obs_path(self, time_info, storm_id, lead_group): """! Creates the list of ASCII files that contain the storm id and init times. The list is used to create an ASCII file which will be used as the option to the -obs or -fcst flag to the MET @@ -624,26 +651,62 @@ def _create_ascii_storm_files_list(self, time_info, storm_id, lead_group): key will match the format "NoLabel_" and if no lead groups are defined, the dictionary should be replaced with None """ + if not self._check_python_embedding(): + return None, None + time_info['storm_id'] = storm_id - all_fcst_files = [] - all_obs_files = [] - if not lead_group: - fcst_files, obs_files = self.subset_input_files(time_info) - if not fcst_files or not obs_files: - return None, None - all_fcst_files.extend(fcst_files) - all_obs_files.extend(obs_files) - label = '' - leads = None - else: + + # get label and lead list if grouping by forecast leads + if lead_group: label = lead_group[0] leads = lead_group[1] - for lead in leads: + else: + label = '' + leads = None + + # if file list are explicitly specified, + # return the file list file paths + if self.c_dict.get('EXPLICIT_FILE_LIST', False): + # set forecast lead to last lead in list to set in output filename + if leads: + time_info['lead'] = leads[-1] + if self.c_dict['USING_BOTH']: + both_path = do_string_sub(self.c_dict['BOTH_INPUT_FILE_LIST'], + **time_info) + self.logger.debug(f"Explicit BOTH file list file: {both_path}") + if not os.path.exists(both_path): + self.log_error(f'Could not find file: {both_path}') + return None, None + + return both_path, both_path + + fcst_path = do_string_sub(self.c_dict['FCST_INPUT_FILE_LIST'], + **time_info) + self.logger.debug(f"Explicit FCST file list file: {fcst_path}") + if not os.path.exists(fcst_path): + self.log_error(f'Could not find forecast file: {fcst_path}') + fcst_path = None + + obs_path = do_string_sub(self.c_dict['OBS_INPUT_FILE_LIST'], + **time_info) + self.logger.debug(f"Explicit OBS file list file: {obs_path}") + if not os.path.exists(obs_path): + self.log_error(f'Could not find observation file: {obs_path}') + obs_path = None + + return fcst_path, obs_path + + all_fcst_files = [] + all_obs_files = [] + lead_loop = leads if leads else [None] + for lead in lead_loop: + if lead is not None: time_info['lead'] = lead - fcst_files, obs_files = self.subset_input_files(time_info) - if fcst_files and obs_files: - all_fcst_files.extend(fcst_files) - all_obs_files.extend(obs_files) + + fcst_files, obs_files = self.subset_input_files(time_info) + if fcst_files and obs_files: + all_fcst_files.extend(fcst_files) + all_obs_files.extend(obs_files) # skip if no files were found if not all_fcst_files or not all_obs_files: @@ -651,22 +714,18 @@ def _create_ascii_storm_files_list(self, time_info, storm_id, lead_group): output_dir = self.get_output_dir(time_info, storm_id, label) - if not self._check_python_embedding(): - return None, None - # create forecast (or both) file list if self.c_dict['USING_BOTH']: data_type = 'BOTH' else: data_type = 'FCST' + fcst_ascii_filename = self._get_ascii_filename(data_type, storm_id, leads) - self.write_list_file(fcst_ascii_filename, - all_fcst_files, - output_dir=output_dir) - - fcst_path = os.path.join(output_dir, fcst_ascii_filename) + fcst_path = self.write_list_file(fcst_ascii_filename, + all_fcst_files, + output_dir=output_dir) if self.c_dict['USING_BOTH']: return fcst_path, fcst_path @@ -675,11 +734,9 @@ def _create_ascii_storm_files_list(self, time_info, storm_id, lead_group): obs_ascii_filename = self._get_ascii_filename('OBS', storm_id, leads) - self.write_list_file(obs_ascii_filename, - all_obs_files, - output_dir=output_dir) - - obs_path = os.path.join(output_dir, obs_ascii_filename) + obs_path = self.write_list_file(obs_ascii_filename, + all_obs_files, + output_dir=output_dir) return fcst_path, obs_path diff --git a/metplus/wrappers/tcrmw_wrapper.py b/metplus/wrappers/tcrmw_wrapper.py index 7c0e438d70..c7cc1a23a0 100755 --- a/metplus/wrappers/tcrmw_wrapper.py +++ b/metplus/wrappers/tcrmw_wrapper.py @@ -65,19 +65,22 @@ def create_c_dict(self): c_dict['CONFIG_FILE'] = self.get_config_file('TCRMWConfig_wrapped') c_dict['INPUT_DIR'] = self.config.getdir('TC_RMW_INPUT_DIR', '') - c_dict['INPUT_TEMPLATE'] = self.config.getraw('filename_templates', + c_dict['INPUT_TEMPLATE'] = self.config.getraw('config', 'TC_RMW_INPUT_TEMPLATE') + c_dict['INPUT_FILE_LIST'] = self.config.getraw( + 'config', 'TC_RMW_INPUT_FILE_LIST' + ) c_dict['OUTPUT_DIR'] = self.config.getdir('TC_RMW_OUTPUT_DIR', '') c_dict['OUTPUT_TEMPLATE'] = ( - self.config.getraw('filename_templates', + self.config.getraw('config', 'TC_RMW_OUTPUT_TEMPLATE') ) c_dict['DECK_INPUT_DIR'] = self.config.getdir('TC_RMW_DECK_INPUT_DIR', '') c_dict['DECK_INPUT_TEMPLATE'] = ( - self.config.getraw('filename_templates', + self.config.getraw('config', 'TC_RMW_DECK_TEMPLATE') ) @@ -291,13 +294,6 @@ def find_input_files(self, time_info): @param time_info time dictionary to use for string substitution @returns Input file list if all files were found, None if not. """ - - # tc_rmw currently doesn't support an ascii file that contains a list of input files - # setting this to False will list each file in the command, which can be difficult to read - # when the tool supports reading a file list file, we should use the logic when - # use_file_list = True - use_file_list = False - # get deck file deck_file = self.find_data(time_info, data_type='DECK') if not deck_file: @@ -305,32 +301,42 @@ def find_input_files(self, time_info): self.c_dict['DECK_FILE'] = deck_file - all_input_files = [] - lead_seq = util.get_lead_sequence(self.config, time_info) - for lead in lead_seq: - self.clear() - time_info['lead'] = lead - time_info = time_util.ti_calculate(time_info) + # get input files + if self.c_dict['INPUT_FILE_LIST']: + self.logger.debug("Explicit file list file: " + f"{self.c_dict['INPUT_FILE_LIST']}") + list_file = do_string_sub(self.c_dict['INPUT_FILE_LIST'], + **time_info) + if not os.path.exists(list_file): + self.log_error(f'Could not find file list: {list_file}') + return None + else: + all_input_files = [] - # get a list of the input data files, write to an ascii file if there are more than one - input_files = self.find_data(time_info, return_list=True) - if not input_files: - continue + for lead in lead_seq: + self.clear() + time_info['lead'] = lead - all_input_files.extend(input_files) + time_info = time_util.ti_calculate(time_info) - if not all_input_files: - return None + # get a list of the input data files, + # write to an ascii file if there are more than one + input_files = self.find_data(time_info, return_list=True) + if not input_files: + continue + + all_input_files.extend(input_files) + + if not all_input_files: + return None - if use_file_list: # create an ascii file with a list of the input files - list_file = self.write_list_file(f"{os.path.basename(adeck_file)}_data_files.txt", - all_input_files) - self.infiles.append(list_file) - else: - self.infiles.extend(all_input_files) + list_file = f"{os.path.basename(deck_file)}_data_files.txt" + list_file = self.write_list_file(list_file, all_input_files) + + self.infiles.append(list_file) # set LEAD_LIST to list of forecast leads used if lead_seq != [0]: diff --git a/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf b/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf index a919fc00b9..af9e283abc 100644 --- a/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf +++ b/parm/use_cases/met_tool_wrapper/PB2NC/PB2NC.conf @@ -23,8 +23,8 @@ PB2NC_OUTPUT_TEMPLATE = sample_pb.nc PB2NC_CONFIG_FILE = {PARM_BASE}/met_config/PB2NCConfig_wrapped -PB2NC_WINDOW_BEGIN = -1800 -PB2NC_WINDOW_END = 1800 +PB2NC_OBS_WINDOW_BEGIN = -1800 +PB2NC_OBS_WINDOW_END = 1800 PB2NC_VALID_BEGIN = {valid?fmt=%Y%m%d_%H} PB2NC_VALID_END = {valid?fmt=%Y%m%d_%H?shift=1d} diff --git a/parm/use_cases/model_applications/convection_allowing_models/EnsembleStat_fcstHRRRE_obsHRRRE_Sfc_MultiField.conf b/parm/use_cases/model_applications/convection_allowing_models/EnsembleStat_fcstHRRRE_obsHRRRE_Sfc_MultiField.conf index 3c3f135dc0..f3dc96a313 100644 --- a/parm/use_cases/model_applications/convection_allowing_models/EnsembleStat_fcstHRRRE_obsHRRRE_Sfc_MultiField.conf +++ b/parm/use_cases/model_applications/convection_allowing_models/EnsembleStat_fcstHRRRE_obsHRRRE_Sfc_MultiField.conf @@ -84,11 +84,11 @@ PB2NC_TIME_SUMMARY_TYPES = PB2NC_TIME_SUMMARY_VALID_FREQ = 0 PB2NC_TIME_SUMMARY_VALID_THRESH = 0.0 -PB2NC_WINDOW_BEGIN = -900 -PB2NC_WINDOW_END = 900 +PB2NC_OBS_WINDOW_BEGIN = -900 +PB2NC_OBS_WINDOW_END = 900 -OBS_ENSEMBLE_STAT_WINDOW_BEGIN = -900 -OBS_ENSEMBLE_STAT_WINDOW_END = 900 +ENSEMBLE_STAT_OBS_WINDOW_BEGIN = -900 +ENSEMBLE_STAT_OBS_WINDOW_END = 900 # number of expected members for ensemble. Should correspond with the # number of items in the list for FCST_ENSEMBLE_STAT_INPUT_TEMPLATE From 01b4c14fb4e8bf8d2982bd39a2410731c0eb70e4 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 7 Feb 2022 10:31:52 -0700 Subject: [PATCH 283/770] Per PR #1387 review, fix typo --- docs/Users_Guide/glossary.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/glossary.rst b/docs/Users_Guide/glossary.rst index 20085aa151..4fd656173c 100644 --- a/docs/Users_Guide/glossary.rst +++ b/docs/Users_Guide/glossary.rst @@ -8987,7 +8987,7 @@ METplus Configuration Glossary OBS_SERIES_ANALYSIS_INPUT_FILE_LIST Specifies an explicit path to a file list file to pass into - series_analysis with the -fcst argument. If set, + series_analysis with the -obs argument. If set, :term:`FCST_SERIES_ANALYSIS_INPUT_FILE_LIST` must also be set and :term:`OBS_SERIES_ANALYSIS_INPUT_TEMPLATE` and :term:`OBS_SERIES_ANALYSIS_INPUT_DIR` are ignored. From 4e1f9490481ffd23357a18ffb839d6a8e3ba339e Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 7 Feb 2022 15:23:03 -0700 Subject: [PATCH 284/770] Bugfix 1421 EnsembleStat use fcst fields in ens dictionary if ens is unset (#1422) --- .../GridStat_fcstHRRR-TLE_obsStgIV_GRIB.py | 2 +- .../test_ensemble_stat_wrapper.py | 86 +++++++++++++++++-- internal_tests/use_cases/all_use_cases.txt | 2 +- metplus/wrappers/ensemble_stat_wrapper.py | 4 + 4 files changed, 85 insertions(+), 9 deletions(-) diff --git a/docs/use_cases/model_applications/precipitation/GridStat_fcstHRRR-TLE_obsStgIV_GRIB.py b/docs/use_cases/model_applications/precipitation/GridStat_fcstHRRR-TLE_obsStgIV_GRIB.py index f009510eed..afca6623fb 100644 --- a/docs/use_cases/model_applications/precipitation/GridStat_fcstHRRR-TLE_obsStgIV_GRIB.py +++ b/docs/use_cases/model_applications/precipitation/GridStat_fcstHRRR-TLE_obsStgIV_GRIB.py @@ -2,7 +2,7 @@ Grid-Stat: 6hr PQPF Probability Verification ========================================================================== -model_applications/precipitation/GridStat_fcstHRRR +model_applications/precipitation/GridStat_fcstHRRR-TLE _obsStgIV_GRIB.conf """ diff --git a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py index 4a12ceef65..3575d59cc7 100644 --- a/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py +++ b/internal_tests/pytests/ensemble_stat/test_ensemble_stat_wrapper.py @@ -31,7 +31,7 @@ time_fmt = '%Y%m%d%H' run_times = ['2005080700', '2005080712'] -def set_minimum_config_settings(config): +def set_minimum_config_settings(config, set_fields=True): # set config variables to prevent command from running and bypass check # if input files actually exist config.set('config', 'DO_NOT_RUN_EXE', True) @@ -59,12 +59,84 @@ def set_minimum_config_settings(config): '{OUTPUT_BASE}/EnsembleStat/output') config.set('config', 'ENSEMBLE_STAT_OUTPUT_TEMPLATE', '{valid?fmt=%Y%m%d%H}') - config.set('config', 'FCST_VAR1_NAME', fcst_name) - config.set('config', 'FCST_VAR1_LEVELS', fcst_level) - config.set('config', 'OBS_VAR1_NAME', obs_name) - config.set('config', 'OBS_VAR1_LEVELS', obs_level) - config.set('config', 'ENS_VAR1_NAME', ens_name) - config.set('config', 'ENS_VAR1_LEVELS', ens_level) + if set_fields: + config.set('config', 'FCST_VAR1_NAME', fcst_name) + config.set('config', 'FCST_VAR1_LEVELS', fcst_level) + config.set('config', 'OBS_VAR1_NAME', obs_name) + config.set('config', 'OBS_VAR1_LEVELS', obs_level) + config.set('config', 'ENS_VAR1_NAME', ens_name) + config.set('config', 'ENS_VAR1_LEVELS', ens_level) + +@pytest.mark.parametrize( + 'config_overrides, env_var_values', [ + # 0 : 3 ens, 1 fcst, 1 obs + ({'ENS_VAR1_NAME': 'ens_name_1', + 'ENS_VAR1_LEVELS': 'ENS_LEVEL_1', + 'ENS_VAR2_NAME': 'ens_name_2', + 'ENS_VAR2_LEVELS': 'ENS_LEVEL_2A, ENS_LEVEL_2B', + 'FCST_VAR1_NAME': 'fcst_name_1', + 'FCST_VAR1_LEVELS': 'FCST_LEVEL_1', + 'OBS_VAR1_NAME': 'obs_name_1', + 'OBS_VAR1_LEVELS': 'OBS_LEVEL_1', + }, + {'METPLUS_ENS_FIELD': ('field = [' + '{ name="ens_name_1"; level="ENS_LEVEL_1"; },' + '{ name="ens_name_2"; level="ENS_LEVEL_2A"; },' + '{ name="ens_name_2"; level="ENS_LEVEL_2B"; }' + '];'), + 'METPLUS_FCST_FIELD': ('field = [' + '{ name="fcst_name_1"; level="FCST_LEVEL_1"; }' + '];'), + 'METPLUS_OBS_FIELD': ('field = [' + '{ name="obs_name_1"; level="OBS_LEVEL_1"; }' + '];'), + }), + # 1 : no ens, 1 fcst, 1 obs -- use fcst for ens + ({'FCST_VAR1_NAME': 'fcst_name_1', + 'FCST_VAR1_LEVELS': 'FCST_LEVEL_1', + 'OBS_VAR1_NAME': 'obs_name_1', + 'OBS_VAR1_LEVELS': 'OBS_LEVEL_1', + }, + {'METPLUS_ENS_FIELD': ('field = [' + '{ name="fcst_name_1"; level="FCST_LEVEL_1"; }' + '];'), + 'METPLUS_FCST_FIELD': ('field = [' + '{ name="fcst_name_1"; level="FCST_LEVEL_1"; }' + '];'), + 'METPLUS_OBS_FIELD': ('field = [' + '{ name="obs_name_1"; level="OBS_LEVEL_1"; }' + '];'), + }), + ] +) +def test_ensemble_stat_field_info(metplus_config, config_overrides, + env_var_values): + + config = metplus_config() + + set_minimum_config_settings(config, set_fields=False) + + # set config variable overrides + for key, value in config_overrides.items(): + config.set('config', key, value) + + wrapper = EnsembleStatWrapper(config) + assert wrapper.isOK + + all_cmds = wrapper.run_all_times() + + assert len(all_cmds) == 2 + + actual_env_vars = all_cmds[0][1] + for key, expected_value in env_var_values.items(): + match = next((item for item in actual_env_vars if + item.startswith(key)), None) + assert match is not None + actual_value = match.split('=', 1)[1] + assert actual_value == expected_value + print(f"ACTUAL : {actual_value}") + print(f"EXPECTED: {expected_value}") + @pytest.mark.parametrize( 'config_overrides, env_var_values', [ diff --git a/internal_tests/use_cases/all_use_cases.txt b/internal_tests/use_cases/all_use_cases.txt index 051b1e961e..576ac6fbff 100644 --- a/internal_tests/use_cases/all_use_cases.txt +++ b/internal_tests/use_cases/all_use_cases.txt @@ -115,7 +115,7 @@ Category: precipitation 4::GridStat_fcstHRRR-TLE_obsStgIV_GRIB:: model_applications/precipitation/GridStat_fcstHRRR-TLE_obsStgIV_GRIB.conf 5::MTD_fcstHRRR-TLE_FcstOnly_RevisionSeries_GRIB:: model_applications/precipitation/MTD_fcstHRRR-TLE_FcstOnly_RevisionSeries_GRIB.conf 6::MTD_fcstHRRR-TLE_obsMRMS:: model_applications/precipitation/MTD_fcstHRRR-TLE_obsMRMS.conf - +7::EnsembleStat_fcstWOFS_obsWOFS:: model_applications/precipitation/EnsembleStat_fcstWOFS_obsWOFS.conf:: Category: s2s 0::GridStat_SeriesAnalysis_fcstNMME_obsCPC_seasonal_forecast:: model_applications/s2s/GridStat_SeriesAnalysis_fcstNMME_obsCPC_seasonal_forecast.conf:: netcdf4_env diff --git a/metplus/wrappers/ensemble_stat_wrapper.py b/metplus/wrappers/ensemble_stat_wrapper.py index a2f3165346..99680e5db3 100755 --- a/metplus/wrappers/ensemble_stat_wrapper.py +++ b/metplus/wrappers/ensemble_stat_wrapper.py @@ -431,6 +431,10 @@ def run_at_time_all_fields(self, time_info): self.log_error("Could not build field info for fcst, obs, or ens") return + # if ens is not set, use fcst + if not ens_field: + ens_field = fcst_field + self.format_field('FCST', fcst_field) self.format_field('OBS', obs_field) self.format_field('ENS', ens_field) From 1651be3daffc67a8b1b3ca2bfbe9cd310b644dd0 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 8 Feb 2022 09:59:53 -0700 Subject: [PATCH 285/770] fixed missing path change for docker file path --- .github/jobs/docker_update_data_volumes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/jobs/docker_update_data_volumes.py b/.github/jobs/docker_update_data_volumes.py index de14dbbc29..d19d731e4c 100755 --- a/.github/jobs/docker_update_data_volumes.py +++ b/.github/jobs/docker_update_data_volumes.py @@ -22,7 +22,7 @@ # path to script that builds docker data volumes BUILD_DOCKER_IMAGES = os.path.join(os.environ.get('GITHUB_WORKSPACE', ''), - 'ci', + 'scripts', 'docker', 'docker_data', 'build_docker_images.sh') From 9d2b5062abe4f243a67ed49b031e7449487f74c7 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 8 Feb 2022 12:24:24 -0700 Subject: [PATCH 286/770] removed variables that are not used --- ...ointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf | 4 ---- .../PointStat_fcstGFS_obsNAM_Sfc_MultiField_PrepBufr.conf | 4 ---- 2 files changed, 8 deletions(-) diff --git a/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf b/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf index 058cd73a15..ca2f810ca5 100644 --- a/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf +++ b/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsGDAS_UpperAir_MultiField_PrepBufr.conf @@ -45,10 +45,6 @@ POINT_STAT_INTERP_TYPE_WIDTH = 2 POINT_STAT_OUTPUT_FLAG_SL1L2 = STAT POINT_STAT_OUTPUT_FLAG_VL1L2 = STAT - -# Either conus_sfc or upper_air -PB2NC_VERTICAL_LOCATION = upper_air - # # PB2NC # diff --git a/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsNAM_Sfc_MultiField_PrepBufr.conf b/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsNAM_Sfc_MultiField_PrepBufr.conf index 2d9b0a1ca3..bf86801b45 100644 --- a/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsNAM_Sfc_MultiField_PrepBufr.conf +++ b/parm/use_cases/model_applications/medium_range/PointStat_fcstGFS_obsNAM_Sfc_MultiField_PrepBufr.conf @@ -46,10 +46,6 @@ POINT_STAT_INTERP_TYPE_WIDTH = 2 POINT_STAT_OUTPUT_FLAG_SL1L2 = STAT POINT_STAT_OUTPUT_FLAG_VL1L2 = STAT - -# Either conus_sfc or upper_air -PB2NC_VERTICAL_LOCATION = conus_sfc - # # PB2NC # From 9605006a576d58ee14edbd1048e27f9109113f60 Mon Sep 17 00:00:00 2001 From: j-opatz <59586397+j-opatz@users.noreply.github.com> Date: Tue, 8 Feb 2022 16:33:37 -0700 Subject: [PATCH 287/770] Feature 1408 use case ptype (#1425) * adding imagery, config file, updating internal tests * updated lists, auto testing * corrected old reference * Update use_case_groups.json --- .github/parm/use_case_groups.json | 7 +- ...at_fcstMULTI_obsMETAR_PtypeComparisons.png | Bin 0 -> 258336 bytes ...tat_fcstMULTI_obsMETAR_PtypeComparisons.py | 160 ++++++++++++++++++ internal_tests/use_cases/all_use_cases.txt | 2 + ...t_fcstMULTI_obsMETAR_PtypeComparisons.conf | 133 +++++++++++++++ 5 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 docs/_static/precipitation-PointStat_fcstMULTI_obsMETAR_PtypeComparisons.png create mode 100644 docs/use_cases/model_applications/precipitation/PointStat_fcstMULTI_obsMETAR_PtypeComparisons.py create mode 100644 parm/use_cases/model_applications/precipitation/PointStat_fcstMULTI_obsMETAR_PtypeComparisons.conf diff --git a/.github/parm/use_case_groups.json b/.github/parm/use_case_groups.json index 85959b3fb9..119576e300 100644 --- a/.github/parm/use_case_groups.json +++ b/.github/parm/use_case_groups.json @@ -111,7 +111,12 @@ }, { "category": "precipitation", - "index_list": "3-8", + "index_list": "3-7", + "run": false + }, + { + "category": "precipitation", + "index_list": "8", "run": false }, { diff --git a/docs/_static/precipitation-PointStat_fcstMULTI_obsMETAR_PtypeComparisons.png b/docs/_static/precipitation-PointStat_fcstMULTI_obsMETAR_PtypeComparisons.png new file mode 100644 index 0000000000000000000000000000000000000000..3e8d337938afef51d9daec5fc957f05d1c933afe GIT binary patch literal 258336 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