Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pydantic ModelEntry #1391

Merged
merged 27 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4084c36
starting
lewisblake Oct 15, 2024
ac90e76
passing all but three aeroval tests
lewisblake Oct 15, 2024
e57d3f9
WIP, breaks some cams2_83 stuff
lewisblake Oct 15, 2024
eb55657
model_ts_type_read allowed to be None
lewisblake Oct 31, 2024
e17bb21
Update README.rst
lewisblake Oct 15, 2024
f9f0ea1
Update README.rst
lewisblake Oct 16, 2024
fa66ae3
add ratpm10pm25 to EMEP calculated variables
jgriesfeller Oct 14, 2024
bf66121
add config example for ratpm10pm25 and ratpm25pm10
jgriesfeller Oct 16, 2024
28f8e7b
rename directory for clearness
jgriesfeller Oct 16, 2024
44cc55f
add some documentation
jgriesfeller Oct 16, 2024
b00ea07
add pm ratio tests
jgriesfeller Oct 16, 2024
2f5f2a0
linting
jgriesfeller Oct 16, 2024
bd55ef5
linting
jgriesfeller Oct 16, 2024
c7785f8
ruff
jgriesfeller Oct 16, 2024
d987fd6
add coverage
jgriesfeller Oct 16, 2024
3a00a07
add seperate example config test
jgriesfeller Oct 17, 2024
30a7fdb
linting
jgriesfeller Oct 17, 2024
0d1ead8
typo
jgriesfeller Oct 17, 2024
36a4f33
adjusted to code review
jgriesfeller Oct 17, 2024
5e1822d
linting
jgriesfeller Oct 17, 2024
dc384f8
a fix to test wtih charlie
lewisblake Oct 18, 2024
c527247
safter version until model_kwargs fixed in pydantic model_entry
lewisblake Oct 18, 2024
c0314a1
add model_data_dir to model_entry
lewisblake Nov 5, 2024
234f859
Use documented attribute
magnusuMET Nov 6, 2024
7ed6026
add attributes which previously were given as kwargs
lewisblake Nov 10, 2024
2c704b5
clean up dead code
lewisblake Nov 10, 2024
c322808
add _lowlevel_helpers.py without old classes
lewisblake Nov 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@ pyaerocom
.. |Coverage| image:: https://codecov.io/gh/metno/pyaerocom/branch/main-dev/graph/badge.svg?token=A0AdX8YciZ
:target: https://codecov.io/gh/metno/pyaerocom

.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.13927979.svg
:target: https://doi.org/10.5281/zenodo.13927979
.. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.13927979.svg
:target: https://doi.org/10.5281/zenodo.13927979
52 changes: 52 additions & 0 deletions docs/_static/aeroval/sample_pm_ratios.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Example aeroval configuration for pm ratios
# using the EMEP reader (which has a built in pm ratio calculation)

import os
from getpass import getuser

from pyaerocom.aeroval import EvalSetup, ExperimentProcessor
from pyaerocom.aeroval.config.pmratios.base_config import get_CFG

reportyear = year = 2019
CFG = get_CFG(
reportyear=reportyear,
year=year,
# model directory
model_dir="/lustre/storeB/project/fou/kl/CAMEO/u8_cams0201/",
)
user = getuser()

CFG.update(
dict(
json_basedir=os.path.abspath(
f"/home/{user}/data/aeroval-local-web/data"
), # always adjust to your environment
coldata_basedir=os.path.abspath(f"/home/{user}/data/aeroval-local-web/coldata"),
# always adjust to your environment
clear_existing_json=True,
add_model_maps=True,
# if True, the analysis will stop whenever an error occurs (else, errors that
# occurred will be written into the logfiles)
raise_exceptions=False,
modelmaps_opts=dict(maps_freq="monthly", maps_res_deg=5),
# Regional filter for analysis
periods=[f"{year}"],
proj_id="RATPM",
exp_id=f"ratpm testing {year}",
exp_name=f"Evaluation of EMEP runs for {year}",
exp_descr=(
f"Evaluation of EMEP runs for {year} for CAMEO. The EMEP model, is compared against observations from EEA and EBAS."
),
exp_pi="[email protected]",
public=True,
# directory where colocated data files are supposed to be stored
weighted_stats=True,
)
)

stp = EvalSetup(**CFG)

ana = ExperimentProcessor(stp)
ana.update_interface()

res = ana.run()
7 changes: 6 additions & 1 deletion docs/aeroval-examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ detailed explanations of the setup parameters. A configuration could be run as t

The code blocks below are the Python configuruation files *cfg_examples_example1.py* and *sample_gridded_io_aux.py*.

Example 1
Example 1: NorESM, CAMS reanalysis against AERONET
---------

NorESM2 and CAMS reanalysis vs AERONET and merged satellite AOD dataset.
Expand All @@ -21,3 +21,8 @@ Example IO aux file for model reading
.. literalinclude:: _static/aeroval/sample_gridded_io_aux.py


Example for pm ratios compared to EMEP model data
---------------------

.. literalinclude:: _static/aeroval/sample_pm_ratios.py

25 changes: 6 additions & 19 deletions pyaerocom/_lowlevel_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,6 @@ def validate(self, val):
return val


class DictType(Validator):
def validate(self, val):
if not isinstance(val, dict):
raise ValueError(f"need dict, got {val}")
return val


class FlexList(Validator):
"""list that can be instantated via input str, tuple or list or None"""

Expand Down Expand Up @@ -170,17 +163,6 @@ def validate(self, val):
return val


class DictStrKeysListVals(Validator):
def validate(self, val: dict):
if not isinstance(val, dict):
raise ValueError(f"need dict, got {val}")
if any(not isinstance(x, str) for x in val):
raise ValueError(f"all keys need to be str type in {val}")
if any(not isinstance(x, list) for x in val.values()):
raise ValueError(f"all values need to be list type in {val}")
return val


class Loc(abc.ABC):
"""Abstract descriptor representing a path location

Expand All @@ -194,7 +176,12 @@ class Loc(abc.ABC):
"""

def __init__(
self, default=None, assert_exists=False, auto_create=False, tooltip=None, logger=None
self,
default=None,
assert_exists=False,
auto_create=False,
tooltip=None,
logger=None,
):
self.assert_exists = assert_exists
self.auto_create = auto_create
Expand Down
2 changes: 1 addition & 1 deletion pyaerocom/aeroval/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def get_entry(self, key) -> ModelEntry:
"""
try:
entry = self[key]
entry["model_name"] = key
entry.model_name = key
return entry
except (KeyError, AttributeError):
raise EntryNotAvailable(f"no such entry {key}")
Expand Down
Empty file.
240 changes: 240 additions & 0 deletions pyaerocom/aeroval/config/pmratios/base_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
"""
Global config for ratio pm10 vs pm25
"""

import copy
import logging
import os

logger = logging.getLogger(__name__)

# Constraints
DEFAULT_RESAMPLE_CONSTRAINTS = dict(
yearly=dict(monthly=9),
monthly=dict(
daily=21,
weekly=3,
),
daily=dict(hourly=18),
)

DEFAULT_RESAMPLE_CONSTRAINTS_DAILY = dict(
daily=dict(hourly=18),
)

# ODCSFUN_EEANRT = "EEAAQeRep.NRT;concpm10/EEAAQeRep.NRT;concpm25"
ODCSFUN_EEAV2 = "EEAAQeRep.v2;concpm10/EEAAQeRep.v2;concpm25"
ODCSFUN_EBAS = "EBASMC;concpm10/EBASMC;concpm25"


def get_CFG(reportyear, year, model_dir) -> dict:
"""create aeroval configuration dict to run the variable
ratpm10pm25 (ratio pm10 vspm25)

:returns: a dict of a model configuration usable for EvalSetup
"""
# get current path for reference to local gridded_io_aux.py

CFG = dict(
json_basedir=os.path.abspath("/home/jang/data/aeroval-local-web/data"),
coldata_basedir=os.path.abspath("/home/jang/data/aeroval-local-web/coldata"),
# io_aux_file=os.path.abspath("/home/jang/data/aeroval-local-web/gridded_io_aux.py"), not needed for ReadMscwCtm
# io_aux_file=os.path.join(base_conf_path, "gridded_io_aux.py"),
# var_scale_colmap_file=os.path.abspath(
# "/home/jang/data/aeroval-local-web/pyaerocom-config/config_files/CAMEO/user_var_scale_colmap.ini"
# ),
# if True, existing colocated data files will be deleted and contours will be overwritten
reanalyse_existing=True,
only_json=False,
add_model_maps=False,
only_model_maps=False,
modelmaps_opts=dict(maps_freq="monthly", maps_res_deg=5),
clear_existing_json=False,
# if True, the analysis will stop whenever an error occurs (else, errors that
# occurred will be written into the logfiles)
raise_exceptions=False,
# Regional filter for analysis
filter_name="ALL-wMOUNTAINS",
# colocation frequency (no statistics in higher resolution can be computed)
ts_type="daily",
map_zoom="Europe",
freqs=[
"yearly",
"monthly",
"daily",
],
periods=[f"{year}"],
main_freq="monthly",
zeros_to_nan=False,
use_diurnal=True,
min_num_obs=DEFAULT_RESAMPLE_CONSTRAINTS,
colocate_time=True,
obs_remove_outliers=False,
model_remove_outliers=False,
harmonise_units=True,
regions_how="country",
annual_stats_constrained=True,
proj_id="emep",
exp_id=f"{reportyear}-reporting",
exp_name=f"Evaluation of EMEP runs for {reportyear} EMEP reporting",
exp_descr=(
f"Evaluation of EMEP runs for {reportyear} EMEP reporting. The EMEP model, simulated for {year}, is compared against observations from EEA and EBAS."
),
exp_pi="[email protected]",
public=True,
# directory where colocated data files are supposed to be stored
weighted_stats=True,
var_order_menu=[
# Gases
"ratpm10pm25",
"ratpm25pm10",
"concNno",
"concNno2",
"concNtno3",
"concNhno3",
"concNtnh",
"concNnh3",
"concnh4",
"concSso2",
"concso4t",
"concso4c",
"vmro3",
"vmro3max",
"vmro3mda8",
"vmrox",
"vmrco",
# PMs
"concpm10",
"concpm25",
"concno3pm10",
"concno3pm25",
"concnh4pm25",
"concso4pm25",
"concCecpm10",
"concCecpm25",
"concCocpm10", # SURF_ugC_PM_OMCOARSE missing in model-output
"concCocpm25",
"concsspm10",
"concsspm25",
# Depositions
"wetrdn",
"wetoxs",
"wetoxn",
"prmm",
],
)

CFG["model_cfg"] = {
"EMEPcameo": dict(
model_id="EMEP,",
model_data_dir=model_dir,
gridded_reader_id={"model": "ReadMscwCtm"},
# model_read_aux={},
model_ts_type_read="daily",
),
}

"""
Filters
"""

BASE_FILTER = {
"latitude": [30, 82],
"longitude": [-30, 90],
}

EBAS_FILTER = {
**BASE_FILTER,
"data_level": [None, 2],
"set_flags_nan": True,
}

OBS_GROUNDBASED = {
##################
# EBAS
##################
"EBAS-d-10": dict(
obs_id="EBASratd10",
web_interface_name="EBAS-d",
obs_vars=["ratpm10pm25"],
obs_vert_type="Surface",
colocate_time=True,
min_num_obs=DEFAULT_RESAMPLE_CONSTRAINTS,
ts_type="daily",
obs_filters=EBAS_FILTER,
obs_type="ungridded",
obs_merge_how={
"ratpm10pm25": "eval",
},
obs_aux_requires={
"ratpm10pm25": {
"EBASMC": [
"concpm10",
"concpm25",
],
}
},
obs_aux_funs={
"ratpm10pm25":
# variables used in computation method need to be based on AeroCom
# units, since the colocated StationData objects (from which the
# new UngriddedData is computed, will perform AeroCom unit check
# and conversion)
"(EBASMC;concpm10/EBASMC;concpm25)"
},
obs_aux_units={"ratpm10pm25": "1"},
),
"EBAS-d-25": dict(
obs_id="EBASratd25",
web_interface_name="EBAS-d",
obs_vars=["ratpm25pm10"],
obs_vert_type="Surface",
colocate_time=True,
min_num_obs=DEFAULT_RESAMPLE_CONSTRAINTS,
ts_type="daily",
obs_filters=EBAS_FILTER,
obs_type="ungridded",
obs_merge_how={
"ratpm25pm10": "eval",
},
obs_aux_requires={
"ratpm25pm10": {
"EBASMC": [
"concpm10",
"concpm25",
],
}
},
obs_aux_funs={
"ratpm25pm10":
# variables used in computation method need to be based on AeroCom
# units, since the colocated StationData objects (from which the
# new UngriddedData is computed, will perform AeroCom unit check
# and conversion)
"(EBASMC;concpm25/EBASMC;concpm10)"
},
obs_aux_units={"ratpm25pm10": "1"},
),
"EBAS-d-tc": dict(
obs_id="EBASMC",
web_interface_name="EBAS-d",
obs_vars=[
"concpm10",
"concpm25",
],
obs_vert_type="Surface",
colocate_time=True,
min_num_obs=DEFAULT_RESAMPLE_CONSTRAINTS,
ts_type="daily",
obs_filters=EBAS_FILTER,
),
}

# Setup for supported satellite evaluations
OBS_SAT = {}

OBS_CFG = {**OBS_GROUNDBASED, **OBS_SAT}

CFG["obs_cfg"] = OBS_CFG

return copy.deepcopy(CFG)
3 changes: 1 addition & 2 deletions pyaerocom/aeroval/experiment_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,9 +784,8 @@ def _create_menu_dict(self) -> dict:
if mod_name not in new[var]["obs"][obs_name][vert_code]:
new[var]["obs"][obs_name][vert_code][mod_name] = {}

model_id = mcfg["model_id"]
new[var]["obs"][obs_name][vert_code][mod_name] = {
"model_id": model_id,
"model_id": mcfg.model_id,
"model_var": mod_var,
"obs_var": obs_var,
}
Expand Down
Loading
Loading