|
7 | 7 | """ |
8 | 8 |
|
9 | 9 | from collections import OrderedDict |
| 10 | +from pathlib import Path |
10 | 11 | from typing import Dict, List, Optional, Tuple, Union |
11 | 12 |
|
| 13 | +import iris |
| 14 | +import joblib |
| 15 | +import pandas as pd |
12 | 16 | from iris.cube import Cube, CubeList |
13 | 17 |
|
14 | 18 | from improver.metadata.probabilistic import ( |
@@ -51,6 +55,7 @@ def __init__(self): |
51 | 55 | ("altitude", pa.float32()), |
52 | 56 | ("time", pa.timestamp("s", "utc")), |
53 | 57 | ("wmo_id", pa.string()), |
| 58 | + ("station_id", pa.string()), |
54 | 59 | ("ob_value", pa.float32()), |
55 | 60 | ] |
56 | 61 | ) |
@@ -263,6 +268,85 @@ def split_forecasts_and_bias_files(cubes: CubeList) -> Tuple[Cube, Optional[Cube |
263 | 268 | return forecast_cube, bias_cubes |
264 | 269 |
|
265 | 270 |
|
| 271 | +def split_pickle_parquet_and_netcdf(files): |
| 272 | + """Split the input files into pickle, parquet, and netcdf files. |
| 273 | + Only a single pickle file is expected. |
| 274 | +
|
| 275 | + Args: |
| 276 | + files: |
| 277 | + A list of input file paths which will be split into pickle, |
| 278 | + parquet, and netcdf files. |
| 279 | + Returns: |
| 280 | + - A flattened cube list containing all the cubes contained within the |
| 281 | + provided paths to NetCDF files. |
| 282 | + - A list of paths to Parquet files. |
| 283 | + - A loaded pickle file. |
| 284 | + Raises: |
| 285 | + ValueError: If multiple pickle files provided, as only one is ever expected. |
| 286 | + """ |
| 287 | + cubes = iris.cube.CubeList() |
| 288 | + loaded_pickles = [] |
| 289 | + parquets = [] |
| 290 | + |
| 291 | + for file_path in files: |
| 292 | + if not file_path.exists(): |
| 293 | + continue |
| 294 | + |
| 295 | + # Directories indicate we are working with parquet files. |
| 296 | + if file_path.is_dir(): |
| 297 | + parquets.append(file_path) |
| 298 | + continue |
| 299 | + |
| 300 | + try: |
| 301 | + cube = iris.load(file_path) |
| 302 | + cubes.extend(cube) |
| 303 | + except ValueError: |
| 304 | + try: |
| 305 | + loaded_pickles.append(joblib.load(file_path)) |
| 306 | + except Exception as e: |
| 307 | + msg = f"Failed to load {file_path}: {e}" |
| 308 | + raise ValueError(msg) |
| 309 | + |
| 310 | + if len(loaded_pickles) > 1: |
| 311 | + msg = "Multiple pickle inputs have been provided. Only one is expected." |
| 312 | + raise ValueError(msg) |
| 313 | + |
| 314 | + return ( |
| 315 | + cubes if cubes else None, |
| 316 | + parquets if parquets else None, |
| 317 | + loaded_pickles[0] if loaded_pickles else None, |
| 318 | + ) |
| 319 | + |
| 320 | + |
| 321 | +def identify_parquet_type(parquet_paths: List[Path]): |
| 322 | + """Determine whether the provided parquet paths contain forecast or truth data. |
| 323 | + This is done by checking the columns within the parquet files for the presence |
| 324 | + of a forecast_period column which is only present for forecast data. |
| 325 | + Args: |
| 326 | + parquet_paths: |
| 327 | + A list of paths to Parquet files. |
| 328 | + Returns: |
| 329 | + - The path to the Parquet file containing the historical forecasts. |
| 330 | + - The path to the Parquet file containing the truths. |
| 331 | + """ |
| 332 | + import pyarrow.parquet as pq |
| 333 | + |
| 334 | + forecast_table_path = None |
| 335 | + truth_table_path = None |
| 336 | + for file_path in parquet_paths: |
| 337 | + try: |
| 338 | + example_file_path = next(file_path.glob("**/*.parquet")) |
| 339 | + except StopIteration: |
| 340 | + continue |
| 341 | + try: |
| 342 | + pq.read_schema(example_file_path).field("forecast_period") |
| 343 | + forecast_table_path = file_path |
| 344 | + except KeyError: |
| 345 | + truth_table_path = file_path |
| 346 | + |
| 347 | + return forecast_table_path, truth_table_path |
| 348 | + |
| 349 | + |
266 | 350 | def validity_time_check(forecast: Cube, validity_times: List[str]) -> bool: |
267 | 351 | """Check the validity time of the forecast matches the accepted validity times |
268 | 352 | within the validity times list. |
@@ -307,3 +391,25 @@ def add_warning_comment(forecast: Cube) -> Cube: |
307 | 391 | "however, no calibration has been applied." |
308 | 392 | ) |
309 | 393 | return forecast |
| 394 | + |
| 395 | + |
| 396 | +def get_training_period_cycles( |
| 397 | + cycletime: str, forecast_period: Union[int, str], training_length: int |
| 398 | +): |
| 399 | + """Generate a list of forecast reference times for the training period. |
| 400 | +
|
| 401 | + Args: |
| 402 | + cycletime: The time at which the forecast is issued in a format understood by |
| 403 | + pandas.Timestamp e.g. 20170109T0000Z. |
| 404 | + forecast_period: The forecast period in seconds. |
| 405 | + training_length: The number of days in the training period. |
| 406 | + """ |
| 407 | + forecast_period_td = pd.Timedelta(int(forecast_period), unit="seconds") |
| 408 | + |
| 409 | + return pd.date_range( |
| 410 | + end=pd.Timestamp(cycletime) |
| 411 | + - pd.Timedelta(1, unit="days") |
| 412 | + - forecast_period_td.floor("D"), |
| 413 | + periods=int(training_length), |
| 414 | + freq="D", |
| 415 | + ) |
0 commit comments