diff --git a/doc/source/analyzing/fields.rst b/doc/source/analyzing/fields.rst index ff309a380f2..a6a5b20cbb2 100644 --- a/doc/source/analyzing/fields.rst +++ b/doc/source/analyzing/fields.rst @@ -122,6 +122,28 @@ field, like its default units or the source code for it. print(ds.field_info["gas", "pressure"].get_units()) print(ds.field_info["gas", "pressure"].get_source()) +Defining properties for on-disk fields +-------------------------------------- + +Occasionally a dataset will have extra fields that are not defined in its +associated frontend. When this is the case, those fields will not be +assigned units, aliases, or a properly formatted display name. These properties +can be defined manually by calling +:meth:`~yt.data_objects.static_output.Dataset.register_field`. + +.. code-block:: python + + ds = yt.load("DD1234/DD1234") + ds.register_field(("enzo", "DinosaurDensity"), units="code_mass/code_length**3") + +This can also be used to override properties defined by the frontend. Note, this +must be done immediately after loading the dataset (i.e., before the ``field_list`` +has been populated). + +To do this within a configuration file, see :ref:`per-field-config`. However, note +that the configuration file method will only operate when the field is not defined +by the frontend; it will not override. + Using fields to access data --------------------------- diff --git a/yt/data_objects/static_output.py b/yt/data_objects/static_output.py index 515ab007c96..2bd201fd130 100644 --- a/yt/data_objects/static_output.py +++ b/yt/data_objects/static_output.py @@ -180,6 +180,7 @@ class Dataset(abc.ABC): _ionization_label_format = "roman_numeral" _determined_fields: dict[str, list[FieldKey]] | None = None fields_detected = False + _registered_fields = None # these are set in self._parse_parameter_file() domain_left_edge = MutableAttribute(True) @@ -1716,6 +1717,47 @@ def quan(self): self._quan = functools.partial(YTQuantity, registry=self.unit_registry) return self._quan + def register_field(self, name, units=None, aliases=None, display_name=None): + """ + Register properties for an on-disk field. + + Register or override units, aliases, or the display name for an + on-disk field. + + Note, this must be called immediately after yt.load (i.e., before + the list of fields is generated). + + Parameters + ---------- + + name : str + name of the field + units : str + units of the field + aliases : list + a list of alias names + display_name: str + the name used in plots + + """ + if self._instantiated_index: + raise RuntimeError( + "Cannot call register_field after field_list is populated. " + "This must be called immediately after load." + ) + if self._registered_fields is None: + self._registered_fields = {} + + entry = {} + if units is not None: + entry["units"] = units + if aliases is not None: + entry["aliases"] = aliases + if display_name is not None: + entry["display_name"] = display_name + + self._registered_fields[name] = entry + def add_field( self, name, function, sampling_type, *, force_override=False, **kwargs ): diff --git a/yt/fields/field_info_container.py b/yt/fields/field_info_container.py index b3837f5ad22..4b4bbc5cafa 100644 --- a/yt/fields/field_info_container.py +++ b/yt/fields/field_info_container.py @@ -150,7 +150,13 @@ def setup_particle_fields(self, ptype, ftype="gas", num_neighbors=64): raise RuntimeError if field[0] not in self.ds.particle_types: continue + units = self.ds.field_units.get(field, None) + rfields = self.ds._registered_fields + if rfields is not None: + if entry := rfields.get(field): + units = entry.get("units", units) + if units is None: try: units = ytcfg.get("fields", *field, "units") @@ -256,6 +262,7 @@ def setup_fluid_aliases(self, ftype: FieldType = "gas") -> None: raise RuntimeError if field[0] in self.ds.particle_types: continue + args = known_other_fields.get(field[1], None) if args is not None: units, aliases, display_name = args @@ -273,6 +280,15 @@ def setup_fluid_aliases(self, ftype: FieldType = "gas") -> None: # field *name* is in there, then the field *tuple*. units = self.ds.field_units.get(field[1], units) units = self.ds.field_units.get(field, units) + + # allow user to override with call to ds.register_fields + rfields = self.ds._registered_fields + if rfields is not None: + if entry := rfields.get(field): + units = entry.get("units", units) + aliases = entry.get("aliases", aliases) + display_name = entry.get("display_name", display_name) + self.add_output_field( field, sampling_type="cell", units=units, display_name=display_name ) diff --git a/yt/fields/tests/test_fields.py b/yt/fields/tests/test_fields.py index 744972b93cd..9d1720a5470 100644 --- a/yt/fields/tests/test_fields.py +++ b/yt/fields/tests/test_fields.py @@ -504,6 +504,18 @@ def test_deposit_amr(): assert_allclose_units(gpm, dpm) +@requires_module("h5py") +@requires_file(ISOGAL) +def test_register_field(): + field = ("enzo", "Phi_pField") + # this is made up + unit_string = "Msun*code_length/kg" + ds = load(ISOGAL) + ds.register_field(field, units=unit_string) + ds.field_list + assert ds.field_info[field].units == unit_string + + def test_ion_field_labels(): fields = [ "O_p1_number_density",