diff --git a/pyaerocom/aeroval/_processing_base.py b/pyaerocom/aeroval/_processing_base.py index 786f243d2..b83c64698 100644 --- a/pyaerocom/aeroval/_processing_base.py +++ b/pyaerocom/aeroval/_processing_base.py @@ -79,29 +79,6 @@ class HasColocator(HasConfig): Config class that also has the ability to co-locate """ - def _get_diurnal_only(self, obs_name): - """ - Check if colocated data is flagged for only diurnal processing - - Parameters - ---------- - obs_name : string - Name of observational subset - colocated_data : ColocatedData - A ColocatedData object that will be checked for suitability of - diurnal processing. - - Returns - ------- - diurnal_only : bool - """ - entry = self.cfg.get_obs_entry(obs_name) - try: - diurnal_only = entry["diurnal_only"] - except KeyError: - diurnal_only = False - return diurnal_only - def get_colocator(self, model_name: str = None, obs_name: str = None) -> Colocator: """ Instantiate colocation engine @@ -130,7 +107,7 @@ def get_colocator(self, model_name: str = None, obs_name: str = None) -> Colocat mod_cfg = self.cfg.get_model_entry(model_name) col_cfg["model_cfg"] = mod_cfg - # LB: Hack and at what lowlevel_helpers's import_from was doing + # Hack and at what lowlevel_helpers's import_from was doing for key, val in mod_cfg.items(): if key in ColocationSetup.model_fields: col_cfg[key] = val @@ -139,12 +116,12 @@ def get_colocator(self, model_name: str = None, obs_name: str = None) -> Colocat pyaro_config = obs_cfg["obs_config"] if "obs_config" in obs_cfg else None col_cfg["obs_config"] = pyaro_config - # LB: Hack and at what lowlevel_helpers's import_from was doing - for key, val in obs_cfg.items(): + # Hack and at what lowlevel_helpers's import_from was doing + for key, val in obs_cfg.model_dump().items(): if key in ColocationSetup.model_fields: col_cfg[key] = val - col_cfg["add_meta"].update(diurnal_only=self._get_diurnal_only(obs_name)) + col_cfg["add_meta"].update(diurnal_only=self.cfg.get_obs_entry(obs_name).diurnal_only) col_stp = ColocationSetup(**col_cfg) col = Colocator(col_stp) diff --git a/pyaerocom/aeroval/collections.py b/pyaerocom/aeroval/collections.py index f9d4c6fc9..96d442b28 100644 --- a/pyaerocom/aeroval/collections.py +++ b/pyaerocom/aeroval/collections.py @@ -11,8 +11,9 @@ class BaseCollection(BrowseDict, abc.ABC): #: maximum length of entry names MAXLEN_KEYS = 25 #: Invalid chars in entry names - FORBIDDEN_CHARS_KEYS = ["_"] + FORBIDDEN_CHARS_KEYS = [] + # TODO: Wait a few release cycles after v0.23.0 and see if this can be removed def _check_entry_name(self, key): if any([x in key for x in self.FORBIDDEN_CHARS_KEYS]): raise EvalEntryNameError( @@ -22,8 +23,6 @@ def _check_entry_name(self, key): def __setitem__(self, key, value): self._check_entry_name(key) - if "web_interface_name" in value: - self._check_entry_name(value["web_interface_name"]) super().__setitem__(key, value) def keylist(self, name_or_pattern: str = None) -> list: @@ -70,7 +69,7 @@ def get_entry(self, key) -> object: @property @abc.abstractmethod - def web_iface_names(self) -> list: + def web_interface_names(self) -> list: """ List of webinterface names for """ @@ -107,7 +106,7 @@ def get_entry(self, key) -> object: """ try: entry = self[key] - entry["obs_name"] = self.get_web_iface_name(key) + entry.obs_name = self.get_web_interface_name(key) return entry except (KeyError, AttributeError): raise EntryNotAvailable(f"no such entry {key}") @@ -127,7 +126,7 @@ def get_all_vars(self) -> list[str]: vars.extend(ocfg.get_all_vars()) return sorted(list(set(vars))) - def get_web_iface_name(self, key): + def get_web_interface_name(self, key): """ Get webinterface name for entry @@ -148,13 +147,10 @@ def get_web_iface_name(self, key): corresponding name """ - entry = self[key] - if "web_interface_name" not in entry: - return key - return entry["web_interface_name"] + return self[key].web_interface_name if self[key].web_interface_name is not None else key @property - def web_iface_names(self) -> list: + def web_interface_names(self) -> list: """ List of web interface names for each obs entry @@ -162,12 +158,12 @@ def web_iface_names(self) -> list: ------- list """ - return [self.get_web_iface_name(key) for key in self.keylist()] + return [self.get_web_interface_name(key) for key in self.keylist()] @property def all_vert_types(self): """List of unique vertical types specified in this collection""" - return list({x["obs_vert_type"] for x in self.values()}) + return list({x.obs_vert_type for x in self.values()}) class ModelCollection(BaseCollection): @@ -224,7 +220,7 @@ def get_entry(self, key) -> ModelEntry: raise EntryNotAvailable(f"no such entry {key}") @property - def web_iface_names(self) -> list: + def web_interface_names(self) -> list: """ List of web interface names for each obs entry diff --git a/pyaerocom/aeroval/experiment_output.py b/pyaerocom/aeroval/experiment_output.py index 19e782795..1dd43f9a6 100644 --- a/pyaerocom/aeroval/experiment_output.py +++ b/pyaerocom/aeroval/experiment_output.py @@ -425,7 +425,7 @@ def _check_clean_ts_file(self, fp) -> bool: return True models_avail = list(data) - models_in_exp = self.cfg.model_cfg.web_iface_names + models_in_exp = self.cfg.model_cfg.web_interface_names if all([mod in models_in_exp for mod in models_avail]): # nothing to clean up return False @@ -512,7 +512,7 @@ def get_model_order_menu(self) -> list: ) order.extend(self.cfg.webdisp_opts.model_order_menu) elif self.cfg.webdisp_opts.obsorder_from_config: - order.extend(self.cfg.model_cfg.web_iface_names) + order.extend(self.cfg.model_cfg.web_interface_names) return order def get_obs_order_menu(self) -> list: @@ -526,7 +526,7 @@ def get_obs_order_menu(self) -> list: ) order.extend(self.cfg.webdisp_opts.obs_order_menu) elif self.cfg.webdisp_opts.obsorder_from_config: - order.extend(self.cfg.obs_cfg.web_iface_names) + order.extend(self.cfg.obs_cfg.web_interface_names) return order def _get_json_output_files(self, dirname) -> list[str]: @@ -752,7 +752,7 @@ def _is_part_of_experiment(self, obs_name, obs_var, mod_name, mod_var) -> bool: allobs = self.cfg.obs_cfg obs_matches = [] for key, ocfg in allobs.items(): - if obs_name == allobs.get_web_iface_name(key): + if obs_name == allobs.get_web_interface_name(key): obs_matches.append(ocfg) if len(obs_matches) == 0: self._invalid["obs"].append(obs_name) diff --git a/pyaerocom/aeroval/experiment_processor.py b/pyaerocom/aeroval/experiment_processor.py index 4f292135e..046846af9 100644 --- a/pyaerocom/aeroval/experiment_processor.py +++ b/pyaerocom/aeroval/experiment_processor.py @@ -34,7 +34,7 @@ def _run_single_entry(self, model_name, obs_name, var_list): logger.info(msg) return ocfg = self.cfg.get_obs_entry(obs_name) - if ocfg["is_superobs"]: + if ocfg.is_superobs: try: engine = SuperObsEngine(self.cfg) engine.run( @@ -47,19 +47,19 @@ def _run_single_entry(self, model_name, obs_name, var_list): if self.raise_exceptions: raise logger.warning("failed to process superobs...") - elif ocfg["only_superobs"]: + elif ocfg.only_superobs: logger.info( f"Skipping json processing of {obs_name}, as this is " f"marked to be used only as part of a superobs " f"network" ) - elif ocfg["only_json"]: - if not ocfg["coldata_dir"]: + elif ocfg.only_json: + if not ocfg.coldata_dir: raise Exception( "No coldata_dir provided for an obs network for which only_json=True. The assumption of setting only_json=True is that colocated files already exist, and so a directory for these files must be provided." ) else: - preprocessed_coldata_dir = ocfg["coldata_dir"] + preprocessed_coldata_dir = ocfg.coldata_dir mask = f"{preprocessed_coldata_dir}/{model_name}/*.nc" files_to_convert = glob.glob(mask) engine = ColdataToJsonEngine(self.cfg) @@ -69,7 +69,7 @@ def _run_single_entry(self, model_name, obs_name, var_list): # If a var_list is given, only run on the obs networks which contain that variable if var_list: var_list_asked = var_list - obs_vars = ocfg["obs_vars"] + obs_vars = ocfg.obs_vars var_list = list(set(obs_vars) & set(var_list)) if not var_list: logger.warning( diff --git a/pyaerocom/aeroval/helpers.py b/pyaerocom/aeroval/helpers.py index 4a855249b..0a692161f 100644 --- a/pyaerocom/aeroval/helpers.py +++ b/pyaerocom/aeroval/helpers.py @@ -162,7 +162,7 @@ def make_dummy_model(obs_list: list, cfg) -> str: tmp_var_obj = Variable() # Loops over variables in obs for obs in obs_list: - for var in cfg.obs_cfg[obs]["obs_vars"]: + for var in cfg.obs_cfg[obs].obs_vars: # Create dummy cube dummy_cube = make_dummy_cube(var, start_yr=start, stop_yr=stop, freq=freq) @@ -185,7 +185,7 @@ def make_dummy_model(obs_list: list, cfg) -> str: for dummy_grid_yr in yr_gen: # Add to netcdf yr = dummy_grid_yr.years_avail()[0] - vert_code = cfg.obs_cfg[obs]["obs_vert_type"] + vert_code = cfg.obs_cfg[obs].obs_vert_type save_name = dummy_grid_yr.aerocom_savename(model_id, var, vert_code, yr, freq) dummy_grid_yr.to_netcdf(outdir, savename=save_name) diff --git a/pyaerocom/aeroval/obsentry.py b/pyaerocom/aeroval/obsentry.py index e7f7e2fd4..7931f0466 100644 --- a/pyaerocom/aeroval/obsentry.py +++ b/pyaerocom/aeroval/obsentry.py @@ -1,29 +1,58 @@ import logging +from pathlib import Path from traceback import format_exc +from typing import Literal + +from pydantic import ( + BaseModel, + ConfigDict, + field_validator, + model_validator, +) from pyaerocom import const -from pyaerocom._lowlevel_helpers import BrowseDict, ListOfStrings, StrType +from pyaerocom._lowlevel_helpers import LayerLimits from pyaerocom.exceptions import InitialisationError -from pyaerocom.metastandards import DataSource logger = logging.getLogger(__name__) -class ObsEntry(BrowseDict): - """Observation configuration for evaluation (dictionary) +SUPPORTED_VERT_CODES: tuple[ + str, + str, + str, +] = ( + "Column", + "Profile", + "Surface", +) + +ALT_NAMES_VERT_CODES: dict = dict(ModelLevel="Profile") + + +SUPPORTED_VERT_LOCS: tuple[str, str, str] = ( + "ground", + "space", + "airborne", +) + + +class ObsEntry(BaseModel): + """Observation configuration for evaluation (BaseModel) Note ---- - Only :attr:`obs_id` and `obs_vars` are mandatory, the rest is optional. + Only :attr:`obs_id` and `obs_vars` are mandatory, the rest are optional. Attributes ---------- obs_id : str ID of observation network in AeroCom database (e.g. 'AeronetSunV3Lev2.daily') - obs_vars : list - list of pyaerocom variable names that are supposed to be analysed - (e.g. ['od550aer', 'ang4487aer']) + Note that this can also be a custom supplied obs_id if and only if bs_aux_requires is provided + obs_vars : tuple[str, ...] + tuple of pyaerocom variable names that are supposed to be analysed + (e.g. ('od550aer', 'ang4487aer')) obs_ts_type_read : :obj:`str` or :obj:`dict`, optional may be specified to explicitly define the reading frequency of the observation data (so far, this does only apply to gridded obsdata such @@ -60,36 +89,103 @@ class ObsEntry(BrowseDict): """ - SUPPORTED_VERT_CODES = ["Column", "Profile", "Surface"] # , "2D"] - ALT_NAMES_VERT_CODES = dict(ModelLevel="Profile") - - SUPPORTED_VERT_LOCS = DataSource.SUPPORTED_VERT_LOCS - - obs_vars = ListOfStrings() - obs_vert_type = StrType() - - def __init__(self, **kwargs): - self.obs_id = "" - - self.obs_vars = [] - self.obs_ts_type_read = None - self.obs_vert_type = "" - self.obs_aux_requires = {} - self.instr_vert_loc = None - - self.is_superobs = False - self.only_superobs = False - self.colocation_layer_limts = None - self.profile_layer_limits = None + ## Pydantic ConfigDict + model_config = ConfigDict( + arbitrary_types_allowed=True, + extra="allow", + allow_mutation=False, + validate_assignment=True, + ) + + ###################### + ## Required attributes + ###################### + obs_vars: str | tuple[str, ...] + obs_id: str | tuple[str, ...] + ###################### + ## Optional attributes + ###################### + obs_ts_type_read: str | dict | None = None + obs_vert_type: Literal["Column", "Profile", "Surface", "ModelLevel"] = "Surface" + obs_aux_requires: dict[str, dict] = {} + instr_vert_loc: str | None = None + is_superobs: bool = False + only_superobs: bool = False + colocation_layer_limts: tuple[LayerLimits, ...] | None = None + profile_layer_limits: tuple[LayerLimits, ...] | None = None + web_interface_name: str | None = None + diurnal_only: bool = False + obs_type: str | None = None + + read_opts_ungridded: dict = {} + # attributes for reading colocated data files made outside of pyaerocom + only_json: bool = False + coldata_dir: str | Path | None = ( + None # TODO: Would like this to be a Path but need to see if it will cause issues down the line + ) + + ############# + ## Validators + ############# + @field_validator("obs_vars") + @classmethod + def validate_obs_vars(cls, v): + if isinstance(v, str): + return (v,) + return v + + @field_validator("instr_vert_loc") + @classmethod + def validate_instr_vert_loc(cls, v): + if isinstance(v, str) and v not in SUPPORTED_VERT_LOCS: + raise AttributeError( + f"Invalid value for instr_vert_loc: {v} for {cls.obs_id}. " + f"Please choose from: {SUPPORTED_VERT_LOCS}" + ) - self.read_opts_ungridded = {} - # attributes for reading colocated data files made outside of pyaerocom - self.only_json = False - self.coldata_dir = None + @field_validator("obs_vert_type") + @classmethod + def check_obs_vert_type(cls, ovt): + """Check if obs_vert_type string is valid alias + Parameters + ---------- + ovt : str + obs_vert_type string + Returns + ------- + str + valid obs_vert_type + Raises + ------ + ValueError + if `ovt` is invalid + """ + if ovt in SUPPORTED_VERT_CODES: + return ovt + if ovt in ALT_NAMES_VERT_CODES: + logger.warning( + f"Please use {ALT_NAMES_VERT_CODES[ovt]} for obs_vert_code and not {ovt}" + ) + ovt = ALT_NAMES_VERT_CODES[ovt] + return ovt + valid = SUPPORTED_VERT_CODES + list(ALT_NAMES_VERT_CODES) + raise ValueError( + f"Invalid value for obs_vert_type: {ovt}. " f"Supported codes are {valid}." + ) - self.update(**kwargs) - self.check_cfg() + @model_validator(mode="after") + def check_cfg(self): + if not self.is_superobs and not isinstance(self.obs_id, str | tuple | dict): + raise ValueError( + f"Invalid value for obs_id: {self.obs_id}. Need str, tuple, or dict " + f"or specification of ids and variables via obs_compute_post" + ) self.check_add_obs() + return self + + ########## + ## Methods + ########## def check_add_obs(self): """Check if this dataset is an auxiliary post dataset""" @@ -101,22 +197,20 @@ def check_add_obs(self): ) if self.obs_id not in const.OBS_IDS_UNGRIDDED: try: - const.add_ungridded_post_dataset(**self) + const.add_ungridded_post_dataset(**self.model_dump()) except Exception: raise InitialisationError( f"Cannot initialise auxiliary reading setup for {self.obs_id}. " f"Reason:\n{format_exc()}" ) - def get_all_vars(self) -> list: + def get_all_vars(self) -> tuple[str, ...]: """ - Get list of all variables associated with this entry + Get a tuple of all variables associated with this entry Returns ------- - list - DESCRIPTION. - + tuple[str, ...] """ return self.obs_vars @@ -133,80 +227,15 @@ def has_var(self, var_name): def get_vert_code(self, var): """Get vertical code name for obs / var combination""" - vc = self["obs_vert_type"] + vc = self.obs_vert_type if isinstance(vc, str): val = vc elif isinstance(vc, dict) and var in vc: val = vc[var] else: raise ValueError(f"invalid value for obs_vert_type: {vc}") - if val not in self.SUPPORTED_VERT_CODES: + if val not in SUPPORTED_VERT_CODES: raise ValueError( - f"invalid value for obs_vert_type: {val}. Choose from " - f"{self.SUPPORTED_VERT_CODES}." + f"invalid value for obs_vert_type: {val}. Choose from " f"{SUPPORTED_VERT_CODES}." ) return val - - def check_cfg(self): - """Check that minimum required attributes are set and okay""" - - if not self.is_superobs and not isinstance(self.obs_id, str | dict): - raise ValueError( - f"Invalid value for obs_id: {self.obs_id}. Need str or dict " - f"or specification of ids and variables via obs_compute_post" - ) - if isinstance(self.obs_vars, str): - self.obs_vars = [self.obs_vars] - elif not isinstance(self.obs_vars, list): - raise ValueError(f"Invalid input for obs_vars. Need list or str, got: {self.obs_vars}") - ovt = self.obs_vert_type - if ovt is None: - raise ValueError( - f"obs_vert_type is not defined. Please specify " - f"using either of the available codes: {self.SUPPORTED_VERT_CODES}. " - f"It may be specified for all variables (as string) " - f"or per variable using a dict" - ) - elif isinstance(ovt, str) and ovt not in self.SUPPORTED_VERT_CODES: - self.obs_vert_type = self._check_ovt(ovt) - elif isinstance(self.obs_vert_type, dict): - for var_name, val in self.obs_vert_type.items(): - if val not in self.SUPPORTED_VERT_CODES: - raise ValueError( - f"Invalid value for obs_vert_type: {self.obs_vert_type} " - f"(variable {var_name}). Supported codes are {self.SUPPORTED_VERT_CODES}." - ) - ovl = self.instr_vert_loc - if isinstance(ovl, str) and ovl not in self.SUPPORTED_VERT_LOCS: - raise AttributeError( - f"Invalid value for instr_vert_loc: {ovl} for {self.obs_id}. " - f"Please choose from: {self.SUPPORTED_VERT_LOCS}" - ) - - def _check_ovt(self, ovt): - """Check if obs_vert_type string is valid alias - - Parameters - ---------- - ovt : str - obs_vert_type string - - Returns - ------- - str - valid obs_vert_type - - Raises - ------ - ValueError - if `ovt` is invalid - """ - if ovt in self.ALT_NAMES_VERT_CODES: - _ovt = self.ALT_NAMES_VERT_CODES[ovt] - logger.warning(f"Please use {_ovt} for obs_vert_code and not {ovt}") - return _ovt - valid = self.SUPPORTED_VERT_CODES + list(self.ALT_NAMES_VERT_CODES) - raise ValueError( - f"Invalid value for obs_vert_type: {self.obs_vert_type}. " - f"Supported codes are {valid}." - ) diff --git a/pyaerocom/aeroval/setup_classes.py b/pyaerocom/aeroval/setup_classes.py index 3a7ccec12..1677c1b3e 100644 --- a/pyaerocom/aeroval/setup_classes.py +++ b/pyaerocom/aeroval/setup_classes.py @@ -9,6 +9,7 @@ import datetime from pyaerocom.aeroval.glob_defaults import VarWebInfo, VarWebScaleAndColormap +from pyaerocom.aeroval.obsentry import ObsEntry if sys.version_info >= (3, 11): from typing import Self @@ -528,8 +529,9 @@ def serialize_model_cfg(self, model_cfg: ModelCollection): ## Methods ########################### - def get_obs_entry(self, obs_name) -> dict: - return self.obs_cfg.get_entry(obs_name).to_dict() + def get_obs_entry(self, obs_name) -> ObsEntry: + """Returns ObsEntry instance for network obs_name""" + return self.obs_cfg.get_entry(obs_name) def get_model_entry(self, model_name) -> dict: """Get model entry configuration diff --git a/pyaerocom/aeroval/superobs_engine.py b/pyaerocom/aeroval/superobs_engine.py index 988345544..fdae8d8b7 100644 --- a/pyaerocom/aeroval/superobs_engine.py +++ b/pyaerocom/aeroval/superobs_engine.py @@ -29,7 +29,7 @@ def _process_entry(self, model_name, obs_name, var_list, try_colocate_if_missing sobs_cfg = self.cfg.obs_cfg.get_entry(obs_name) if var_list is None: - var_list = sobs_cfg["obs_vars"] + var_list = sobs_cfg.obs_vars elif isinstance(var_list, str): var_list = [var_list] elif not isinstance(var_list, list): @@ -75,8 +75,8 @@ def _run_var(self, model_name, obs_name, var_name, try_colocate_if_missing): coldata_files = [] coldata_resolutions = [] vert_codes = [] - obs_needed = self.cfg.obs_cfg[obs_name]["obs_id"] - vert_code = self.cfg.obs_cfg.get_entry(obs_name)["obs_vert_type"] + obs_needed = self.cfg.obs_cfg[obs_name].obs_id + vert_code = self.cfg.obs_cfg.get_entry(obs_name).obs_vert_type for oname in obs_needed: fp, ts_type, vert_code = self._get_coldata_fileinfo( model_name, oname, var_name, try_colocate_if_missing @@ -141,5 +141,5 @@ def _get_coldata_fileinfo(self, model_name, obs_name, var_name, try_colocate_if_ fp = cdf[0] meta = ColocatedData.get_meta_from_filename(fp) ts_type = meta["ts_type"] - vert_code = self.cfg.obs_cfg.get_entry(obs_name)["obs_vert_type"] + vert_code = self.cfg.obs_cfg.get_entry(obs_name).obs_vert_type return (fp, ts_type, vert_code) diff --git a/pyaerocom/colocation/colocation_setup.py b/pyaerocom/colocation/colocation_setup.py index d0703f18d..e53513339 100644 --- a/pyaerocom/colocation/colocation_setup.py +++ b/pyaerocom/colocation/colocation_setup.py @@ -320,7 +320,7 @@ class ColocationSetup(BaseModel): @classmethod def validate_obs_vars(cls, v): if isinstance(v, str): - return [v] + return (v,) return v ts_type: str diff --git a/pyaerocom/io/__init__.py b/pyaerocom/io/__init__.py index 2c67cdd0b..cc84e2454 100644 --- a/pyaerocom/io/__init__.py +++ b/pyaerocom/io/__init__.py @@ -11,7 +11,7 @@ # low level EBAS I/O routines from .ebas_nasa_ames import EbasNasaAmesFile -from .fileconventions import FileConventionRead +from .file_conventions import FileConventionRead from .read_aasetal import ReadAasEtal # read base classes diff --git a/pyaerocom/io/fileconventions.py b/pyaerocom/io/file_conventions.py similarity index 100% rename from pyaerocom/io/fileconventions.py rename to pyaerocom/io/file_conventions.py diff --git a/pyaerocom/io/helpers.py b/pyaerocom/io/helpers.py index da04a192b..5a226ab19 100644 --- a/pyaerocom/io/helpers.py +++ b/pyaerocom/io/helpers.py @@ -94,7 +94,7 @@ def _print_read_info(i, mod, tot_num, last_t, name, logger): # pragma: no cover def get_metadata_from_filename(filename): """Try access metadata information from filename""" - from pyaerocom.io.fileconventions import FileConventionRead + from pyaerocom.io.file_conventions import FileConventionRead fc = FileConventionRead().from_file(filename) return fc.get_info_from_file(filename) diff --git a/pyaerocom/io/iris_io.py b/pyaerocom/io/iris_io.py index 21ead8692..6c499e0dc 100644 --- a/pyaerocom/io/iris_io.py +++ b/pyaerocom/io/iris_io.py @@ -35,7 +35,7 @@ VariableDefinitionError, ) from pyaerocom.helpers import cftime_to_datetime64, make_datetimeindex_from_year -from pyaerocom.io.fileconventions import FileConventionRead +from pyaerocom.io.file_conventions import FileConventionRead from pyaerocom.io.helpers import add_file_to_log from pyaerocom.tstype import TsType @@ -218,7 +218,9 @@ def check_dim_coord_names_cube(cube): from pyaerocom import const coords = dict( - lon=const.COORDINFO["lon"], lat=const.COORDINFO["lat"], time=const.COORDINFO["time"] + lon=const.COORDINFO["lon"], + lat=const.COORDINFO["lat"], + time=const.COORDINFO["time"], ) for coord in cube.dim_coords: diff --git a/pyaerocom/io/readgridded.py b/pyaerocom/io/readgridded.py index 90b62c70c..08212bd6b 100755 --- a/pyaerocom/io/readgridded.py +++ b/pyaerocom/io/readgridded.py @@ -44,7 +44,7 @@ multiply_cubes, subtract_cubes, ) -from pyaerocom.io.fileconventions import FileConventionRead +from pyaerocom.io.file_conventions import FileConventionRead from pyaerocom.io.gridded_reader import GriddedReader from pyaerocom.io.helpers import add_file_to_log from pyaerocom.io.iris_io import concatenate_iris_cubes, load_cubes_custom diff --git a/tests/aeroval/test__processing_base.py b/tests/aeroval/test__processing_base.py index e67ede8de..111611dc7 100644 --- a/tests/aeroval/test__processing_base.py +++ b/tests/aeroval/test__processing_base.py @@ -14,7 +14,12 @@ def setup() -> EvalSetup: """EvalSetup instance""" obs_cfg = dict( obs1=dict(obs_id="obs1", obs_vars=["od550aer"], obs_vert_type="Column"), - obs2=dict(obs_id="obs2", obs_vars=["od550aer"], obs_vert_type="Column", diurnal_only=True), + obs2=dict( + obs_id="obs2", + obs_vars=["od550aer"], + obs_vert_type="Column", + diurnal_only=True, + ), ) return EvalSetup(proj_id="bla", exp_id="blub", obs_cfg=obs_cfg) @@ -44,11 +49,6 @@ def collocator(setup: EvalSetup) -> HasColocator: return HasColocator(setup) -def test_HasColocator_get_diurnal_only(collocator: HasColocator): - assert not collocator._get_diurnal_only("obs1") - assert collocator._get_diurnal_only("obs2") - - @pytest.mark.parametrize("obs_name", [None, "obs1", "obs2"]) def test_HasColocator_get_colocator(collocator: HasColocator, obs_name: str | None): col = collocator.get_colocator(obs_name=obs_name) diff --git a/tests/aeroval/test_collections.py b/tests/aeroval/test_collections.py index 5dc04772c..5f2a98f7c 100644 --- a/tests/aeroval/test_collections.py +++ b/tests/aeroval/test_collections.py @@ -1,4 +1,4 @@ -from pyaerocom.aeroval.collections import ObsCollection +from pyaerocom.aeroval.collections import ObsCollection, ModelCollection def test_obscollection(): @@ -13,3 +13,16 @@ def test_obscollection(): ) assert "AN-EEA-MP" in oc + + +def test_modelcollection(): + mc = ModelCollection(model1=dict(model_id="bla", obs_vars="od550aer", obs_vert_type="Column")) + assert mc + + mc["ECMWF_OSUITE"] = dict( + model_id="ECMWF_OSUITE", + obs_vars=["concpm10"], + obs_vert_type="Surface", + ) + + assert "ECMWF_OSUITE" in mc diff --git a/tests/aeroval/test_setup_classes.py b/tests/aeroval/test_setup_classes.py index c9ce5485b..d9ad77bda 100644 --- a/tests/aeroval/test_setup_classes.py +++ b/tests/aeroval/test_setup_classes.py @@ -5,8 +5,7 @@ import pytest from pyaerocom.aeroval import EvalSetup -from pyaerocom.exceptions import EvalEntryNameError -from tests.fixtures.aeroval.cfg_test_exp1 import CFG, MODELS, OBS_GROUNDBASED +from tests.fixtures.aeroval.cfg_test_exp1 import CFG from pyaerocom.aeroval.modelmaps_helpers import CONTOUR @@ -32,36 +31,6 @@ def test_EvalSetup(cfg_exp1: dict): assert EvalSetup(**cfg_exp1) == EvalSetup.model_validate(cfg_exp1) -@pytest.mark.parametrize( - "update,error", - [ - pytest.param( - dict(model_cfg=dict(WRONG_MODEL=MODELS["TM5-AP3-CTRL"])), - "Invalid name: WRONG_MODEL", - id="model_cfg", - ), - pytest.param( - dict(obs_cfg=dict(WRONG_OBS=OBS_GROUNDBASED["AERONET-Sun"])), - "Invalid name: WRONG_OBS", - id="obs_cfg", - ), - pytest.param( - dict(obs_cfg=dict(OBS=dict(web_interface_name="WRONG_OBS"))), - "Invalid name: WRONG_OBS", - id="web_interface_name", - ), - ], -) -def test_EvalSetup_INVALID_ENTRY_NAMES(cfg_exp1: dict, error: str): - with pytest.raises(EvalEntryNameError) as e: - EvalSetup(**cfg_exp1) - assert error in str(e.value) - - with pytest.raises(EvalEntryNameError) as e: - EvalSetup.model_validate(cfg_exp1) - assert error in str(e.value) - - @pytest.mark.parametrize( "update", (