"""Boundary surface helpers.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
"""
from __future__ import annotations
import os
from collections.abc import Mapping
from typing import cast
from .. import _core
from .. import constant
from .._core_utils import _core_call
from .._base import (
SurfaceBase,
_flatten_colormap,
_flatten_legend_options,
_flatten_scalar_minmax,
_flatten_vector_options,
_prefix_payload,
)
from ..data import Dataset, _resolve_dataset_or_current
from ..exceptions import (
InvalidArgumentError,
InvalidDatasetError,
)
from ..utils import (
Colormap,
Legend,
Range,
ScalarMinMax,
VectorOptions,
_coerce_float,
_coerce_pathlike_str,
_bind_range,
_normalize_range_from_values,
)
__all__: list[str] = ["Boundary", "create_boundary"]
def _normalize_boundary_types(
value: constant.BoundaryTypeSelection | str | list[str] | tuple[str, ...],
) -> str | list[str]:
if isinstance(value, constant.BoundaryTypeSelection):
return str(value.value)
if isinstance(value, str):
selection = value.strip().lower()
if selection in {"all", "none"}:
return selection
raise InvalidArgumentError("types string must be 'all' or 'none'")
if isinstance(value, (list, tuple)):
result: list[str] = []
for item in value:
if not isinstance(item, str):
raise InvalidArgumentError("types entries must be strings")
name = item.strip()
if not name:
raise InvalidArgumentError("types entries cannot be empty")
result.append(name)
return result
raise InvalidArgumentError(
"types must be BoundaryTypeSelection, string, or list of strings"
)
def _normalize_material(value: constant.Material | str) -> str:
if isinstance(value, constant.Material):
return str(value.value)
if isinstance(value, str):
name = value.strip()
if not name:
raise InvalidArgumentError("material cannot be empty")
return name
raise InvalidArgumentError("material must be a constant.Material or string")
def _clip_range_payload(value: object, label: str) -> dict[str, float]:
if isinstance(value, Range):
payload: dict[str, object] = dict(value.to_payload())
elif isinstance(value, dict):
payload = dict(cast(dict[str, object], value))
else:
raise InvalidArgumentError(f"{label} must be a Range or dict")
out: dict[str, float] = {}
for key, val in payload.items():
if key not in {"min", "max"}:
continue
out[key] = _coerce_float(val)
if not out:
raise InvalidArgumentError(f"{label} must provide min and/or max")
if "min" in out and "max" in out and out["min"] > out["max"]:
raise InvalidArgumentError(f"{label}.min must be <= {label}.max")
return out
def _make_clip_range(parent: "Boundary", axis: str) -> Range:
key = f"{axis.lower()}_clip"
def get_state() -> dict[str, object]:
clip = parent._state().get(key)
if isinstance(clip, dict):
return cast(dict[str, object], clip)
raise InvalidArgumentError(
f"{axis} clip is not valid for this boundary surface"
)
def set_state(**kwargs: object) -> None:
_core_call(
_core.boundary_surf_set_clip_range, parent._phigs_obj, axis, **kwargs
)
return _bind_range(
get_state=get_state,
set_state=set_state,
cast=_coerce_float,
)
[docs]
class Boundary(SurfaceBase):
"""Boundary surface wrapper.
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>>
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.coloring = fv.constant.Coloring.SCALAR
>>> bnd.scalar_func = ds.scalar_functions[0]
>>> bnd.display_type = fv.constant.DisplayType.MESH
>>> bnd.transparency = 0.25
>>> bnd.colormap.name = fv.constant.ColormapName.SPECTRUM
"""
_core_prefix = "boundary_surf"
_material_coloring_allowed = True
__slots__ = ("_dataset", "_x_clip", "_y_clip", "_z_clip", "_r_clip", "_t_clip")
def __init__(
self, phigs_obj: int, dataset_id: int, dataset: Dataset | None = None
) -> None:
"""Create a boundary-surface wrapper around an existing host object.
Args:
phigs_obj: Host PHIGS object identifier for the boundary surface.
dataset_id: Host dataset identifier backing the surface.
dataset: Optional live :class:`Dataset` wrapper for function lookup
and validation.
"""
super().__init__(phigs_obj, dataset_id)
self._dataset = dataset
self._x_clip = _make_clip_range(self, "X")
self._y_clip = _make_clip_range(self, "Y")
self._z_clip = _make_clip_range(self, "Z")
self._r_clip = _make_clip_range(self, "R")
self._t_clip = _make_clip_range(self, "T")
def _require_dataset(self) -> Dataset:
dataset = self._dataset
if dataset is None:
raise InvalidDatasetError("boundary surface is missing a dataset reference")
dataset._ensure_valid()
return dataset
def _lookup_function_id(
self, name: str, mapping: dict[str, int], label: str
) -> int:
if not isinstance(name, str):
raise InvalidArgumentError(f"{label} function name must be a string")
needle = name.strip()
if not needle:
raise InvalidArgumentError(f"{label} function name cannot be empty")
needle_lower = needle.lower()
for func_name, func_id in mapping.items():
if func_name.lower() == needle_lower:
return func_id
raise InvalidArgumentError(f"{label} function not found: {needle}")
def _set_clip_range(self, axis: str, label: str, value: Range) -> None:
if not isinstance(value, Range):
raise InvalidArgumentError(f"{label} must be a Range")
if value.min is None or value.max is None:
raise InvalidArgumentError(f"{label} must define both min and max")
if value.min > value.max:
raise InvalidArgumentError(f"{label}.min must be <= {label}.max")
_core_call(
_core.boundary_surf_set_clip_range,
self._phigs_obj,
axis,
min=float(value.min),
max=float(value.max),
)
@property
def x_clip(self) -> Range:
"""`Range`: X clip range controller.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.x_clip = fv.Range(-1.0, 1.0)
"""
return self._x_clip
@x_clip.setter
def x_clip(self, value: Range) -> None:
self._set_clip_range("X", "x_clip", value)
@property
def y_clip(self) -> Range:
"""`Range`: Y clip range controller.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.y_clip.min = bnd.y_clip.min + 0.1
"""
return self._y_clip
@y_clip.setter
def y_clip(self, value: Range) -> None:
self._set_clip_range("Y", "y_clip", value)
@property
def z_clip(self) -> Range:
"""`Range`: Z clip range controller.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.z_clip.min = bnd.z_clip.min + 0.1
"""
return self._z_clip
@z_clip.setter
def z_clip(self, value: Range) -> None:
self._set_clip_range("Z", "z_clip", value)
@property
def r_clip(self) -> Range:
"""`Range`: R clip range controller (cylindrical datasets).
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.r_clip.min = bnd.r_clip.min + 0.1
"""
return self._r_clip
@r_clip.setter
def r_clip(self, value: Range) -> None:
self._set_clip_range("R", "r_clip", value)
@property
def t_clip(self) -> Range:
"""`Range`: Theta clip range controller (cylindrical datasets).
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.t_clip.min = bnd.t_clip.min + 1.0
"""
return self._t_clip
@t_clip.setter
def t_clip(self, value: Range) -> None:
self._set_clip_range("T", "t_clip", value)
@property
def scalar_func(self) -> str | None:
"""`str | None`: Selected scalar function name.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.scalar_func = ds.scalar_functions[0]
"""
name = self._state().get("scalar_func", "")
return str(name) if name else None
@scalar_func.setter
def scalar_func(self, value: str) -> None:
dataset = self._require_dataset()
func_id = self._lookup_function_id(
value, dataset._scalar_function_ids, "scalar"
)
_core_call(_core.boundary_surf_set_scalar_func, self._phigs_obj, func_id)
@property
def vector_func(self) -> str | None:
"""`str | None`: Selected vector function name.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.vector_func = "Momentum Vectors [PLOT3D]"
"""
name = self._state().get("vector_func", "")
return str(name) if name else None
@vector_func.setter
def vector_func(self, value: str) -> None:
dataset = self._require_dataset()
func_id = self._lookup_function_id(
value, dataset._vector_function_ids, "vector"
)
_core_call(_core.boundary_surf_set_vector_func, self._phigs_obj, func_id)
@property
def threshold_func(self) -> str | None:
"""`str | None`: Selected threshold function name.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.threshold_func = "Density (Q1)"
"""
name = self._state().get("threshold_func", "")
return str(name) if name else None
@threshold_func.setter
def threshold_func(self, value: str) -> None:
dataset = self._require_dataset()
func_id = self._lookup_function_id(
value, dataset._scalar_function_ids, "threshold"
)
_core_call(_core.boundary_surf_set_threshold_func, self._phigs_obj, func_id)
@property
def threshold(self) -> bool:
"""`bool`: Threshold enabled state.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.threshold = True
"""
return bool(self._state().get("threshold", False))
@threshold.setter
def threshold(self, value: bool) -> None:
_core_call(_core.boundary_surf_set_threshold, self._phigs_obj, bool(value))
@property
def threshold_range(self) -> Range:
"""`Range`: Threshold range.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.threshold_range = bnd.threshold_range
"""
state = self._state()
return Range(
min=_coerce_float(state.get("threshold_min", 0.0)),
max=_coerce_float(state.get("threshold_max", 0.0)),
)
@threshold_range.setter
def threshold_range(self, value: Range) -> None:
if not isinstance(value, Range):
raise InvalidArgumentError("threshold_range must be a Range")
if value.min is None or value.max is None:
raise InvalidArgumentError("threshold_range must define both min and max")
if value.min > value.max:
raise InvalidArgumentError("threshold_range min must be <= max")
_core_call(
_core.boundary_surf_set_threshold_range,
self._phigs_obj,
value.min,
value.max,
)
@property
def material(self) -> str | None:
"""`str | None`: Material name used when ``coloring`` is material.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.material = "Gold"
"""
name = self._state().get("material", "")
return str(name) if name else None
@material.setter
def material(self, value: constant.Material | str) -> None:
_core_call(
_core.boundary_surf_set_material,
self._phigs_obj,
_normalize_material(value),
)
@property
def contours(self) -> constant.ContourColoring:
"""`ContourColoring`: Contour color mode.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.contours = "scalar"
"""
return constant.ContourColoring(
str(
self._state().get("contours", constant.ContourColoring.NONE.value)
).lower()
)
@contours.setter
def contours(self, value: constant.ContourColoring | str) -> None:
contours = (
value.value if isinstance(value, constant.ContourColoring) else str(value)
)
_core_call(_core.boundary_surf_set_contours, self._phigs_obj, contours)
@property
def show_mesh(self) -> bool:
"""`bool`: Mesh overlay visibility.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.show_mesh = True
"""
return bool(self._state().get("show_mesh", False))
@show_mesh.setter
def show_mesh(self, value: bool) -> None:
_core_call(_core.boundary_surf_set_show_mesh, self._phigs_obj, bool(value))
@property
def transparency(self) -> float:
"""`float`: Surface transparency (0.0-1.0).
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.transparency = 0.25
"""
return _coerce_float(self._state().get("transparency", 0.0))
@transparency.setter
def transparency(self, value: float) -> None:
_core_call(_core.boundary_surf_set_transparency, self._phigs_obj, float(value))
@property
def types(self) -> list[str]:
"""`list[str]`: Active boundary type names.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds)
>>> bnd.types = ds.boundary_types # equivalent to fv.constant.BoundaryTypeSelection.ALL
"""
value = self._state().get("types", [])
if isinstance(value, list):
return [str(name) for name in cast(list[object], value)]
return []
@types.setter
def types(
self,
value: constant.BoundaryTypeSelection | str | list[str] | tuple[str, ...],
) -> None:
_core_call(
_core.boundary_surf_set_types,
self._phigs_obj,
_normalize_boundary_types(value),
)
@property
def available_types(self) -> list[str]:
"""`list[str]`: All boundary type names available in the dataset.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.available_types
"""
dataset = self._require_dataset()
return list(dataset.boundary_types)
@property
def vector_options(self) -> VectorOptions:
"""`VectorOptions`: Vector display options.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.vector_options.head_type = fv.constant.VectorHeadType.HEAD3D
"""
return self._get_vector_options()
@vector_options.setter
def vector_options(self, value: VectorOptions | dict[str, object]) -> None:
self._set_vector_options(value)
@property
def scalar_minmax(self) -> ScalarMinMax:
"""`ScalarMinMax`: Scalar min/max annotation options.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.scalar_minmax.show = True
>>> bnd.scalar_minmax.min.text = "Min: %%SCALAR_MIN"
"""
return self._get_scalar_minmax()
@scalar_minmax.setter
def scalar_minmax(self, value: ScalarMinMax | dict[str, object]) -> None:
self._set_scalar_minmax(value)
[docs]
def export(self, filename: str | os.PathLike[str], format: str = "txt") -> None:
"""Export this boundary surface to a file.
Args:
filename: Destination export path.
format: Export format string. Supported values are ``txt``,
``mat``, and ``csv``. Structured datasets support ``txt``
only; ``mat`` and ``csv`` require an unstructured dataset.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.export("/tmp/boundary_surface.csv", format="csv")
"""
filename = _coerce_pathlike_str(filename, "filename")
if not filename.strip():
raise InvalidArgumentError("filename cannot be empty")
if not isinstance(format, str):
raise InvalidArgumentError("format must be a string")
fmt = format.strip().lower()
if fmt == "text":
fmt = "txt"
if fmt not in {"txt", "mat", "csv"}:
raise InvalidArgumentError("format must be txt, mat, or csv")
if fmt in {"mat", "csv"}:
dataset = self._require_dataset()
if not dataset._is_unstructured_cached():
raise InvalidArgumentError(
"format must be txt for structured boundary datasets; mat and csv are supported only for unstructured datasets"
)
_core_call(_core.boundary_surf_export, self._phigs_obj, filename, fmt)
[docs]
def modify(
self,
*,
x_clip: Range | dict[str, object] | None = None,
y_clip: Range | dict[str, object] | None = None,
z_clip: Range | dict[str, object] | None = None,
r_clip: Range | dict[str, object] | None = None,
t_clip: Range | dict[str, object] | None = None,
coloring: constant.Coloring | str | None = None,
material: constant.Material | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
display_type: constant.DisplayType | str | None = None,
line_type: constant.LineType | str | None = None,
contours: constant.ContourColoring | str | None = None,
show_mesh: bool | None = None,
visibility: bool | None = None,
transparency: float | None = None,
scalar_func: str | None = None,
vector_func: str | None = None,
threshold: bool | None = None,
threshold_func: str | None = None,
threshold_range: Range | None = None,
vector_options: VectorOptions | None = None,
scalar_minmax: ScalarMinMax | None = None,
colormap: Colormap | None = None,
legend: Legend | None = None,
types: constant.BoundaryTypeSelection
| str
| list[str]
| tuple[str, ...]
| None = None,
) -> None:
"""Modify multiple boundary properties atomically.
Args:
x_clip: Optional X clipping range.
y_clip: Optional Y clipping range.
z_clip: Optional Z clipping range.
r_clip: Optional R clipping range.
t_clip: Optional T clipping range.
coloring: Coloring mode for the boundary surface.
material: Material selection used when material coloring is active.
geometric_color: Geometric color id used when geometric coloring is
active.
display_type: Surface display type.
line_type: Line style used for wireframe-capable display modes.
contours: Contour coloring mode.
show_mesh: Whether the mesh overlay is shown.
visibility: Whether the boundary surface is visible.
transparency: Surface transparency amount.
scalar_func: Scalar function name used for scalar coloring.
vector_func: Vector function name used for vector display modes.
threshold: Whether thresholding is enabled.
threshold_func: Scalar function name used for thresholding.
threshold_range: Threshold value range.
vector_options: Vector display options.
scalar_minmax: Scalar min/max annotation options.
colormap: Scalar colormap options.
legend: Legend options.
types: Boundary-type selection or explicit boundary-type names.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.modify(coloring=fv.constant.Coloring.SCALAR, transparency=0.3)
"""
kwargs = {
"x_clip": x_clip,
"y_clip": y_clip,
"z_clip": z_clip,
"r_clip": r_clip,
"t_clip": t_clip,
"coloring": coloring,
"material": material,
"geometric_color": geometric_color,
"display_type": display_type,
"line_type": line_type,
"contours": contours,
"show_mesh": show_mesh,
"visibility": visibility,
"transparency": transparency,
"scalar_func": scalar_func,
"vector_func": vector_func,
"threshold": threshold,
"threshold_func": threshold_func,
"threshold_range": threshold_range,
"vector_options": vector_options,
"scalar_minmax": scalar_minmax,
"colormap": colormap,
"legend": legend,
"types": types,
}
if not any(value is not None for value in kwargs.values()):
raise InvalidArgumentError("modify requires at least one property")
payload = self._build_modify_payload(kwargs)
_core_call(_core.boundary_surf_modify, self._phigs_obj, payload)
def _build_modify_payload(self, kwargs: Mapping[str, object]) -> dict[str, object]:
payload: dict[str, object] = {}
for key, prefix in (
("x_clip", "x_clip_"),
("y_clip", "y_clip_"),
("z_clip", "z_clip_"),
("r_clip", "r_clip_"),
("t_clip", "t_clip_"),
):
clip = kwargs.get(key)
if clip is not None:
clip_payload = _clip_range_payload(clip, key)
payload.update(
_prefix_payload(
prefix, {name: value for name, value in clip_payload.items()}
)
)
coloring = kwargs.get("coloring")
if coloring is not None:
mode = (
coloring.value
if isinstance(coloring, constant.Coloring)
else str(coloring).strip().lower()
)
payload["coloring"] = mode
material = kwargs.get("material")
if material is not None:
if not isinstance(material, (constant.Material, str)):
raise InvalidArgumentError(
"material must be a constant.Material or string"
)
payload["material"] = _normalize_material(material)
geometric_color = kwargs.get("geometric_color")
if geometric_color is not None:
if not isinstance(geometric_color, (constant.GeometricColor, int)):
raise InvalidArgumentError(
"geometric_color must be a constant.GeometricColor or int"
)
payload["geometric_color"] = int(geometric_color)
display_type = kwargs.get("display_type")
if display_type is not None:
payload["display_type"] = (
display_type.value
if isinstance(display_type, constant.DisplayType)
else str(display_type)
)
line_type = kwargs.get("line_type")
if line_type is not None:
payload["line_type"] = (
line_type.value
if isinstance(line_type, constant.LineType)
else str(line_type)
)
contours = kwargs.get("contours")
if contours is not None:
payload["contours"] = (
contours.value
if isinstance(contours, constant.ContourColoring)
else str(contours)
)
show_mesh = kwargs.get("show_mesh")
if show_mesh is not None:
payload["show_mesh"] = bool(show_mesh)
visibility = kwargs.get("visibility")
if visibility is not None:
payload["visibility"] = bool(visibility)
transparency = kwargs.get("transparency")
if transparency is not None:
payload["transparency"] = _coerce_float(transparency)
scalar_func = kwargs.get("scalar_func")
if scalar_func is not None:
if not isinstance(scalar_func, str):
raise InvalidArgumentError("scalar_func must be a string")
dataset = self._require_dataset()
payload["scalar_func_id"] = self._lookup_function_id(
scalar_func, dataset._scalar_function_ids, "scalar"
)
vector_func = kwargs.get("vector_func")
if vector_func is not None:
if not isinstance(vector_func, str):
raise InvalidArgumentError("vector_func must be a string")
dataset = self._require_dataset()
payload["vector_func_id"] = self._lookup_function_id(
vector_func, dataset._vector_function_ids, "vector"
)
threshold = kwargs.get("threshold")
if threshold is not None:
payload["threshold"] = bool(threshold)
threshold_func = kwargs.get("threshold_func")
if threshold_func is not None:
if not isinstance(threshold_func, str):
raise InvalidArgumentError("threshold_func must be a string")
dataset = self._require_dataset()
payload["threshold_func_id"] = self._lookup_function_id(
threshold_func, dataset._scalar_function_ids, "threshold"
)
threshold_range = kwargs.get("threshold_range")
if threshold_range is not None:
if not isinstance(threshold_range, Range):
raise InvalidArgumentError("threshold_range must be a Range")
if threshold_range.min is None or threshold_range.max is None:
raise InvalidArgumentError(
"threshold_range must define both min and max"
)
if threshold_range.min > threshold_range.max:
raise InvalidArgumentError("threshold_range min must be <= max")
payload["threshold_min"] = float(threshold_range.min)
payload["threshold_max"] = float(threshold_range.max)
vector_options = kwargs.get("vector_options")
if vector_options is not None:
if isinstance(vector_options, VectorOptions):
payload.update(
_prefix_payload(
"vector_", _flatten_vector_options(vector_options.to_payload())
)
)
elif isinstance(vector_options, dict):
payload.update(
_prefix_payload(
"vector_",
_flatten_vector_options(
cast(dict[str, object], vector_options)
),
)
)
else:
raise InvalidArgumentError(
"vector_options must be a VectorOptions or dict"
)
scalar_minmax = kwargs.get("scalar_minmax")
if scalar_minmax is not None:
scalar_minmax_payload: dict[str, object]
scalar_minmax_show: bool | None
if isinstance(scalar_minmax, ScalarMinMax):
scalar_minmax_payload = scalar_minmax.to_payload()
scalar_minmax_show = scalar_minmax.show
elif isinstance(scalar_minmax, dict):
scalar_minmax_payload, scalar_minmax_show = _flatten_scalar_minmax(
cast(dict[str, object], scalar_minmax)
)
else:
raise InvalidArgumentError(
"scalar_minmax must be a ScalarMinMax or dict"
)
payload.update(_prefix_payload("scalar_minmax_", scalar_minmax_payload))
if scalar_minmax_show is not None:
payload["scalar_minmax_show"] = bool(scalar_minmax_show)
colormap = kwargs.get("colormap")
if colormap is not None:
if isinstance(colormap, Colormap):
payload.update(
_prefix_payload(
"scalar_colormap_", _flatten_colormap(colormap.to_payload())
)
)
elif isinstance(colormap, dict):
payload.update(
_prefix_payload(
"scalar_colormap_",
_flatten_colormap(cast(dict[str, object], colormap)),
)
)
else:
raise InvalidArgumentError("colormap must be a Colormap or dict")
legend = kwargs.get("legend")
if legend is not None:
if isinstance(legend, Legend):
payload.update(
_prefix_payload(
"legend_", _flatten_legend_options(legend.to_payload())
)
)
elif isinstance(legend, dict):
payload.update(
_prefix_payload(
"legend_",
_flatten_legend_options(cast(dict[str, object], legend)),
)
)
else:
raise InvalidArgumentError("legend must be a Legend or dict")
types = kwargs.get("types")
if types is not None:
if not isinstance(
types, (constant.BoundaryTypeSelection, str, list, tuple)
):
raise InvalidArgumentError(
"types must be BoundaryTypeSelection, string, or list of strings"
)
payload["types"] = _normalize_boundary_types(
cast(
constant.BoundaryTypeSelection | str | list[str] | tuple[str, ...],
types,
)
)
return payload
def _copy_create_kwargs(self) -> dict[str, object]:
def clip_payload_or_none(axis: str) -> dict[str, float] | None:
clip = state.get(f"{axis.lower()}_clip")
if not isinstance(clip, dict):
return None
payload = cast(dict[str, object], clip)
normalized = _normalize_range_from_values(
min_value=_coerce_float(payload["min"]),
max_value=_coerce_float(payload["max"]),
abs_min=None
if payload.get("abs_min") is None
else _coerce_float(payload["abs_min"]),
abs_max=None
if payload.get("abs_max") is None
else _coerce_float(payload["abs_max"]),
)
abs_min = _coerce_float(normalized.abs_min)
abs_max = _coerce_float(normalized.abs_max)
min_value = _coerce_float(normalized.min)
max_value = _coerce_float(normalized.max)
tol = 1e-6
min_is_default = abs(min_value - abs_min) <= tol
max_is_default = abs(max_value - abs_max) <= tol
if min_is_default and max_is_default:
return None
out: dict[str, float] = {}
if not min_is_default:
out["min"] = min_value
if not max_is_default:
out["max"] = max_value
return out if out else None
state = self._state()
coloring = self.coloring
threshold_range = self.threshold_range
payload: dict[str, object] = {
"coloring": coloring,
"material": self.material,
"geometric_color": self.geometric_color,
"display_type": self.display_type,
"line_type": self.line_type,
"contours": self.contours,
"show_mesh": self.show_mesh,
"visibility": self.visibility,
"transparency": self.transparency,
"scalar_func": self.scalar_func,
"vector_func": self.vector_func,
"threshold": self.threshold,
"threshold_func": self.threshold_func,
"threshold_range": Range(
_coerce_float(threshold_range.min),
_coerce_float(threshold_range.max),
),
"vector_options": self.vector_options.copy(),
"types": list(self.types),
}
if coloring == constant.Coloring.SCALAR:
payload["scalar_minmax"] = self.scalar_minmax.copy()
payload["colormap"] = self.colormap.copy()
legend = self.legend.copy()
if bool(legend.show):
payload["legend"] = legend
clip_map = {
"x_clip": "X",
"y_clip": "Y",
"z_clip": "Z",
"r_clip": "R",
"t_clip": "T",
}
for key, axis in clip_map.items():
clip_payload = clip_payload_or_none(axis)
if clip_payload is not None:
payload[key] = clip_payload
return payload
[docs]
def copy(self) -> "Boundary":
"""Return a new boundary surface with the same settings.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> copy_obj = bnd.copy()
"""
dataset = self._require_dataset()
kwargs = self._copy_create_kwargs()
return create_boundary(
dataset,
x_clip=cast(Range | dict[str, object] | None, kwargs.get("x_clip")),
y_clip=cast(Range | dict[str, object] | None, kwargs.get("y_clip")),
z_clip=cast(Range | dict[str, object] | None, kwargs.get("z_clip")),
r_clip=cast(Range | dict[str, object] | None, kwargs.get("r_clip")),
t_clip=cast(Range | dict[str, object] | None, kwargs.get("t_clip")),
coloring=cast(constant.Coloring | str | None, kwargs.get("coloring")),
material=cast(constant.Material | str | None, kwargs.get("material")),
geometric_color=cast(
constant.GeometricColor | int | None,
kwargs.get("geometric_color"),
),
display_type=cast(
constant.DisplayType | str | None, kwargs.get("display_type")
),
line_type=cast(constant.LineType | str | None, kwargs.get("line_type")),
contours=cast(
constant.ContourColoring | str | None, kwargs.get("contours")
),
show_mesh=cast(bool | None, kwargs.get("show_mesh")),
visibility=cast(bool | None, kwargs.get("visibility")),
transparency=cast(float | None, kwargs.get("transparency")),
scalar_func=cast(str | None, kwargs.get("scalar_func")),
vector_func=cast(str | None, kwargs.get("vector_func")),
threshold=cast(bool | None, kwargs.get("threshold")),
threshold_func=cast(str | None, kwargs.get("threshold_func")),
threshold_range=cast(Range | None, kwargs.get("threshold_range")),
vector_options=cast(VectorOptions | None, kwargs.get("vector_options")),
scalar_minmax=cast(ScalarMinMax | None, kwargs.get("scalar_minmax")),
colormap=cast(Colormap | None, kwargs.get("colormap")),
legend=cast(Legend | None, kwargs.get("legend")),
types=cast(
constant.BoundaryTypeSelection
| str
| list[str]
| tuple[str, ...]
| None,
kwargs.get("types"),
),
)
[docs]
def delete(self) -> None:
"""Delete this boundary surface.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.delete()
"""
self._delete_surface(_core.boundary_surf_delete)
[docs]
def create_boundary(
dataset: Dataset | None = None,
*,
x_clip: Range | dict[str, object] | None = None,
y_clip: Range | dict[str, object] | None = None,
z_clip: Range | dict[str, object] | None = None,
r_clip: Range | dict[str, object] | None = None,
t_clip: Range | dict[str, object] | None = None,
coloring: constant.Coloring | str | None = None,
material: constant.Material | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
display_type: constant.DisplayType | str | None = None,
line_type: constant.LineType | str | None = None,
contours: constant.ContourColoring | str | None = None,
show_mesh: bool | None = None,
visibility: bool | None = None,
transparency: float | None = None,
scalar_func: str | None = None,
vector_func: str | None = None,
threshold: bool | None = None,
threshold_func: str | None = None,
threshold_range: Range | None = None,
vector_options: VectorOptions | None = None,
scalar_minmax: ScalarMinMax | None = None,
colormap: Colormap | None = None,
legend: Legend | None = None,
types: constant.BoundaryTypeSelection
| str
| list[str]
| tuple[str, ...]
| None = None,
) -> Boundary:
"""Create a boundary surface for a dataset.
Args:
dataset: Dataset to attach the new boundary surface to. When omitted,
the current dataset is used.
x_clip: Optional X clipping range.
y_clip: Optional Y clipping range.
z_clip: Optional Z clipping range.
r_clip: Optional R clipping range.
t_clip: Optional T clipping range.
coloring: Coloring mode for the boundary surface.
material: Material selection used when material coloring is active.
geometric_color: Geometric color id used when geometric coloring is
active.
display_type: Surface display type.
line_type: Line style used for wireframe-capable display modes.
contours: Contour coloring mode.
show_mesh: Whether the mesh overlay is shown.
visibility: Whether the new boundary surface is initially visible.
transparency: Surface transparency amount.
scalar_func: Scalar function name used for scalar coloring.
vector_func: Vector function name used for vector display modes.
threshold: Whether thresholding is enabled.
threshold_func: Scalar function name used for thresholding.
threshold_range: Threshold value range.
vector_options: Vector display options.
scalar_minmax: Scalar min/max annotation options.
colormap: Scalar colormap options.
legend: Legend options.
types: Boundary-type selection or explicit boundary-type names.
Example usage:
.. code-block:: python
>>> import os
>>> import fieldview as fv
>>> data_dir = os.path.join(fv.home, "examples", "f18")
>>> ds = fv.data.load_plot3d(
... os.path.join(data_dir, "f18i9b_g_bin"),
... os.path.join(data_dir, "f18i9b_q_bin"),
... )
>>> bnd = fv.vis.create_boundary(ds, types=fv.constant.BoundaryTypeSelection.ALL)
>>> bnd.coloring = fv.constant.Coloring.SCALAR
>>> bnd.scalar_func = ds.scalar_functions[0]
>>> bnd.colormap.name = fv.constant.ColormapName.SPECTRUM
"""
dataset = _resolve_dataset_or_current(dataset)
has_modifications = any(
value is not None
for value in (
x_clip,
y_clip,
z_clip,
r_clip,
t_clip,
coloring,
material,
geometric_color,
display_type,
line_type,
contours,
show_mesh,
visibility,
transparency,
scalar_func,
vector_func,
threshold,
threshold_func,
threshold_range,
vector_options,
scalar_minmax,
colormap,
legend,
types,
)
)
phigs_obj = _core_call(
_core.boundary_surf_create, dataset.dataset_id, has_modifications
)
surface = Boundary(phigs_obj, dataset.dataset_id, dataset)
if has_modifications:
try:
surface.modify(
x_clip=x_clip,
y_clip=y_clip,
z_clip=z_clip,
r_clip=r_clip,
t_clip=t_clip,
coloring=coloring,
material=material,
geometric_color=geometric_color,
display_type=display_type,
line_type=line_type,
contours=contours,
show_mesh=show_mesh,
visibility=visibility,
transparency=transparency,
scalar_func=scalar_func,
vector_func=vector_func,
threshold=threshold,
threshold_func=threshold_func,
threshold_range=threshold_range,
vector_options=vector_options,
scalar_minmax=scalar_minmax,
colormap=colormap,
legend=legend,
types=types,
)
except Exception:
_core_call(_core.boundary_surf_delete, phigs_obj)
raise
return surface