Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 14 additions & 6 deletions doc/source/analyzing/filtering.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,29 @@ used if you simply need to access the NumPy arrays:
Cut Regions
^^^^^^^^^^^

Cut regions are a more general solution to filtering mesh fields. The output
Cut regions are a more general solution to filtering mesh fields. The output
of a cut region is an entirely new data object, which can be treated like any
other data object to generate images, examine its values, etc. See `this <mesh_filter>`_.

In addition to inputting string parameters into cut_region to specify filters,
wrapper functions exist that allow the user to use a simplified syntax for
filtering out unwanted regions. Such wrapper functions are methods of
:func: ``YTSelectionContainer3D``.

.. notebook-cell::

import yt

ds = yt.load("Enzo_64/DD0042/data0042")
ad = ds.all_data()

low_density = ad.cut_region(
ds.fields.gas.density < ds.quan(0.1, "mp/cm**3")
)
high_temperature = ad.cut_region(
ds.fields.gas.temperature > ds.quan(1e5, "K")
)

In addition to using directly cut_region to specify filters,
wrapper functions exist that allow the user to use a simplified syntax for
filtering out unwanted regions. Such wrapper functions are methods of
:func: ``YTSelectionContainer3D``.

overpressure_and_fast = ad.include_above(("gas", "pressure"), 1e-14)
# You can chain include_xx and exclude_xx to produce the intersection of cut regions
overpressure_and_fast = overpressure_and_fast.include_above(
Expand Down
38 changes: 31 additions & 7 deletions doc/source/analyzing/mesh_filter.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The only argument to a cut region is a conditional on field output from a data object. The only catch is that you *must* denote the data object in the conditional as \"obj\" regardless of the actual object's name. \n",
"The only argument to a cut region is a conditional or a list of conditionals on field output from a data object.\n",
"\n",
"Here we create three new data objects which are copies of the all_data object (a region object covering the entire spatial domain of the simulation), but we've filtered on just \"hot\" material, the \"dense\" material, and the \"overpressure and fast\" material."
]
Expand All @@ -41,6 +41,35 @@
"outputs": [],
"source": [
"ad = ds.all_data()\n",
"\n",
"hot_ad = ad.cut_region(ds.fields.gas.temperature > ds.quan(1e6, \"K\"))\n",
"dense_ad = ad.cut_region(ds.fields.gas.density > ds.quan(5e-30, \"g/cm**3\"))\n",
"\n",
"# You can further cut a cut_region\n",
"dense_and_cool_ad = dense_ad.cut_region(ds.fields.gas.temperature < ds.quan(1e5, \"K\"))\n",
"\n",
"# Or you can provide multiple cuts in one go\n",
"overpressure_and_fast_ad = ad.cut_region([\n",
" ds.fields.gas.pressure > ds.quan(1e-14, \"dyne/cm**2\"),\n",
" ds.fields.gas.velocity_magnitude > ds.quan(1e2, \"km/s\")\n",
"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It is also possible to define a cut region using the `ad.cut_region('obj[...]')` syntax. The content of the string will be evaluated as a Python expression, the only catch is that you *must* denote the data object in the conditional as `obj` regardless of the actual object's name.\n",
"\n",
"The following is equivalent to the previous examples:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"hot_ad = ad.cut_region(['obj[\"gas\", \"temperature\"] > 1e6'])\n",
"dense_ad = ad.cut_region(['obj[\"gas\", \"density\"] > 5e-30'])\n",
"\n",
Expand All @@ -57,7 +86,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"You can also construct a cut_region using the include_ and exclude_ functions as well."
"You can also construct a `cut_region` using the `include_` and `exclude_` functions."
]
},
{
Expand Down Expand Up @@ -145,11 +174,6 @@
"proj2.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
34 changes: 34 additions & 0 deletions doc/source/developing/creating_derived_fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,40 @@ using basic arithmetic if necessary:
If you find yourself using the same custom-defined fields over and over, you should put them in your plugins file as
described in :ref:`plugin-file`.

Short-hand syntax for simple cases
----------------------------------
The workflow described above (define a function, pass it to ``ds.add_field`` specifying
``name``, ``sampling_type``, and ``units``) gives you maximal freedom. However, it can
be somewhat verbose, especially for simple cases. Take the first example, where we
derive the pressure from the gas density and specific thermal energy. We can employ
the following short-hand syntax:

.. code-block:: python

ds.add_field(
("gas", "pressure"),
ds.fields.gas.density / ds.fields.gas.specific_thermal_energy * (ds.gamma - 1.0),
units="dyne/cm**2",
)

This saves you from writing a separate function. Another allowed -- an even more compact -- syntax
is:

.. code-block:: python

ds.fields.gas.pressure = (
ds.fields.gas.density / ds.fields.gas.specific_thermal_energy * (ds.gamma - 1.0)
)

Note that, here, you lose the ability to specify the ``sampling_type`` and the ``units``.
Internally, yt will inherit the sampling type from the terms of the equation and infer the units
automatically.

These two syntaxes only support simple algebraic expressions (``+``, ``-``, ``*``, ``/``, ``<``, ``>``, ``<=`` and ``==``)
with operands that are either (combination of) fields or constants.
It is sufficient for many simple cases, but if you need to do something more complex, you
will need to write a separate function. See for example the following section.

A More Complicated Example
--------------------------

Expand Down
8 changes: 8 additions & 0 deletions yt/data_objects/data_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,14 @@ def _determine_fields(self, fields):
explicit_fields = []
for field in iter_fields(fields):
if field in self._container_fields:
if not isinstance(field, (str, tuple)):
# NOTE: Container fields must be strings or tuples
# otherwise, we end up filling _determined_fields
# with DerivedFields (typing error, and causes
# bugs down the line).
raise RuntimeError(
f"Container fields must be tuples, got {field!r}"
)
explicit_fields.append(field)
continue

Expand Down
28 changes: 20 additions & 8 deletions yt/data_objects/selection_objects/cut_region.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
YTSelectionContainer3D,
)
from yt.data_objects.static_output import Dataset
from yt.funcs import iter_fields, validate_object, validate_sequence
from yt.fields.derived_field import DerivedFieldCombination
from yt.funcs import iter_fields, validate_object
from yt.geometry.selection_routines import points_in_cells
from yt.utilities.exceptions import YTIllDefinedCutRegion
from yt.utilities.on_demand_imports import _scipy
Expand Down Expand Up @@ -54,14 +55,15 @@ def __init__(
if locals is None:
locals = {}
validate_object(data_source, YTSelectionContainer)
validate_sequence(conditionals)
conditionals = list(always_iterable(conditionals))
for condition in conditionals:
validate_object(condition, str)
validate_object(condition, (str, DerivedFieldCombination))

validate_object(ds, Dataset)
validate_object(field_parameters, dict)
validate_object(base_object, YTSelectionContainer)

self.conditionals = list(always_iterable(conditionals))
self.conditionals = conditionals
if isinstance(data_source, YTCutRegion):
# If the source is also a cut region, add its conditionals
# and set the source to be its source.
Expand All @@ -82,6 +84,10 @@ def __init__(
def _check_filter_fields(self):
fields = []
for cond in self.conditionals:
if isinstance(cond, DerivedFieldCombination):
fields.extend(cond.getDependentFields())
continue

for field in re.findall(r"\[([A-Za-z0-9_,.'\"\(\)]+)\]", cond):
fd = field.replace('"', "").replace("'", "")
if "," in fd:
Expand Down Expand Up @@ -125,8 +131,11 @@ def blocks(self):
m = m.copy()
with obj._field_parameter_state(self.field_parameters):
for cond in self.conditionals:
ss = eval(cond)
m = np.logical_and(m, ss, m)
if isinstance(cond, DerivedFieldCombination):
ss = cond(None, obj)
else:
ss = eval(cond)
m &= ss
if not np.any(m):
continue
yield obj, m
Expand All @@ -144,12 +153,15 @@ def _cond_ind(self):
locals["obj"] = obj
with obj._field_parameter_state(self.field_parameters):
for cond in self.conditionals:
res = eval(cond, locals)
if isinstance(cond, DerivedFieldCombination):
res = cond(None, obj)
else:
res = eval(cond, locals)
if ind is None:
ind = res
if ind.shape != res.shape:
raise YTIllDefinedCutRegion(self.conditionals)
np.logical_and(res, ind, ind)
ind &= res
return ind

def _part_ind_KDTree(self, ptype):
Expand Down
25 changes: 21 additions & 4 deletions yt/data_objects/static_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,18 @@
ParticleType,
)
from yt.config import ytcfg
from yt.data_objects.particle_filters import ParticleFilter, filter_registry
from yt.data_objects.particle_filters import (
ParticleFilter,
add_particle_filter,
filter_registry,
)
from yt.data_objects.region_expression import RegionExpression
from yt.data_objects.unions import ParticleUnion
from yt.fields.derived_field import DerivedField, ValidateSpatial
from yt.fields.derived_field import (
DerivedField,
DerivedFieldCombination,
ValidateSpatial,
)
from yt.fields.field_type_container import FieldTypeContainer
from yt.fields.fluid_fields import setup_gradient_fields
from yt.funcs import iter_fields, mylog, set_intersection, setdefaultattr
Expand Down Expand Up @@ -875,6 +883,9 @@ def add_particle_filter(self, filter):
# concatenation fields.
n = getattr(filter, "name", filter)
self.known_filters[n] = None
if isinstance(filter, DerivedFieldCombination):
add_particle_filter(filter.name, filter)
filter = filter.name
if isinstance(filter, str):
used = False
f = filter_registry.get(filter, None)
Expand Down Expand Up @@ -1717,7 +1728,7 @@ def quan(self):
return self._quan

def add_field(
self, name, function, sampling_type, *, force_override=False, **kwargs
self, name, function, *, sampling_type=None, force_override=False, **kwargs
):
"""
Dataset-specific call to add_field
Expand Down Expand Up @@ -1757,7 +1768,13 @@ def add_field(
"""
from yt.fields.field_functions import validate_field_function

validate_field_function(function)
if not isinstance(function, DerivedFieldCombination):
if sampling_type is None:
raise ValueError("You must specify a sampling_type for the field.")
validate_field_function(function)
else:
sampling_type = function.sampling_type

self.index
if force_override and name in self.index.field_list:
raise RuntimeError(
Expand Down
Loading
Loading