Skip to content

Commit 49032ad

Browse files
authored
Update CMIP6 variable filtering and error handling (#124)
* Update Emon variable filtering to include only compatible variables from the CMIP6 table * Fix dimensions mapping for specific_humidity in ACCESS-ESM1.6 * Add error handling for CMIP6Vocabulary initialization and enhance VariableNotFoundError with suggestions * pre-commit
1 parent bd19269 commit 49032ad

4 files changed

Lines changed: 142 additions & 21 deletions

File tree

src/access_moppy/driver.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,24 @@ def __init__(
7272

7373
self.parent_info = {**_default_parent_info, **(parent_info or {})}
7474

75-
# Create the CMIP6Vocabulary instance
76-
self.vocab = CMIP6Vocabulary(
77-
compound_name=compound_name,
78-
experiment_id=experiment_id,
79-
source_id=source_id,
80-
variant_label=variant_label,
81-
grid_label=grid_label,
82-
activity_id=activity_id,
83-
parent_info=self.parent_info,
84-
)
75+
# Create the CMIP6Vocabulary instance with error handling
76+
try:
77+
self.vocab = CMIP6Vocabulary(
78+
compound_name=compound_name,
79+
experiment_id=experiment_id,
80+
source_id=source_id,
81+
variant_label=variant_label,
82+
grid_label=grid_label,
83+
activity_id=activity_id,
84+
parent_info=self.parent_info,
85+
)
86+
except Exception as e:
87+
# For VariableNotFoundError, just re-raise as-is (it already has good messaging)
88+
# For other exceptions, add context about the compound name
89+
if "VariableNotFoundError" in str(type(e)):
90+
raise
91+
else:
92+
raise type(e)(f"Error processing '{compound_name}': {str(e)}") from e
8593

8694
# Initialize the CMORiser based on the compound name
8795
table, _ = compound_name.split(".") # cmor_name now extracted internally

src/access_moppy/mappings/ACCESS-ESM1.6_mappings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
},
153153
"huss": {
154154
"CF standard Name": "specific_humidity",
155-
"dimensions": {"time": "time", "lat_v": "lat", "lon_u": "lon"},
155+
"dimensions": {"time": "time", "lat": "lat", "lon": "lon"},
156156
"units": "1",
157157
"positive": null,
158158
"model_variables": ["fld_s03i237"],

src/access_moppy/vocabulary_processors.py

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,38 @@
33
import uuid
44
from datetime import datetime, timezone
55
from importlib.resources import as_file, files
6-
from typing import Any, Dict, Optional
6+
from typing import Any, Dict, List, Optional
77

88
from access_moppy import _creator
99

1010

11+
class VariableNotFoundError(ValueError):
12+
"""
13+
Exception raised when a requested variable is not found in the specified CMIP6 table.
14+
15+
Provides helpful suggestions for alternative tables or similar variables.
16+
"""
17+
18+
def __init__(
19+
self,
20+
variable_name: str,
21+
table_name: str,
22+
suggestions: Optional[List[str]] = None,
23+
):
24+
self.variable_name = variable_name
25+
self.table_name = table_name
26+
self.suggestions = suggestions or []
27+
28+
message = f"Variable '{variable_name}' not found in CMIP6 table '{table_name}'."
29+
30+
if self.suggestions:
31+
message += "\n\nSuggestions:\n" + "\n".join(
32+
f" • {s}" for s in self.suggestions
33+
)
34+
35+
super().__init__(message)
36+
37+
1138
class CMIP6Vocabulary:
1239
cv_dir = "access_moppy.vocabularies.cmip6_cmor_tables.CMIP6_CVs"
1340
table_dir = "access_moppy.vocabularies.cmip6_cmor_tables.Tables"
@@ -140,9 +167,94 @@ def _get_variable_entry(self) -> Dict[str, Any]:
140167

141168
return var_entry
142169
except KeyError:
143-
raise ValueError(
144-
f"Variable '{self.cmor_name}' not found in table {self.table}."
170+
# Generate helpful suggestions
171+
suggestions = self._get_variable_suggestions()
172+
raise VariableNotFoundError(self.cmor_name, self.table, suggestions)
173+
174+
def _get_variable_suggestions(self) -> List[str]:
175+
"""
176+
Generate helpful suggestions when a variable is not found.
177+
178+
Returns:
179+
List of suggestion strings for the user.
180+
"""
181+
suggestions = []
182+
183+
# Check if variable exists in other CMIP6 tables
184+
common_tables = ["Amon", "Lmon", "Omon", "Emon", "day", "6hrLev", "3hr"]
185+
found_in_tables = []
186+
187+
for table in common_tables:
188+
if table == self.table:
189+
continue # Skip current table
190+
191+
try:
192+
table_file = f"CMIP6_{table}.json"
193+
table_resource = files(self.table_dir) / table_file
194+
195+
with as_file(table_resource) as table_path:
196+
with open(table_path, "r", encoding="utf-8") as f:
197+
table_data = json.load(f)
198+
199+
if self.cmor_name in table_data.get("variable_entry", {}):
200+
found_in_tables.append(table)
201+
202+
except (FileNotFoundError, KeyError):
203+
continue # Table doesn't exist or has no variable_entry
204+
205+
if found_in_tables:
206+
table_list = ", ".join(found_in_tables)
207+
suggestions.append(
208+
f"Variable '{self.cmor_name}' is available in table(s): {table_list}"
145209
)
210+
suggestions.append(f"Try using: {found_in_tables[0]}.{self.cmor_name}")
211+
212+
# Check for similar variable names in current table
213+
try:
214+
current_table_data = self._load_table()
215+
available_vars = list(current_table_data.get("variable_entry", {}).keys())
216+
217+
# Find variables with similar names (simple string similarity)
218+
similar_vars = []
219+
for var in available_vars:
220+
if len(var) > 2 and (
221+
self.cmor_name.lower() in var.lower()
222+
or var.lower() in self.cmor_name.lower()
223+
or
224+
# Check for common root (first 3 characters)
225+
(
226+
len(self.cmor_name) >= 3
227+
and len(var) >= 3
228+
and self.cmor_name[:3].lower() == var[:3].lower()
229+
)
230+
):
231+
similar_vars.append(var)
232+
233+
if similar_vars:
234+
similar_list = ", ".join(similar_vars[:5]) # Limit to 5 suggestions
235+
suggestions.append(
236+
f"Similar variables in {self.table} table: {similar_list}"
237+
)
238+
239+
# Show a sample of available variables if no similar ones found
240+
elif available_vars:
241+
sample_vars = ", ".join(available_vars[:10]) # Show first 10
242+
total_count = len(available_vars)
243+
if total_count > 10:
244+
sample_vars += f" (and {total_count - 10} more)"
245+
suggestions.append(
246+
f"Available variables in {self.table} table: {sample_vars}"
247+
)
248+
249+
except Exception:
250+
pass # Don't fail if we can't load suggestions
251+
252+
# Add general guidance
253+
suggestions.append(
254+
"Visit https://clipc-services.ceda.ac.uk/dreq/index.html to browse CMIP6 variables"
255+
)
256+
257+
return suggestions
146258

147259
def _get_axes(self) -> Dict[str, Any]:
148260
# Resolve resource inside the module path

tests/conftest.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,14 @@ def _filter_variables_by_test_data(variables, table_name):
228228
"mrso", # Total Soil Moisture Content (if soil data available)
229229
],
230230
"Emon": [
231-
# Subset of atmosphere and land variables that work
232-
"tas",
233-
"pr",
234-
"uas",
235-
"vas",
236-
"psl",
237-
"huss",
231+
# Only variables that actually exist in the Emon CMIP6 table
232+
# AND are compatible with the test data (aiihca.pa-101909_mon.nc)
233+
# From atmosphere component:
234+
"hus", # Specific Humidity
235+
"ps", # Surface Air Pressure
236+
"ua", # Eastward Wind
237+
"va", # Northward Wind
238+
# Note: co23D and cSoil exist in Emon but have dimension issues with test data
238239
],
239240
}
240241

0 commit comments

Comments
 (0)