Skip to content

Commit 0afa0c5

Browse files
committed
Add fieldset.describe
1 parent 85b694d commit 0afa0c5

3 files changed

Lines changed: 108 additions & 1 deletion

File tree

src/parcels/_core/fieldset.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from parcels._core.utils.time import get_datetime_type_calendar
2222
from parcels._core.utils.time import is_compatible as datetime_is_compatible
2323
from parcels._python import NOTSET, NotSetType
24+
from parcels._reprs import fieldset_describe
2425
from parcels.interpolators import (
2526
XConstantField,
2627
)
@@ -284,6 +285,10 @@ def from_sgrid_conventions(
284285
model = StructuredModelData.from_sgrid_conventions(ds, mesh, vector_fields)
285286
return cls([model])
286287

288+
def describe(self):
289+
"""Return a table description of a FieldSet, which fields it has and their interpolation methods."""
290+
return fieldset_describe(self)
291+
287292

288293
def assert_compatible_fieldsets(left: FieldSet, right: FieldSet) -> None:
289294
"""Assert that two FieldSets can be combined without name conflicts.

src/parcels/_reprs.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
from __future__ import annotations
44

55
import textwrap
6-
from typing import TYPE_CHECKING, Any, cast
6+
from dataclasses import dataclass
7+
from typing import TYPE_CHECKING, Any, Literal, cast
78

89
import numpy as np
910
import xarray as xr
1011

12+
from parcels._python import isinstance_noimport
13+
1114
if TYPE_CHECKING:
1215
from parcels import Field, FieldSet, ParticleSet
1316
from parcels._core.field import VectorField
17+
from parcels._core.model import ModelData
1418

1519

1620
def fieldset_repr(fieldset: FieldSet) -> str:
@@ -177,3 +181,69 @@ def _format_list_items_multiline(items: list[str] | dict, level: int = 1, with_b
177181

178182
def is_builtin_object(obj):
179183
return obj.__class__.__module__ == "builtins"
184+
185+
186+
@dataclass
187+
class _FieldSetDescriptionRow:
188+
type_: Literal["Field", "VectorField", "Context value"]
189+
model_id: int | None
190+
name: str
191+
interp_method: str | None
192+
193+
def to_dict(self) -> dict[str, str]:
194+
return {
195+
"Type": self.type_,
196+
"Dataset origin": str(self.model_id) if self.model_id is not None else "-",
197+
"Name": self.name,
198+
"Interp method": str(self.interp_method) if self.interp_method is not None else "-",
199+
}
200+
201+
202+
def _print_table(rows: list[_FieldSetDescriptionRow]) -> str:
203+
import pandas as pd
204+
205+
dicts = [r.to_dict() for r in rows]
206+
return pd.DataFrame(dicts).sort_values(["Dataset origin", "Type", "Name"]).to_markdown(index=False)
207+
208+
209+
def fieldset_describe(fieldset: FieldSet) -> str:
210+
rows: list[_FieldSetDescriptionRow] = []
211+
models: dict[int, int] = {} # mapping of memory ID to a human readable ID
212+
213+
assert fieldset._fields is not None
214+
215+
for field in fieldset._fields.values():
216+
model_id: int
217+
218+
# Set human readable model ID
219+
parent_id = id(_get_parent_model(field))
220+
models[parent_id] = models.get(parent_id, len(models))
221+
model_id = models[parent_id]
222+
223+
type_ = field.__class__.__name__
224+
assert type_ in ("Field", "VectorField")
225+
226+
rows.append(
227+
_FieldSetDescriptionRow(
228+
type_=type_,
229+
model_id=model_id,
230+
name=field.name,
231+
interp_method=repr(field.interp_method),
232+
)
233+
)
234+
for k, v in fieldset.context.items():
235+
rows.append(
236+
_FieldSetDescriptionRow(
237+
type_="Context value",
238+
model_id=None,
239+
name=k,
240+
interp_method=repr(v),
241+
)
242+
)
243+
return _print_table(rows)
244+
245+
246+
def _get_parent_model(field: Field | VectorField) -> ModelData:
247+
if isinstance_noimport(field, "Field"):
248+
return field.model
249+
return field.U.model

tests/test_fieldset.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@
1818
ds = datasets_structured["ds_2d_left"]
1919

2020

21+
@pytest.fixture
22+
def fieldset_two_models():
23+
ds1 = datasets_structured["ds_2d_left"][["U_A_grid", "V_A_grid", "grid"]].rename({"U_A_grid": "U", "V_A_grid": "V"})
24+
ds2 = datasets_structured["ds_2d_left"][["U_A_grid", "V_A_grid", "grid"]].rename(
25+
{"U_A_grid": "U_wind", "V_A_grid": "V_wind"}
26+
)
27+
28+
fset1 = FieldSet.from_sgrid_conventions(ds1, mesh="flat")
29+
fset2 = FieldSet.from_sgrid_conventions(ds2, mesh="flat", vector_fields={"UV_wind": ("U_wind", "V_wind")})
30+
fset2.add_context("my_value", 2.0)
31+
fset2.add_context("my_list", [1, 2, "hello"])
32+
return fset1 + fset2
33+
34+
2135
def test_fieldset_init_wrong_types():
2236
with pytest.raises(ValueError, match="Expected `model` to be a ModelData object. Got .*"):
2337
FieldSet([1.0, 2.0, 3.0])
@@ -373,3 +387,21 @@ def test_fieldset_add_context_values():
373387

374388
assert fset.context["c1"] == 1.0
375389
assert fset.context["c2"] == 2.0
390+
391+
392+
def test_fieldset_describe(fieldset_two_models):
393+
fieldset = fieldset_two_models
394+
expected = """\
395+
| Type | Dataset origin | Name | Interp method |
396+
|:--------------|:-----------------|:---------|:----------------------|
397+
| Context value | - | my_list | [1, 2, 'hello'] |
398+
| Context value | - | my_value | 2.0 |
399+
| Field | 0 | U | XLinear(...) |
400+
| Field | 0 | V | XLinear(...) |
401+
| VectorField | 0 | UV | XLinear_Velocity(...) |
402+
| Field | 1 | U_wind | XLinear(...) |
403+
| Field | 1 | V_wind | XLinear(...) |
404+
| VectorField | 1 | UV_wind | XLinear_Velocity(...) |"""
405+
actual = fieldset.describe()
406+
# breakpoint()
407+
assert actual == expected

0 commit comments

Comments
 (0)