diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b72dcb..718d77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added optional `output_filename_format` to config.yaml to configure the output file name [!30](https://github.com/dmidk/sunflow/pull/30), @KristianHMoller +- Added `INPUT_FILENAME_FORMAT` and `OUTPUT_FILENAME_FORMAT` environment variables, which take priority over the corresponding config.yaml values [!30](https://github.com/dmidk/sunflow/pull/30), @KristianHMoller +- Added the dataset used for the run (e.g. `KNMI_MTG`) as the `satellite_data_source` global attribute in the output file [!30](https://github.com/dmidk/sunflow/pull/30), @KristianHMoller - Added configuration for KNMI_MTG [!26](https://github.com/dmidk/sunflow/pull/26), @KristianHMoller - Added `satellite` as a required field in config.yaml to be printed in the outpt file [!26](https://github.com/dmidk/sunflow/pull/26), @KristianHMoller +### Changed +- Renamed `filename_format` to `input_filename_format` in config.yaml. It has no default and must be set in config.yaml or via `INPUT_FILENAME_FORMAT` [!30](https://github.com/dmidk/sunflow/pull/30), @KristianHMoller +- Changed the default output filename to include the dataset name and an ISO 8601-compliant datetime (`SolarNowcast_{dataset_name}_{iso8601_compact_timestamp}.nc`, e.g. `SolarNowcast_DWD_2026-03-09T1200Z.nc`). KNMI_MSG and KNMI_MTG keep their own `output_filename_format` in config.yaml [!30](https://github.com/dmidk/sunflow/pull/30), @KristianHMoller + ## [v1.3.0] ### Added diff --git a/README.md b/README.md index 239db59..d5f2864 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ podman run -it --rm --entrypoint="" sunflow bash | `MAX_CLEARSKY_FALLBACK_DAYS` | `3` | Days back to search for fallback clear-sky data | | `MIN_SOLAR_ELEVATION_DEGREES` | `6` | Minimum maximum-corner solar elevation required to run | | `ENSEMBLE_STATISTICS` | `median,mean,p10,p25,p75,p90` | Comma-separated list of statistics for ensemble output. Allowed: `median`, `mean`, `p10`, `p25`, `p75`, `p90` (aliases `10th_percentile`, `25th_percentile`, `75th_percentile`, `90th_percentile` are accepted). | +| `INPUT_FILENAME_FORMAT` | None (required) | Template for input data filenames, e.g. `NetCDF4_sds_{pds_timestamp}.nc`. Must be set here or as `input_filename_format` in `config.yaml`; the environment variable takes priority. See `config.yaml` for the available template variables. | +| `OUTPUT_FILENAME_FORMAT` | `SolarNowcast_{dataset_name}_{iso8601_compact_timestamp}.nc` | Template for forecast output filenames. Can also be set as `output_filename_format` in `config.yaml`; the environment variable takes priority. | #### Data Source Configuration @@ -130,7 +132,7 @@ podman run -it --rm --entrypoint="" sunflow bash ### Volume Mounts -- `/data/nowcasts` - Forecast output files (NetCDF format) +- `/data/nowcasts` - Forecast output files (NetCDF format), named `SolarNowcast__.nc` by default (configurable via `output_filename_format` in `config.yaml`) - `/data/satellite_data` - Input satellite data archive ### Arguments diff --git a/config.yaml b/config.yaml index 2f842aa..cdf65e0 100644 --- a/config.yaml +++ b/config.yaml @@ -9,21 +9,32 @@ KNMI_MSG: nc_variable_names: sds: sds sds_cs: sds_cs - # Required: Filename format for input data files - # filename_format: "{dataset_name}_{timestamp}_{domain_satellite_name}.nc" + # Required here or via the INPUT_FILENAME_FORMAT environment variable + # (the environment variable takes priority): Filename format for input data files + # input_filename_format: "{dataset_name}_{timestamp}_{domain_satellite_name}.nc" # Format for PDS data: - filename_format: "NetCDF4_sds_{pds_timestamp}.nc" + input_filename_format: "NetCDF4_sds_{pds_timestamp}.nc" # Available template variables: # {dataset_name}: Dataset name (e.g., KNMI) # {domain_satellite_name}: Satellite domain identifier (e.g., NW_EUROPE_SATELLITE) # {timestamp}: Compact format YYYYMMDDHHMM # {pds_timestamp}: PDS format YYYY-MM-DDTHH_MM_SSZ + # {iso8601_compact_timestamp}: ISO 8601 format timestamp (e.g., YYYY-MM-DDTHHMMZ) # {year}: Four-digit year (e.g., 2026) # {month}: Two-digit month (e.g., 03) # {day}: Two-digit day of month (e.g., 09) # {hour}: Two-digit hour (e.g., 12) # {second}: Two-digit second (e.g., 00) # Path separators are supported, e.g.: "{year}/{month}/{day}/{dataset_name}_{timestamp}.nc" + # + output_filename_format: "SolarNowcast_{timestamp}.nc" + # Optional: output_filename_format (or the OUTPUT_FILENAME_FORMAT environment + # variable, which takes priority) + # Filename format for output forecast files. + # Defaults to "SolarNowcast_{dataset_name}_{iso8601_compact_timestamp}.nc" if omitted. + # Supports the same template variables as input_filename_format, except + # {domain_satellite_name}, which is not available for output files. + # output_filename_format: "SolarNowcast_{dataset_name}_{iso8601_compact_timestamp}.nc" satellite: Meteosat Second Generation (MSG) KNMI_MTG: @@ -36,7 +47,8 @@ KNMI_MTG: nc_variable_names: sds: sds sds_cs: sds_cs - filename_format: "NetCDF4_sds_{pds_timestamp}.nc" + input_filename_format: "NetCDF4_sds_{pds_timestamp}.nc" + output_filename_format: "SolarNowcast_MTG-CPP_{iso8601_compact_timestamp}.nc" input_data_frequency_minutes: 10 satellite: Meteosat Third Generation (MTG) @@ -48,16 +60,16 @@ DWD: nc_variable_names: sds: SIS sds_cs: SISc - # Required: Filename format for input data files + # Required here or via the INPUT_FILENAME_FORMAT environment variable: Filename format for input data files # Supports subdirectories via path separators, e.g.: "{year}/{month}/{day}/{dataset_name}_{timestamp}_{domain_satellite_name}.nc" - filename_format: "{dataset_name}_{timestamp}_{domain_satellite_name}.nc" + input_filename_format: "{dataset_name}_{timestamp}_{domain_satellite_name}.nc" satellite: Meteosat Second Generation (MSG) specMAGIC_MTG: nc_variable_names: sds: GHI sds_cs: CSR - filename_format: "GHI/GHI{year}{month}{day}{hour}{minute}.nc" + input_filename_format: "GHI/GHI{year}{month}{day}{hour}{minute}.nc" input_data_frequency_minutes: 10 clearsky: method: file @@ -68,7 +80,7 @@ specMAGIC_MTG_pvlib: nc_variable_names: sds: GHI sds_cs: GHI_clearsky - filename_format: "GHI/GHI{year}{month}{day}{hour}{minute}.nc" + input_filename_format: "GHI/GHI{year}{month}{day}{hour}{minute}.nc" input_data_frequency_minutes: 10 clearsky: method: pvlib diff --git a/sunflow/config.py b/sunflow/config.py index 7173cbc..5cd3fb9 100644 --- a/sunflow/config.py +++ b/sunflow/config.py @@ -5,6 +5,9 @@ from typing import Self DEFAULT_ENSEMBLE_STATISTICS = "median,mean,p10,p25,p75,p90" +DEFAULT_OUTPUT_FILENAME_FORMAT = ( + "SolarNowcast_{dataset_name}_{iso8601_compact_timestamp}.nc" +) _ALLOWED_STATISTICS = { "median", "mean", @@ -87,6 +90,8 @@ class NowcastConfig: max_clearsky_fallback_days: int min_solar_elevation_degrees: float ensemble_statistics: list[str] + input_filename_format: str | None + output_filename_format: str @classmethod def from_env( @@ -113,18 +118,21 @@ def from_env( - MAX_CLEARSKY_FALLBACK_DAYS (default: 3) - MIN_SOLAR_ELEVATION_DEGREES (default: 6) - ENSEMBLE_STATISTICS (default: median,mean,p10,p25,p75,p90) + - INPUT_FILENAME_FORMAT (no default; must be set here or in config.yaml) + - OUTPUT_FILENAME_FORMAT + (default: SolarNowcast_{dataset_name}_{iso8601_compact_timestamp}.nc) """ overrides = overrides or {} - def _get_value(name: str, default: str) -> str: + def _get_optional_value(name: str) -> str | None: env_value = os.getenv(name) if env_value is not None: return env_value override_value = overrides.get(name.lower()) if override_value is None: - return default + return None if name == "ENSEMBLE_STATISTICS": if isinstance(override_value, str): @@ -134,6 +142,10 @@ def _get_value(name: str, default: str) -> str: return str(override_value) + def _get_value(name: str, default: str) -> str: + value = _get_optional_value(name) + return default if value is None else value + ens_members = ensemble_members # Reference for default noise values: # A. Carpentieri, D. Folini, D. Nerini, S. Pulkkinen, M. Wild, A. Meyer, @@ -166,6 +178,10 @@ def _get_value(name: str, default: str) -> str: _get_value("MIN_SOLAR_ELEVATION_DEGREES", "6") ), ensemble_statistics=statistics, + input_filename_format=_get_optional_value("INPUT_FILENAME_FORMAT"), + output_filename_format=_get_value( + "OUTPUT_FILENAME_FORMAT", DEFAULT_OUTPUT_FILENAME_FORMAT + ), ) diff --git a/sunflow/data_io.py b/sunflow/data_io.py index e96e2cb..f2d8036 100644 --- a/sunflow/data_io.py +++ b/sunflow/data_io.py @@ -74,6 +74,7 @@ def fetch_current_data_with_retry( dataset_name, domain_satellite_name, nowcast_config.satellite_data_directory, + nowcast_config.input_filename_format, ) case "files": check_current_data_existence_file( @@ -81,7 +82,7 @@ def fetch_current_data_with_retry( dataset_name, domain_satellite_name, nowcast_config.satellite_data_directory, - config["filename_format"], + nowcast_config.input_filename_format, domain_satellite, ) case "s3": @@ -90,7 +91,7 @@ def fetch_current_data_with_retry( dataset_name, domain_satellite_name, s3_config, - config["filename_format"], + nowcast_config.input_filename_format, domain_satellite, ) @@ -120,30 +121,38 @@ def fetch_current_data_with_retry( raise -def generate_input_filename( +def generate_filename( time_step: datetime, dataset_name: str, - domain_satellite_name: str, filename_format: str, + domain_satellite_name: str | None = None, ) -> str: - """Generate input filename based on a format template string. + """Generate an input or output filename from a format template string. - Works uniformly across all modes (download, files, s3). + Works uniformly across all modes (download, files, s3) and for both input + files (NowcastConfig.input_filename_format) and forecast output files + (NowcastConfig.output_filename_format). Args: - time_step: Datetime of the data timestep. + time_step: Datetime of the data timestep or forecast reference time. dataset_name: Name of the dataset source. - domain_satellite_name: Satellite domain identifier. - filename_format: Template string from config['filename_format']. + filename_format: Template string to substitute variables into. + domain_satellite_name: Satellite domain identifier. Only available + as a template variable when provided (input filenames). Returns: Filename string with all template variables substituted. + Raises: + KeyError: If the template uses {domain_satellite_name} but no + domain_satellite_name was provided. + Template variables supported: {dataset_name}: Name of the dataset source - {domain_satellite_name}: Satellite domain identifier + {domain_satellite_name}: Satellite domain identifier (input only) {timestamp}: Compact format YYYYMMDDHHMM {pds_timestamp}: PDS format YYYY-MM-DDTHH_MM_SSZ + {iso8601_compact_timestamp}: ISO 8601 compact format timestamp YYYY-MM-DDTHHMMZ {year}: Four-digit year (e.g. 2026) {month}: Two-digit month (e.g. 03) {day}: Two-digit day (e.g. 09) @@ -153,26 +162,24 @@ def generate_input_filename( Path separators in the result are supported, so a format like ``{year}/{month}/{day}/{dataset_name}_{timestamp}.nc`` resolves to a - file inside a date-structured subdirectory of *satellite_data_directory*. + file inside a date-structured subdirectory. """ - # Generate both time formats - timestamp_compact = time_step.strftime("%Y%m%d%H%M") - timestamp_pds = time_step.strftime("%Y-%m-%dT%H_%M_%SZ") - - # Substitute template variables - filename = filename_format.format( - dataset_name=dataset_name, - domain_satellite_name=domain_satellite_name, - timestamp=timestamp_compact, - pds_timestamp=timestamp_pds, + fields = {"dataset_name": dataset_name} + if domain_satellite_name is not None: + fields["domain_satellite_name"] = domain_satellite_name + + return filename_format.format( + timestamp=time_step.strftime("%Y%m%d%H%M"), + pds_timestamp=time_step.strftime("%Y-%m-%dT%H_%M_%SZ"), + iso8601_compact_timestamp=time_step.strftime("%Y-%m-%dT%H%MZ"), year=time_step.strftime("%Y"), month=time_step.strftime("%m"), day=time_step.strftime("%d"), hour=time_step.strftime("%H"), minute=time_step.strftime("%M"), second=time_step.strftime("%S"), + **fields, ) - return filename def check_current_data_existence_file( @@ -180,7 +187,7 @@ def check_current_data_existence_file( dataset_name: str, domain_satellite_name: str, satellite_data_directory: str, - filename_format: str, + input_filename_format: str, required_domain: str | None = None, ) -> None: """Check for existence of current data file. @@ -190,15 +197,19 @@ def check_current_data_existence_file( dataset_name: Name of the dataset source. domain_satellite_name: Satellite domain identifier. satellite_data_directory: Directory containing data files. - filename_format: Template string from config['filename_format']. + input_filename_format: Template string from + NowcastConfig.input_filename_format. required_domain: Optional domain string that must be covered by the file. Raises: FileNotFoundError: If the expected file does not exist. RuntimeError: If required_domain is provided and file coverage is insufficient. """ - filename = generate_input_filename( - request_time, dataset_name, domain_satellite_name, filename_format + filename = generate_filename( + request_time, + dataset_name, + input_filename_format, + domain_satellite_name, ) filepath = os.path.join(satellite_data_directory, filename) logger.info(f"Checking existence of data at {filepath}") @@ -220,7 +231,7 @@ def load_data_from_files( domain_satellite_name: str, satellite_data_directory: str, data_type: str, - filename_format: str, + input_filename_format: str, bbox: str, ) -> xr.Dataset: """Load time steps from files (operational mode). @@ -231,7 +242,8 @@ def load_data_from_files( domain_satellite_name: Satellite domain identifier. satellite_data_directory: Directory containing data files. data_type: Type of data for logging (options: past data, clearsky data). - filename_format: Template string from config['filename_format']. + input_filename_format: Template string from + NowcastConfig.input_filename_format. Returns: xarray Dataset concatenated along a 'time' dimension. @@ -242,8 +254,11 @@ def load_data_from_files( collected: list[xr.Dataset] = [] for time_step in time_steps: - filename = generate_input_filename( - time_step, dataset_name, domain_satellite_name, filename_format + filename = generate_filename( + time_step, + dataset_name, + input_filename_format, + domain_satellite_name, ) filepath = os.path.join(satellite_data_directory, filename) @@ -279,7 +294,7 @@ def check_current_data_existence_s3( dataset_name: str, domain_satellite_name: str, s3_config: S3Config, - filename_format: str, + input_filename_format: str, required_domain: str | None = None, ) -> None: """Check for existence of current data file in S3. @@ -289,15 +304,19 @@ def check_current_data_existence_s3( dataset_name: Name of the dataset source. domain_satellite_name: Satellite domain identifier. s3_config: S3 configuration object. - filename_format: Template string from config['filename_format']. + input_filename_format: Template string from + NowcastConfig.input_filename_format. required_domain: Optional domain string that must be covered by the file. Raises: FileNotFoundError: If the expected file does not exist in S3. RuntimeError: If required_domain is provided and file coverage is insufficient. """ - filename = generate_input_filename( - request_time, dataset_name, domain_satellite_name, filename_format + filename = generate_filename( + request_time, + dataset_name, + input_filename_format, + domain_satellite_name, ) s3_path = f"s3://{s3_config.bucket}/{s3_config.input_prefix}/{filename}" logger.info(f"Checking existence of data at {s3_path}") @@ -329,7 +348,7 @@ def load_data_from_s3( domain_satellite_name: str, s3_config: S3Config, data_type: str, - filename_format: str, + input_filename_format: str, bbox: str, ) -> xr.Dataset: """Load time steps from S3. @@ -340,7 +359,8 @@ def load_data_from_s3( domain_satellite_name: Satellite domain identifier. s3_config: S3 configuration object. data_type: Type of data for logging (options: past data, clearsky data). - filename_format: Template string from config['filename_format']. + input_filename_format: Template string from + NowcastConfig.input_filename_format. Returns: xarray Dataset concatenated along a 'time' dimension. @@ -352,8 +372,11 @@ def load_data_from_s3( fs = _build_s3_filesystem(s3_config) for time_step in time_steps: - filename = generate_input_filename( - time_step, dataset_name, domain_satellite_name, filename_format + filename = generate_filename( + time_step, + dataset_name, + input_filename_format, + domain_satellite_name, ) s3_path = f"s3://{s3_config.bucket}/{s3_config.input_prefix}/{filename}" @@ -380,7 +403,7 @@ def fetch_clearsky_with_fallback( time_steps: list[datetime], run_mode: str, max_fallback_days: int, - filename_format: str, + clearsky_filename_format: str, config: dict[str, Any], bbox: str, dataset_name: str, @@ -399,7 +422,7 @@ def fetch_clearsky_with_fallback( time_steps: Requested clearsky times (typically forecast times - 1 day). run_mode: One of 'download', 'files', or 's3'. max_fallback_days: Maximum number of days back to search per time step. - filename_format: Template string for clear-sky files. + clearsky_filename_format: Template string for clear-sky files. config: Dataset configuration dict. bbox: Bounding box string. dataset_name: Name of the dataset source. @@ -441,7 +464,7 @@ def fetch_clearsky_with_fallback( domain_satellite_name, nowcast_config.satellite_data_directory, "clearsky data", - filename_format, + clearsky_filename_format, bbox=bbox, ) case "s3": @@ -451,7 +474,7 @@ def fetch_clearsky_with_fallback( domain_satellite_name, s3_config, "clearsky data", - filename_format, + clearsky_filename_format, bbox=bbox, ) @@ -506,10 +529,12 @@ def save_forecast( n_steps: Number of forecast time steps to write. latitudes: 1-D array of latitude values (degrees). longitudes: 1-D array of longitude values (degrees). - dataset_name: Name of the dataset source. + dataset_name: Name of the dataset source, written as the + 'satellite_data_source' global attribute. satellite: Satellite platform name written as a global attribute. - nowcast_config: NowcastConfig object supplying output directory - and input data frequency. + nowcast_config: NowcastConfig object supplying output directory, + output filename format (see `generate_filename`) and input data + frequency. model_version: Model version string written as a global attribute. output_mode: Output aggregation mode label written to global NetCDF attrs (expected: 'deterministic', 'ensemble_statistics', @@ -521,7 +546,9 @@ def save_forecast( Filename (basename only) of the written NetCDF file. """ input_data_frequency_minutes = nowcast_config.input_data_frequency_minutes - filename = f"SolarNowcast_{time_step.strftime('%Y%m%d%H%M')}.nc" + filename = generate_filename( + time_step, dataset_name, nowcast_config.output_filename_format + ) data_vars: dict[str, tuple[list[str], np.ndarray, dict[str, str]]] statistics_attr = "" @@ -619,6 +646,7 @@ def save_forecast( ), "output_mode": output_mode, "satellite": satellite, + "satellite_data_source": dataset_name, "history": ( f"Created " f"{datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC" @@ -639,6 +667,7 @@ def save_forecast( save_forecast_to_s3(ds, filename, s3_config, time_encoding) else: output_path = os.path.join(nowcast_config.nowcast_directory, filename) + os.makedirs(os.path.dirname(output_path), exist_ok=True) ds.to_netcdf(output_path, encoding={"time": time_encoding}) return filename @@ -659,7 +688,7 @@ def save_forecast_to_s3( Args: dataset: xarray Dataset to write, as produced by `save_forecast`. filename: Basename of the destination file (e.g. - 'SolarNowcast_202603091200.nc'). + 'SolarNowcast_DWD_2026-03-09T1200Z.nc'). s3_config: S3Config object supplying bucket, output prefix, and endpoint URL. time_encoding: Encoding dict for the 'time' variable (must include at diff --git a/sunflow/downloaders.py b/sunflow/downloaders.py index 6a3f8e3..47d1a6e 100644 --- a/sunflow/downloaders.py +++ b/sunflow/downloaders.py @@ -7,7 +7,7 @@ import xarray as xr from loguru import logger -from .data_io import generate_input_filename +from .data_io import generate_filename from .geospatial import subset_to_bbox @@ -74,12 +74,13 @@ def download_current_data( dataset_name: str, domain_satellite_name: str, satellite_data_directory: str, + input_filename_format: str, ) -> None: """Download satellite data for a single timestep and save it to disk. Downloads all configured variables for the requested time, merges them into a single dataset, and writes the result as a NetCDF file using - the filename format defined in config['filename_format']. The actual + the provided input filename format. The actual timestamp is taken from the dataset (not request_time) to ensure the filename reflects the true observation time. @@ -89,11 +90,13 @@ def download_current_data( Args: request_time: Requested observation time. config: Dataset configuration dict from config.yaml, including - base_url, variables, format, crs, and filename_format. + base_url, variables, format and crs. bbox: Bounding box string lon_min,lat_min,lon_max,lat_max. dataset_name: Name of the dataset source. domain_satellite_name: Satellite domain identifier used in the output filename. satellite_data_directory: Directory where the NetCDF file is saved. + input_filename_format: Template string for the saved filename (from + NowcastConfig.input_filename_format). """ if dataset_name in ["KNMI_MSG", "KNMI_MTG"]: datasets = [] @@ -119,11 +122,11 @@ def download_current_data( # Save data current_time_dt = current_time.astype("datetime64[s]").astype(datetime) - filename = generate_input_filename( + filename = generate_filename( current_time_dt, dataset_name, + input_filename_format, domain_satellite_name, - config["filename_format"], ) output_path = os.path.join(satellite_data_directory, filename) merged_ds.to_netcdf(output_path) diff --git a/sunflow/main.py b/sunflow/main.py index fb9feaa..cb9e95a 100644 --- a/sunflow/main.py +++ b/sunflow/main.py @@ -252,7 +252,7 @@ def run_nowcast( nc_variable_names = config["nc_variable_names"].copy() clearsky_config = config.get( "clearsky", - {"method": "file", "path": config["filename_format"]}, + {"method": "file", "path": nowcast_config.input_filename_format}, ) # Fetch current data (with retry loop in operational mode) @@ -313,7 +313,7 @@ def run_nowcast( domain_satellite_name, nowcast_config.satellite_data_directory, "past data", - config["filename_format"], + nowcast_config.input_filename_format, bbox=domain_satellite, ) case "s3": @@ -323,7 +323,7 @@ def run_nowcast( domain_satellite_name, s3_config, "past data", - config["filename_format"], + nowcast_config.input_filename_format, bbox=domain_satellite, ) @@ -344,7 +344,7 @@ def run_nowcast( nc_variable_names["sds_cs"], ) ) - elif clearsky_config["path"] != config["filename_format"]: + elif clearsky_config["path"] != nowcast_config.input_filename_format: logger.info("Loading separate clearsky data for past observations...") clearsky_data = fetch_clearsky_with_fallback( past_time_steps, @@ -479,12 +479,12 @@ def run_nowcast( if full_ensemble: output_forecast = solar_forecast - output_mode = "full_ensemble" + output_mode = "Full ensemble" logger.info("Saving full ensemble forecast") else: if solar_forecast.shape[0] == 1: output_forecast = solar_forecast - output_mode = "deterministic" + output_mode = "Deterministic" logger.info( "Saving deterministic forecast " "(single ensemble member, kept as singleton ensemble dimension)" @@ -494,7 +494,7 @@ def run_nowcast( solar_forecast, nowcast_config.ensemble_statistics, ) - output_mode = "ensemble_statistics" + output_mode = "Ensemble statistics" logger.info( "Saving ensemble statistics across members: " f"{', '.join(nowcast_config.ensemble_statistics)} " diff --git a/sunflow/validation.py b/sunflow/validation.py index 297950d..323fedc 100644 --- a/sunflow/validation.py +++ b/sunflow/validation.py @@ -58,7 +58,7 @@ def validate_config(config: dict[str, Any], dataset_name: str) -> None: Raises: SystemExit: If any required key is absent. """ - required_keys = ["filename_format", "satellite"] + required_keys = ["satellite"] missing = [k for k in required_keys if k not in config] if missing: logger.error( @@ -90,6 +90,14 @@ def validate_nowcast_config(nowcast_config: NowcastConfig) -> None: ) sys.exit(1) + if not nowcast_config.input_filename_format: + logger.error( + "No input filename format configured. Set the INPUT_FILENAME_FORMAT " + "environment variable or 'input_filename_format' for the dataset in " + "config.yaml. Exiting.\n" + ) + sys.exit(1) + def validate_run_mode(run_mode: str, dataset_name: str) -> None: """Validate that the run mode is compatible with the dataset. diff --git a/uv.lock b/uv.lock index b02b39e..2717309 100644 --- a/uv.lock +++ b/uv.lock @@ -1590,7 +1590,7 @@ dependencies = [ [[package]] name = "sunflow" -version = "1.2.0" +version = "1.3.0" source = { editable = "." } dependencies = [ { name = "fsspec" },