Source code for fieldview.vis.boundary

"""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