Skip to content

Commit 58bb613

Browse files
Refactor species field definition (#175)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 1cf3be3 commit 58bb613

2 files changed

Lines changed: 123 additions & 23 deletions

File tree

src/flekspy/yt/yt.py

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ class FLEKSFieldInfo(FieldInfoContainer):
2424
rho_units = "code_density"
2525
mass_units = "code_mass"
2626

27-
# TODO: find a way to avoid repeating s0, s1...
28-
known_other_fields = (
27+
_base_fields = (
2928
("Bx", (b_units, ["magnetic_field_x"], r"B_x")),
3029
("By", (b_units, ["magnetic_field_y"], r"B_y")),
3130
("Bz", (b_units, ["magnetic_field_z"], r"B_z")),
@@ -35,28 +34,23 @@ class FLEKSFieldInfo(FieldInfoContainer):
3534
("X", (l_units, [], r"X")),
3635
("Y", (l_units, [], r"Y")),
3736
("Z", (l_units, [], r"Z")),
38-
("rhos0", (rho_units, [], r"\rho")),
39-
("uxs0", (v_units, [], r"u_x")),
40-
("uys0", (v_units, [], r"u_y")),
41-
("uzs0", (v_units, [], r"u_z")),
42-
("pxxs0", (p_units, [], r"P_{xx}")),
43-
("pyys0", (p_units, [], r"P_{yy}")),
44-
("pzzs0", (p_units, [], r"P_{zz}")),
45-
("pxys0", (p_units, [], r"P_{xy}")),
46-
("pxzs0", (p_units, [], r"P_{xz}")),
47-
("pyzs0", (p_units, [], r"P_{yz}")),
48-
("rhos1", (rho_units, [], r"\rho")),
49-
("uxs1", (v_units, [], r"u_x")),
50-
("uys1", (v_units, [], r"u_y")),
51-
("uzs1", (v_units, [], r"u_z")),
52-
("pxxs1", (p_units, [], r"P_{xx}")),
53-
("pyys1", (p_units, [], r"P_{yy}")),
54-
("pzzs1", (p_units, [], r"P_{zz}")),
55-
("pxys1", (p_units, [], r"P_{xy}")),
56-
("pxzs1", (p_units, [], r"P_{xz}")),
57-
("pyzs1", (p_units, [], r"P_{yz}")),
5837
)
5938

39+
_species_fields_template = (
40+
("rho", (rho_units, [], r"\rho")),
41+
("ux", (v_units, [], r"u_x")),
42+
("uy", (v_units, [], r"u_y")),
43+
("uz", (v_units, [], r"u_z")),
44+
("pxx", (p_units, [], r"P_{xx}")),
45+
("pyy", (p_units, [], r"P_{yy}")),
46+
("pzz", (p_units, [], r"P_{zz}")),
47+
("pxy", (p_units, [], r"P_{xy}")),
48+
("pxz", (p_units, [], r"P_{xz}")),
49+
("pyz", (p_units, [], r"P_{yz}")),
50+
)
51+
52+
known_other_fields = _base_fields
53+
6054
known_particle_fields = (
6155
("particle_weight", (mass_units, ["p_w"], r"weight")),
6256
("particle_position_x", (l_units, ["p_x"], "x")),
@@ -78,11 +72,57 @@ def __init__(self, ds, field_list):
7872
finfo.nodal_flag = ds.nodal_flags[field]
7973

8074
def setup_fluid_fields(self):
75+
import re
76+
8177
from yt.fields.magnetic_field import setup_magnetic_field_aliases
8278

8379
for field in self.known_other_fields:
8480
fname = field[0]
85-
self.alias(("mesh", fname), ("boxlib", fname))
81+
# Try to alias to boxlib first, then raw
82+
original_name = ("boxlib", fname)
83+
if original_name not in self:
84+
original_name = ("raw", fname)
85+
86+
self.alias(("mesh", fname), original_name)
87+
88+
# Dynamically alias species fields from the input dataset
89+
# Use field_list instead of index.raw_fields to avoid forcing index construction
90+
species_regex = re.compile(r"^(?P<base>.*)s(?P<idx>\d+)$")
91+
92+
# field_list typically contains tuples like ('boxlib', 'varname') or ('raw', 'varname')
93+
# We need to scan for on-disk fields.
94+
# Use self.field_list instead of self.ds.field_list to be robust against incomplete ds mocks in tests
95+
for ftype, fname in self.field_list:
96+
# We are interested in fields that might be raw fluid fields
97+
if ftype not in ("boxlib", "raw"):
98+
continue
99+
100+
match = species_regex.match(fname)
101+
if match:
102+
base = match.group("base")
103+
for template_field, props in self._species_fields_template:
104+
if base == template_field:
105+
# Construct proper LaTeX display name merging subscripts
106+
base_display = props[2]
107+
suffix = f"s{match.group('idx')}"
108+
if "_" in base_display:
109+
name_part, sub_part = base_display.split("_", 1)
110+
if sub_part.startswith("{") and sub_part.endswith("}"):
111+
sub_content = sub_part[1:-1]
112+
display_name = f"{name_part}_{{{sub_content},{suffix}}}"
113+
else:
114+
display_name = f"{name_part}_{{{sub_part},{suffix}}}"
115+
else:
116+
display_name = f"{base_display}_{{{suffix}}}"
117+
118+
# Alias ("mesh", fname) to the found field
119+
self.alias(
120+
("mesh", fname), (ftype, fname), units=props[0]
121+
)
122+
123+
if ("mesh", fname) in self:
124+
self[("mesh", fname)].display_name = display_name
125+
break
86126

87127
# This function is required by yt to correctly handle magnetic field
88128
# units and set up aliases. See:

tests/test_dynamic_species.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
import unittest
3+
from unittest.mock import MagicMock
4+
from flekspy.yt.yt import FLEKSFieldInfo
5+
6+
class TestDynamicFields(unittest.TestCase):
7+
def test_dynamic_species_registration(self):
8+
import yt.geometry.geometry_enum as geometry_enum
9+
10+
# Mock the dataset
11+
mock_ds = MagicMock()
12+
# Set geometry to CARTESIAN to avoid assert_never(geometry) in yt
13+
mock_ds.geometry = geometry_enum.Geometry.CARTESIAN
14+
15+
# Create a fake list of raw fields that includes a high index species
16+
mock_ds.index.raw_fields = ["Bx", "By", "Bz", "rhos0", "rhos99", "uxs99"]
17+
mock_ds.nodal_flags = {f: 0 for f in mock_ds.index.raw_fields}
18+
19+
# Configure field_units on the mock dataset to return string units
20+
mock_ds.field_units.get.return_value = "code_length" # Default for unknown
21+
22+
# Initialize FieldInfo
23+
# Pass the raw fields as field_list to simulate what yt does
24+
field_list = [("raw", f) for f in mock_ds.index.raw_fields]
25+
fi = FLEKSFieldInfo(mock_ds, field_list)
26+
27+
# Setup fluid fields
28+
fi.setup_fluid_fields()
29+
30+
# Verify that dynamic aliasing logic was executed and created expected fields
31+
# Note: in mocked environment without a real index hierarchy,
32+
# 'alias' might not behave exactly as in real yt unless perfectly mocked.
33+
# But we can check if self.alias was called or if the fields are in 'fi'.
34+
35+
# If alias failed because original_name not in self (which alias() checks),
36+
# we need to make sure original_name IS in self.
37+
38+
# Since we passed field_list to __init__, ('raw', 'rhos0') etc should be in fi.
39+
self.assertIn(("raw", "rhos0"), fi)
40+
self.assertIn(("raw", "rhos99"), fi)
41+
42+
# Check if mesh aliases exist
43+
self.assertIn(("mesh", "rhos0"), fi)
44+
self.assertEqual(fi[("mesh", "rhos0")].units, "code_density")
45+
46+
self.assertIn(("mesh", "rhos99"), fi)
47+
self.assertEqual(fi[("mesh", "rhos99")].units, "code_density")
48+
self.assertIn(r"\rho_{s99}", fi[("mesh", "rhos99")].display_name)
49+
50+
self.assertIn(("mesh", "uxs99"), fi)
51+
self.assertEqual(fi[("mesh", "uxs99")].units, "code_velocity")
52+
# u_x + s99 -> u_{x,s99}
53+
self.assertIn(r"u_{x,s99}", fi[("mesh", "uxs99")].display_name)
54+
55+
# Check that unknown patterns are not registered blindly (though regular fields loop handles aliases for known_other_fields)
56+
# Bx is in known_other_fields
57+
self.assertIn(("mesh", "Bx"), fi)
58+
59+
if __name__ == "__main__":
60+
unittest.main()

0 commit comments

Comments
 (0)