diff --git a/README.md b/README.md index 25281cff..4b4986aa 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,10 @@ heatwave_evaluation_list = [ ), ] # Load in the EWB default list of event cases -case_metadata = ewb.cases.load_ewb_events_yaml_into_case_list() +case_metadata = ewb.cases.load_cases() # Create the evaluation class, with cases and evaluation objects declared -ewb_instance = ewb.evaluation( +ewb_instance = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_metadata, evaluation_objects=heatwave_evaluation_list, ) diff --git a/data_prep/ibtracs_bounds.py b/data_prep/ibtracs_bounds.py index 0dc962d9..0eb98f73 100644 --- a/data_prep/ibtracs_bounds.py +++ b/data_prep/ibtracs_bounds.py @@ -541,7 +541,7 @@ def update_cases_with_storm_bounds(storm_bounds, all_storms_df): """ logger.info("Updating cases with storm bounds...") - cases_all = ewb.cases.load_ewb_events_yaml_into_case_list() + cases_all = ewb.cases.load_cases() cases_new = cases_all.copy() # Update the yaml cases with storm bounds from IBTrACS data diff --git a/docs/examples/applied_ar.py b/docs/examples/applied_ar.py index 8bd82177..cc325feb 100644 --- a/docs/examples/applied_ar.py +++ b/docs/examples/applied_ar.py @@ -9,11 +9,11 @@ # Load case data from the default events.yaml # Users can also define their own cases_dict structure -case_yaml = ewb.load_cases() +case_yaml = ewb.cases.load_cases() # ERA5 target -era5_target = ewb.targets.ERA5( +era5_target = ewb.inputs.ERA5( variables=[ ewb.derived.AtmosphericRiverVariables( output_variables=["atmospheric_river_land_intersection"] @@ -22,7 +22,7 @@ ) # Forecast (HRES) -hres_forecast = ewb.forecasts.ZarrForecast( +hres_forecast = ewb.inputs.ZarrForecast( source="gs://weatherbench2/datasets/hres/2016-2022-0012-1440x721.zarr", name="HRES", variables=[ @@ -30,7 +30,7 @@ output_variables=["atmospheric_river_land_intersection"] ) ], - variable_mapping=ewb.HRES_metadata_variable_mapping, + variable_mapping=ewb.inputs.HRES_metadata_variable_mapping, ) grap_forecast = ewb.inputs.get_cira_icechunk( @@ -56,7 +56,7 @@ ) # Create a list of evaluation objects for atmospheric river ar_evaluation_objects = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="atmospheric_river", metric_list=[ ewb.metrics.CriticalSuccessIndex(), @@ -66,7 +66,7 @@ target=era5_target, forecast=hres_forecast, ), - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="atmospheric_river", metric_list=[ ewb.metrics.CriticalSuccessIndex(), @@ -76,7 +76,7 @@ target=era5_target, forecast=grap_forecast, ), - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="atmospheric_river", metric_list=[ ewb.metrics.CriticalSuccessIndex(), @@ -91,7 +91,7 @@ if __name__ == "__main__": # Initialize ExtremeWeatherBench; will only run on cases with event_type # atmospheric_river - ar_ewb = ewb.evaluation( + ar_ewb = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_yaml, evaluation_objects=ar_evaluation_objects, ) diff --git a/docs/examples/applied_freeze.py b/docs/examples/applied_freeze.py index 99d75708..13b33ece 100644 --- a/docs/examples/applied_freeze.py +++ b/docs/examples/applied_freeze.py @@ -9,23 +9,23 @@ # Load case data from the default events.yaml # Users can also define their own cases_dict structure -case_yaml = ewb.load_cases() +case_yaml = ewb.cases.load_cases() # ERA5 target -era5_freeze_target = ewb.targets.ERA5( +era5_freeze_target = ewb.inputs.ERA5( variables=["surface_air_temperature"], chunks=None, ) # GHCN target -ghcn_freeze_target = ewb.targets.GHCN(variables=["surface_air_temperature"]) +ghcn_freeze_target = ewb.inputs.GHCN(variables=["surface_air_temperature"]) # Forecast (FCNv2) using helper in defaults fcnv2_forecast = ewb.defaults.cira_fcnv2_freeze_forecast # Load the climatology for DurationMeanError -climatology = ewb.get_climatology(quantile=0.15) +climatology = ewb.defaults.get_climatology(quantile=0.15) # Define the metrics metrics_list = [ @@ -36,13 +36,13 @@ # Create a list of evaluation objects for freeze freeze_evaluation_object = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="freeze", metric_list=metrics_list, target=ghcn_freeze_target, forecast=fcnv2_forecast, ), - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="freeze", metric_list=metrics_list, target=era5_freeze_target, @@ -52,7 +52,7 @@ if __name__ == "__main__": # Initialize ExtremeWeatherBench runner instance - freeze_ewb = ewb.evaluation( + freeze_ewb = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_yaml, evaluation_objects=freeze_evaluation_object, ) diff --git a/docs/examples/applied_heatwave.py b/docs/examples/applied_heatwave.py index c1ca31ca..44bad68d 100644 --- a/docs/examples/applied_heatwave.py +++ b/docs/examples/applied_heatwave.py @@ -9,31 +9,31 @@ # Load case data from the default events.yaml # Users can also define their own cases_dict structure -case_yaml = ewb.load_cases() +case_yaml = ewb.cases.load_cases() # Define targets # ERA5 target -era5_heatwave_target = ewb.targets.ERA5( +era5_heatwave_target = ewb.inputs.ERA5( variables=["surface_air_temperature"], chunks=None, ) # GHCN target -ghcn_heatwave_target = ewb.targets.GHCN( +ghcn_heatwave_target = ewb.inputs.GHCN( variables=["surface_air_temperature"], ) # Define forecast (HRES) -hres_forecast = ewb.forecasts.ZarrForecast( +hres_forecast = ewb.inputs.ZarrForecast( name="hres_forecast", source="gs://weatherbench2/datasets/hres/2016-2022-0012-1440x721.zarr", variables=["surface_air_temperature"], - variable_mapping=ewb.HRES_metadata_variable_mapping, + variable_mapping=ewb.inputs.HRES_metadata_variable_mapping, preprocess=ewb.defaults.preprocess_heatwave_forecast_dataset, ) # Load the climatology for DurationMeanError -climatology = ewb.get_climatology(quantile=0.85) +climatology = ewb.defaults.get_climatology(quantile=0.85) # Define the metrics metrics_list = [ @@ -45,13 +45,13 @@ # Create a list of evaluation objects for heatwave heatwave_evaluation_object = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="heat_wave", metric_list=metrics_list, target=ghcn_heatwave_target, forecast=hres_forecast, ), - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="heat_wave", metric_list=metrics_list, target=era5_heatwave_target, @@ -60,7 +60,7 @@ ] if __name__ == "__main__": # Initialize ExtremeWeatherBench - heatwave_ewb = ewb.evaluation( + heatwave_ewb = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_yaml, evaluation_objects=heatwave_evaluation_object, ) diff --git a/docs/examples/applied_severe.py b/docs/examples/applied_severe.py index 4f4b1f66..cacbd24c 100644 --- a/docs/examples/applied_severe.py +++ b/docs/examples/applied_severe.py @@ -8,25 +8,25 @@ # Load case data from the default events.yaml -case_yaml = ewb.load_cases() +case_yaml = ewb.cases.load_cases() # Subset to one severe convection case for example case_list = [n for n in case_yaml if n.case_id_number == 305] # Define PPH target -pph_target = ewb.targets.PPH( +pph_target = ewb.inputs.PPH( variables=["practically_perfect_hindcast"], ) # Define LSR target -lsr_target = ewb.targets.LSR() +lsr_target = ewb.inputs.LSR() # Define HRES forecast -hres_forecast = ewb.forecasts.ZarrForecast( +hres_forecast = ewb.inputs.ZarrForecast( name="hres_forecast", source="gs://weatherbench2/datasets/hres/2016-2022-0012-1440x721.zarr", variables=[ewb.derived.CravenBrooksSignificantSevere(layer_depth=100)], - variable_mapping=ewb.HRES_metadata_variable_mapping, + variable_mapping=ewb.inputs.HRES_metadata_variable_mapping, storage_options={"remote_options": {"anon": True}}, ) @@ -58,7 +58,7 @@ # Define evaluation objects for severe convection: # One evaluation object for PPH pph_evaluation_objects = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="severe_convection", metric_list=pph_metrics, target=pph_target, @@ -68,7 +68,7 @@ # One evaluation object for LSR lsr_evaluation_objects = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="severe_convection", metric_list=lsr_metrics, target=lsr_target, @@ -78,7 +78,7 @@ if __name__ == "__main__": # Initialize ExtremeWeatherBench with both evaluation objects - severe_ewb = ewb.evaluation( + severe_ewb = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_list, evaluation_objects=lsr_evaluation_objects + pph_evaluation_objects, ) diff --git a/docs/examples/applied_tc.py b/docs/examples/applied_tc.py index a803630d..3482d695 100644 --- a/docs/examples/applied_tc.py +++ b/docs/examples/applied_tc.py @@ -8,22 +8,22 @@ # Load the case collection from the YAML file -case_yaml = ewb.load_cases() +case_yaml = ewb.cases.load_cases() # Select single case (TC Ida) case_list = [n for n in case_yaml if n.case_id_number == 220] # Define IBTrACS target, no arguments needed as defaults are sufficient -ibtracs_target = ewb.targets.IBTrACS() +ibtracs_target = ewb.inputs.IBTrACS() # # Define HRES forecast -hres_forecast = ewb.forecasts.ZarrForecast( +hres_forecast = ewb.inputs.ZarrForecast( name="hres_forecast", source="gs://weatherbench2/datasets/hres/2016-2022-0012-1440x721.zarr", # Define tropical cyclone track derived variable to include in the forecast variables=[ewb.derived.TropicalCycloneTrackVariables()], # Define metadata variable mapping for HRES forecast - variable_mapping=ewb.HRES_metadata_variable_mapping, + variable_mapping=ewb.inputs.HRES_metadata_variable_mapping, storage_options={"remote_options": {"anon": True}}, # Preprocess the HRES forecast to include geopotential thickness calculation preprocess=ewb.defaults.preprocess_hres_tc_forecast_dataset, @@ -63,21 +63,21 @@ # the relevant cases inside the events YAML file tc_evaluation_object = [ # HRES forecast - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="tropical_cyclone", metric_list=composite_landfall_metrics, target=ibtracs_target, forecast=hres_forecast, ), # Pangu forecast - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="tropical_cyclone", metric_list=composite_landfall_metrics, target=ibtracs_target, forecast=pangu_forecast, ), # FCNv2 forecast - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="tropical_cyclone", metric_list=composite_landfall_metrics, target=ibtracs_target, @@ -87,7 +87,7 @@ if __name__ == "__main__": # Initialize ExtremeWeatherBench - tc_ewb = ewb.evaluation( + tc_ewb = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_list, evaluation_objects=tc_evaluation_object, ) diff --git a/docs/examples/example_config.py b/docs/examples/example_config.py index f41d5e44..338d784e 100644 --- a/docs/examples/example_config.py +++ b/docs/examples/example_config.py @@ -10,22 +10,22 @@ import extremeweatherbench as ewb # Define targets (observation data) -era5_heatwave_target = ewb.targets.ERA5( +era5_heatwave_target = ewb.inputs.ERA5( variables=["surface_air_temperature"], chunks=None, ) # Define forecasts -fcnv2_forecast = ewb.forecasts.KerchunkForecast( +fcnv2_forecast = ewb.inputs.KerchunkForecast( name="fcnv2_forecast", source="gs://extremeweatherbench/FOUR_v200_GFS.parq", variables=["surface_air_temperature"], - variable_mapping=ewb.CIRA_metadata_variable_mapping, + variable_mapping=ewb.inputs.CIRA_metadata_variable_mapping, ) # Define evaluation objects evaluation_objects = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="heat_wave", metric_list=[ ewb.metrics.MaximumMeanAbsoluteError(), @@ -38,7 +38,7 @@ # Load case data from the default events.yaml # Users can also define their own cases_dict structure -cases_list = ewb.load_cases() +cases_list = ewb.cases.load_cases() # Alternatively, users could define custom cases like this: # cases_list = [ diff --git a/docs/usage.md b/docs/usage.md index 4e58f8e3..edfc8a70 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -12,10 +12,10 @@ tropical cyclones, and atmospheric rivers: ```python import extremeweatherbench as ewb -eval_objects = ewb.get_brightband_evaluation_objects() -cases = ewb.load_cases() +eval_objects = ewb.defaults.get_brightband_evaluation_objects() +cases = ewb.cases.load_cases() -runner = ewb.evaluation( +runner = ewb.evaluate.ExtremeWeatherBench( case_metadata=cases, evaluation_objects=eval_objects ) @@ -32,26 +32,41 @@ ewb --default ## API Overview -ExtremeWeatherBench provides a hierarchical API for accessing its components: +ExtremeWeatherBench provides a submodule-based API. All classes and functions +are accessed through their submodule: ```python import extremeweatherbench as ewb # Main evaluation entry point -ewb.evaluation(...) # Alias for ExtremeWeatherBench class - -# Hierarchical access via namespaces -ewb.targets.ERA5(...) # Target classes -ewb.forecasts.ZarrForecast(...) # Forecast classes -ewb.metrics.MeanAbsoluteError() # Metric classes -ewb.derived.AtmosphericRiverVariables() # Derived variables -ewb.regions.BoundingBoxRegion(...) # Region classes -ewb.cases.IndividualCase # Case metadata classes - -# Also available at top level for convenience -ewb.ERA5(...) -ewb.ZarrForecast(...) -ewb.load_cases() +ewb.evaluate.ExtremeWeatherBench(...) + +# Inputs: targets and forecasts +ewb.inputs.ERA5(...) +ewb.inputs.GHCN(...) +ewb.inputs.IBTrACS() +ewb.inputs.ZarrForecast(...) +ewb.inputs.KerchunkForecast(...) +ewb.inputs.EvaluationObject(...) + +# Metrics +ewb.metrics.MeanAbsoluteError() +ewb.metrics.MaximumMeanAbsoluteError() + +# Derived variables +ewb.derived.AtmosphericRiverVariables() +ewb.derived.TropicalCycloneTrackVariables() + +# Regions +ewb.regions.BoundingBoxRegion(...) + +# Cases +ewb.cases.IndividualCase +ewb.cases.load_cases() + +# Defaults (pre-built targets, forecasts, and helpers) +ewb.defaults.era5_heatwave_target +ewb.defaults.get_climatology(quantile=0.85) ``` ## Running an Evaluation for a Single Event Type @@ -69,11 +84,11 @@ import extremeweatherbench as ewb There are three built-in `ForecastBase` classes to set up a forecast: `ZarrForecast`, `XarrayForecast`, and `KerchunkForecast`. Here is an example of a `ZarrForecast`, using Weatherbench2's HRES zarr store: ```python -hres_forecast = ewb.forecasts.ZarrForecast( +hres_forecast = ewb.inputs.ZarrForecast( source="gs://weatherbench2/datasets/hres/2016-2022-0012-1440x721.zarr", name="HRES", variables=["surface_air_temperature"], - variable_mapping=ewb.HRES_metadata_variable_mapping, # built-in mapping available + variable_mapping=ewb.inputs.HRES_metadata_variable_mapping, # built-in mapping storage_options={"remote_options": {"anon": True}}, ) ``` @@ -92,8 +107,8 @@ There are required arguments, namely: Next, a target dataset must be defined as well to evaluate against. For this evaluation, we'll use ERA5: ```python -era5_heatwave_target = ewb.targets.ERA5( - source=ewb.ARCO_ERA5_FULL_URI, +era5_heatwave_target = ewb.inputs.ERA5( + source=ewb.inputs.ARCO_ERA5_FULL_URI, variables=["surface_air_temperature"], storage_options={"remote_options": {"anon": True}}, chunks=None, @@ -118,7 +133,7 @@ We then set up an `EvaluationObject` list: ```python heatwave_evaluation_list = [ - ewb.EvaluationObject( + ewb.inputs.EvaluationObject( event_type="heat_wave", metric_list=[ ewb.metrics.MaximumMeanAbsoluteError( @@ -146,9 +161,9 @@ There can be multiple `EvaluationObjects` which are used for an evaluation run. Plugging these all in: ```python -case_yaml = ewb.load_cases() +case_yaml = ewb.cases.load_cases() -ewb_instance = ewb.evaluation( +ewb_instance = ewb.evaluate.ExtremeWeatherBench( case_metadata=case_yaml, evaluation_objects=heatwave_evaluation_list, ) @@ -157,17 +172,25 @@ outputs = ewb_instance.run_evaluation() outputs.to_csv('your_file_name.csv') ``` -Where the EWB default events YAML file is loaded in using `ewb.load_cases()`, then applied to an instance of `ewb.evaluation` along with the `EvaluationObject` list. Finally, we run the evaluation with the `.run_evaluation()` method, where defaults are typically sufficient to run with a small to moderate-sized virtual machine. +Where the EWB default events YAML file is loaded in using +`ewb.cases.load_cases()`, then applied to an instance +of `ewb.evaluate.ExtremeWeatherBench` along with the `EvaluationObject` list. +Finally, we run the evaluation with the `.run_evaluation()` method, where defaults are +typically sufficient to run with a small to moderate-sized virtual machine. Running locally is feasible but is typically bottlenecked heavily by IO and network bandwidth. Even on a gigabit connection, the rate of data access is significantly slower compared to within a cloud provider VM. The outputs are returned as a pandas DataFrame and can be manipulated in the script, a notebook, etc. -## Backward Compatibility +## Import Patterns -All existing import patterns remain functional: +All of the following import styles work: ```python -from extremeweatherbench import evaluate, inputs, cases, metrics # Still works -from extremeweatherbench.evaluate import ExtremeWeatherBench # Still works +import extremeweatherbench as ewb +ewb.inputs.ERA5(...) + +from extremeweatherbench import inputs, metrics, cases, evaluate +from extremeweatherbench.inputs import ERA5 +from extremeweatherbench.evaluate import ExtremeWeatherBench ``` diff --git a/pyproject.toml b/pyproject.toml index a8ae0ae8..84f459ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ dependencies = [ "icechunk>=1.1.14", "dask[complete]>=2025.1.0", "distributed>=2025.1.0", + "lazy-loader>=0.4", ] [project.optional-dependencies] diff --git a/src/extremeweatherbench/__init__.py b/src/extremeweatherbench/__init__.py index af479c2f..e114f79c 100644 --- a/src/extremeweatherbench/__init__.py +++ b/src/extremeweatherbench/__init__.py @@ -1,339 +1,31 @@ """ExtremeWeatherBench: A benchmarking framework for extreme weather forecasts. -This module provides the public API for ExtremeWeatherBench. Users can import -the package and access all key functionality: +import extremeweatherbench as ewb - import extremeweatherbench as ewb - - # Main entry point for evaluation - ewb.evaluation(case_metadata=..., evaluation_objects=...) - - # Hierarchical access via namespace submodules - ewb.targets.ERA5(...) - ewb.forecasts.ZarrForecast(...) - ewb.metrics.MeanAbsoluteError(...) - - # Also available at top level - ewb.ERA5(...) - ewb.load_cases() +ewb.evaluate.ExtremeWeatherBench(case_metadata=..., evaluation_objects=...) +ewb.inputs.ERA5(...) +ewb.inputs.ZarrForecast(...) +ewb.metrics.MeanAbsoluteError(...) +ewb.cases.load_cases() """ -from types import SimpleNamespace +from importlib.metadata import version -# Import actual modules for backwards compatibility -from extremeweatherbench import calc, cases, defaults, derived, metrics, regions, utils +import lazy_loader as lazy -# Import specific items for top-level access -from extremeweatherbench.calc import ( - convert_from_cartesian_to_latlon, - geopotential_thickness, - great_circle_mask, - haversine_distance, - maybe_calculate_wind_speed, - mixing_ratio, - orography, - pressure_at_surface, - saturation_mixing_ratio, - saturation_vapor_pressure, - specific_humidity_from_relative_humidity, -) -from extremeweatherbench.cases import ( - CaseOperator, - IndividualCase, - build_case_operators, - load_ewb_events_yaml_into_case_list, - load_individual_cases, - load_individual_cases_from_yaml, - read_incoming_yaml, -) -from extremeweatherbench.defaults import ( - DEFAULT_COORDINATE_VARIABLES, - DEFAULT_VARIABLE_NAMES, - cira_fcnv2_atmospheric_river_forecast, - cira_fcnv2_freeze_forecast, - cira_fcnv2_heatwave_forecast, - cira_fcnv2_severe_convection_forecast, - cira_fcnv2_tropical_cyclone_forecast, - era5_atmospheric_river_target, - era5_freeze_target, - era5_heatwave_target, - get_brightband_evaluation_objects, - get_climatology, - ghcn_freeze_target, - ghcn_heatwave_target, - ibtracs_target, - lsr_target, - pph_target, -) -from extremeweatherbench.derived import ( - AtmosphericRiverVariables, - CravenBrooksSignificantSevere, - DerivedVariable, - TropicalCycloneTrackVariables, - maybe_derive_variables, - maybe_include_variables_from_derived_input, -) -from extremeweatherbench.evaluate import ExtremeWeatherBench -from extremeweatherbench.inputs import ( - ARCO_ERA5_FULL_URI, - DEFAULT_GHCN_URI, - ERA5, - GHCN, - IBTRACS_URI, - LSR, - LSR_URI, - PPH, - PPH_URI, - CIRA_metadata_variable_mapping, - ERA5_metadata_variable_mapping, - EvaluationObject, - ForecastBase, - HRES_metadata_variable_mapping, - IBTrACS, - IBTrACS_metadata_variable_mapping, - InputBase, - KerchunkForecast, - TargetBase, - XarrayForecast, - ZarrForecast, - align_forecast_to_target, - check_for_missing_data, - maybe_subset_variables, - open_kerchunk_reference, - zarr_target_subsetter, -) -from extremeweatherbench.metrics import ( - Accuracy, - BaseMetric, - CompositeMetric, - CriticalSuccessIndex, - DurationMeanError, - EarlySignal, - FalseAlarmRatio, - FalseNegatives, - FalsePositives, - LandfallDisplacement, - LandfallIntensityMeanAbsoluteError, - LandfallMetric, - LandfallTimeMeanError, - MaximumLowestMeanAbsoluteError, - MaximumMeanAbsoluteError, - MeanAbsoluteError, - MeanError, - MeanSquaredError, - MinimumMeanAbsoluteError, - RootMeanSquaredError, - SpatialDisplacement, - ThresholdMetric, - TrueNegatives, - TruePositives, -) -from extremeweatherbench.regions import ( - REGION_TYPES, - BoundingBoxRegion, - CenteredRegion, - Region, - RegionSubsetter, - ShapefileRegion, - map_to_create_region, - subset_cases_to_region, - subset_results_to_region, -) -from extremeweatherbench.utils import ( - check_for_vars, - convert_day_yearofday_to_time, - convert_init_time_to_valid_time, - convert_longitude_to_180, - convert_longitude_to_360, - convert_valid_time_to_init_time, - derive_indices_from_init_time_and_lead_time, - determine_temporal_resolution, - extract_tc_names, - filter_kwargs_for_callable, - find_common_init_times, - idx_to_coords, - interp_climatology_to_target, - is_valid_landfall, - load_land_geometry, - maybe_cache_and_compute, - maybe_densify_dataarray, - maybe_get_closest_timestamp_to_center_of_valid_times, - maybe_get_operator, - min_if_all_timesteps_present, - min_if_all_timesteps_present_forecast, - read_event_yaml, - remove_ocean_gridpoints, - stack_dataarray_from_dims, -) - -# Aliases -evaluation = ExtremeWeatherBench -load_cases = load_ewb_events_yaml_into_case_list +__version__ = version("extremeweatherbench") -# Namespace submodules for convenient grouping (these don't shadow actual modules) -targets = SimpleNamespace( - TargetBase=TargetBase, - ERA5=ERA5, - GHCN=GHCN, - IBTrACS=IBTrACS, - LSR=LSR, - PPH=PPH, +__getattr__, __dir__, __all__ = lazy.attach( + __name__, + submodules=[ + "calc", + "cases", + "defaults", + "derived", + "evaluate", + "inputs", + "metrics", + "regions", + "utils", + ], ) - -forecasts = SimpleNamespace( - ForecastBase=ForecastBase, - ZarrForecast=ZarrForecast, - KerchunkForecast=KerchunkForecast, - XarrayForecast=XarrayForecast, -) - -__all__ = [ - # Core evaluation - "evaluation", - "ExtremeWeatherBench", - # Modules - "calc", - "cases", - "defaults", - "derived", - "metrics", - "regions", - "utils", - # Namespace submodules - "targets", - "forecasts", - # Aliases - "load_cases", - # calc - "convert_from_cartesian_to_latlon", - "geopotential_thickness", - "great_circle_mask", - "haversine_distance", - "maybe_calculate_wind_speed", - "mixing_ratio", - "orography", - "pressure_at_surface", - "saturation_mixing_ratio", - "saturation_vapor_pressure", - "specific_humidity_from_relative_humidity", - # cases - "CaseOperator", - "IndividualCase", - "build_case_operators", - "load_ewb_events_yaml_into_case_list", - "load_individual_cases", - "load_individual_cases_from_yaml", - "read_incoming_yaml", - # defaults - "DEFAULT_COORDINATE_VARIABLES", - "DEFAULT_VARIABLE_NAMES", - "cira_fcnv2_atmospheric_river_forecast", - "cira_fcnv2_freeze_forecast", - "cira_fcnv2_heatwave_forecast", - "cira_fcnv2_severe_convection_forecast", - "cira_fcnv2_tropical_cyclone_forecast", - "era5_atmospheric_river_target", - "era5_freeze_target", - "era5_heatwave_target", - "get_brightband_evaluation_objects", - "get_climatology", - "ghcn_freeze_target", - "ghcn_heatwave_target", - "ibtracs_target", - "lsr_target", - "pph_target", - # derived - "AtmosphericRiverVariables", - "CravenBrooksSignificantSevere", - "DerivedVariable", - "TropicalCycloneTrackVariables", - "maybe_derive_variables", - "maybe_include_variables_from_derived_input", - # inputs - "ARCO_ERA5_FULL_URI", - "CIRA_metadata_variable_mapping", - "DEFAULT_GHCN_URI", - "ERA5", - "ERA5_metadata_variable_mapping", - "EvaluationObject", - "ForecastBase", - "GHCN", - "HRES_metadata_variable_mapping", - "IBTrACS", - "IBTrACS_metadata_variable_mapping", - "IBTRACS_URI", - "InputBase", - "KerchunkForecast", - "LSR", - "LSR_URI", - "PPH", - "PPH_URI", - "TargetBase", - "XarrayForecast", - "ZarrForecast", - "align_forecast_to_target", - "check_for_missing_data", - "maybe_subset_variables", - "open_kerchunk_reference", - "zarr_target_subsetter", - # metrics - "Accuracy", - "BaseMetric", - "CompositeMetric", - "CriticalSuccessIndex", - "DurationMeanError", - "EarlySignal", - "FalseAlarmRatio", - "FalseNegatives", - "FalsePositives", - "LandfallDisplacement", - "LandfallIntensityMeanAbsoluteError", - "LandfallMetric", - "LandfallTimeMeanError", - "MaximumLowestMeanAbsoluteError", - "MaximumMeanAbsoluteError", - "MeanAbsoluteError", - "MeanError", - "MeanSquaredError", - "MinimumMeanAbsoluteError", - "RootMeanSquaredError", - "SpatialDisplacement", - "ThresholdMetric", - "TrueNegatives", - "TruePositives", - # regions - "BoundingBoxRegion", - "CenteredRegion", - "REGION_TYPES", - "Region", - "RegionSubsetter", - "ShapefileRegion", - "map_to_create_region", - "subset_cases_to_region", - "subset_results_to_region", - # utils - "check_for_vars", - "convert_day_yearofday_to_time", - "convert_init_time_to_valid_time", - "convert_longitude_to_180", - "convert_longitude_to_360", - "convert_valid_time_to_init_time", - "derive_indices_from_init_time_and_lead_time", - "determine_temporal_resolution", - "extract_tc_names", - "filter_kwargs_for_callable", - "find_common_init_times", - "idx_to_coords", - "interp_climatology_to_target", - "is_valid_landfall", - "load_land_geometry", - "maybe_cache_and_compute", - "maybe_densify_dataarray", - "maybe_get_closest_timestamp_to_center_of_valid_times", - "maybe_get_operator", - "min_if_all_timesteps_present", - "min_if_all_timesteps_present_forecast", - "read_event_yaml", - "remove_ocean_gridpoints", - "stack_dataarray_from_dims", -] diff --git a/src/extremeweatherbench/cases.py b/src/extremeweatherbench/cases.py index 236104c9..646086fc 100644 --- a/src/extremeweatherbench/cases.py +++ b/src/extremeweatherbench/cases.py @@ -164,6 +164,9 @@ def load_individual_cases_from_yaml( def load_ewb_events_yaml_into_case_list() -> list[IndividualCase]: """Loads the EWB events yaml file into a list of IndividualCase objects.""" + logger.warning( + "load_ewb_events_yaml_into_case_list is deprecated. Use load_ewb_events_yaml_into_case_list instead." + ) import extremeweatherbench.data events_yaml_file = importlib.resources.files(extremeweatherbench.data).joinpath( @@ -175,6 +178,9 @@ def load_ewb_events_yaml_into_case_list() -> list[IndividualCase]: return load_individual_cases(yaml_event_case) +load_cases = load_ewb_events_yaml_into_case_list + + def read_incoming_yaml(input_pth: Union[str, pathlib.Path]): """Read events yaml from data into a dictionary. diff --git a/tests/test_init.py b/tests/test_init.py index c06d4f80..e6f007ba 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -147,11 +147,11 @@ def test_utils_public_functions(self): class TestTopLevelImports: - """Test that top-level imports work for commonly used items.""" + """Test that submodule-path imports work for commonly used items.""" def test_top_level_metric_imports(self): - """Test that metrics can be imported at top level.""" - from extremeweatherbench import ( + """Test that metrics are accessible via the metrics submodule.""" + from extremeweatherbench.metrics import ( MeanAbsoluteError, MeanError, MeanSquaredError, @@ -164,8 +164,8 @@ def test_top_level_metric_imports(self): assert RootMeanSquaredError is not None def test_top_level_input_imports(self): - """Test that input classes can be imported at top level.""" - from extremeweatherbench import ERA5, GHCN, IBTrACS, ZarrForecast + """Test that input classes are accessible via the inputs submodule.""" + from extremeweatherbench.inputs import ERA5, GHCN, IBTrACS, ZarrForecast assert ERA5 is not None assert GHCN is not None @@ -173,57 +173,69 @@ def test_top_level_input_imports(self): assert ZarrForecast is not None def test_top_level_region_imports(self): - """Test that region classes can be imported at top level.""" - from extremeweatherbench import BoundingBoxRegion, CenteredRegion, Region + """Test that region classes are accessible via the regions submodule.""" + from extremeweatherbench.regions import ( + BoundingBoxRegion, + CenteredRegion, + Region, + ) assert Region is not None assert BoundingBoxRegion is not None assert CenteredRegion is not None def test_top_level_case_imports(self): - """Test that case classes can be imported at top level.""" - from extremeweatherbench import CaseOperator, IndividualCase + """Test that case classes are accessible via the cases submodule.""" + from extremeweatherbench.cases import CaseOperator, IndividualCase assert IndividualCase is not None assert CaseOperator is not None - def test_evaluation_alias(self): - """Test that evaluation alias works.""" - from extremeweatherbench import ExtremeWeatherBench, evaluation + def test_evaluation_class(self): + """Test that ExtremeWeatherBench is accessible via the evaluate submodule.""" + from extremeweatherbench.evaluate import ExtremeWeatherBench - assert evaluation is ExtremeWeatherBench + assert ExtremeWeatherBench is not None - def test_load_cases_alias(self): - """Test that load_cases alias works.""" - from extremeweatherbench import ( - load_cases, - load_ewb_events_yaml_into_case_list, - ) + def test_load_cases(self): + """Test that load_cases is accessible via the cases submodule.""" + from extremeweatherbench.cases import load_ewb_events_yaml_into_case_list + + assert load_ewb_events_yaml_into_case_list is not None + + +class TestSubmoduleAccess: + """Test the dot-notation submodule access pattern.""" - assert load_cases is load_ewb_events_yaml_into_case_list + def test_inputs_submodule_contains_era5(self): + """Test ERA5 is accessible via ewb.inputs.""" + import extremeweatherbench as ewb + assert hasattr(ewb.inputs, "ERA5") + assert hasattr(ewb.inputs, "GHCN") + assert hasattr(ewb.inputs, "IBTrACS") + assert hasattr(ewb.inputs, "TargetBase") -class TestNamespaceSubmodules: - """Test the convenience namespace submodules.""" + def test_inputs_submodule_contains_forecasts(self): + """Test forecast classes are accessible via ewb.inputs.""" + import extremeweatherbench as ewb - def test_targets_namespace(self): - """Test targets SimpleNamespace contains expected items.""" - from extremeweatherbench import targets + assert hasattr(ewb.inputs, "ZarrForecast") + assert hasattr(ewb.inputs, "KerchunkForecast") + assert hasattr(ewb.inputs, "ForecastBase") - assert isinstance(targets, types.SimpleNamespace) - assert hasattr(targets, "ERA5") - assert hasattr(targets, "GHCN") - assert hasattr(targets, "IBTrACS") - assert hasattr(targets, "TargetBase") + def test_metrics_submodule_contains_metrics(self): + """Test metrics are accessible via ewb.metrics.""" + import extremeweatherbench as ewb - def test_forecasts_namespace(self): - """Test forecasts SimpleNamespace contains expected items.""" - from extremeweatherbench import forecasts + assert hasattr(ewb.metrics, "MeanAbsoluteError") + assert hasattr(ewb.metrics, "RootMeanSquaredError") + + def test_evaluate_submodule_contains_main_class(self): + """Test ExtremeWeatherBench is accessible via ewb.evaluate.""" + import extremeweatherbench as ewb - assert isinstance(forecasts, types.SimpleNamespace) - assert hasattr(forecasts, "ZarrForecast") - assert hasattr(forecasts, "KerchunkForecast") - assert hasattr(forecasts, "ForecastBase") + assert hasattr(ewb.evaluate, "ExtremeWeatherBench") class TestMockPatching: