Source code for fieldview.vis.comp

"""Computational 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"),
    ... )
    >>> cs = fv.vis.create_comp(ds)
"""

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,
    _surface_coloring_mode,
)
from ..data import Dataset, _resolve_dataset_or_current
from ..exceptions import InvalidArgumentError, InvalidDatasetError
from ..utils import (
    Colormap,
    Legend,
    Range,
    RangedValue,
    ScalarMinMax,
    VectorOptions,
    _coerce_float,
    _coerce_int,
    _coerce_pathlike_str,
    _bind_ranged_value,
    _normalize_ranged_value_from_values,
)

__all__: list[str] = ["Comp", "create_comp"]


def _range_from_axis_state(state: dict[str, object]) -> RangedValue:
    normalized = _normalize_ranged_value_from_values(
        min_value=_to_index(state["min"], "state.min"),
        max_value=_to_index(state["max"], "state.max"),
        abs_min=_to_index(state["abs_min"], "state.abs_min"),
        abs_max=_to_index(state["abs_max"], "state.abs_max"),
        current=_to_index(state["current"], "state.current"),
    )
    return RangedValue(
        range=Range(
            min=_to_index(normalized.range.min, "range.min"),
            max=_to_index(normalized.range.max, "range.max"),
            abs_min=_to_index(normalized.range.abs_min, "range.abs_min"),
            abs_max=_to_index(normalized.range.abs_max, "range.abs_max"),
        ),
        value=_to_index(normalized.value, "value"),
    )


def _to_index(value: object, label: str) -> int:
    if isinstance(value, bool):
        raise InvalidArgumentError(f"{label} must be an integer")
    if isinstance(value, int):
        return value
    if isinstance(value, float) and value.is_integer():
        return int(value)
    raise InvalidArgumentError(f"{label} must be an integer")


def _to_non_negative_index(value: object, label: str) -> int:
    index = _to_index(value, label)
    if index < 0:
        raise InvalidArgumentError(f"{label} must be >= 0")
    return index


def _to_positive_index(value: object, label: str) -> int:
    index = _to_index(value, label)
    if index < 1:
        raise InvalidArgumentError(f"{label} must be >= 1")
    return index


def _resolve_grid_selection(
    *,
    grid: object = None,
    grid_index: object = None,
    default_grid_index: int | None = None,
) -> int | None:
    resolved_grid_index = None
    if grid is not None:
        resolved_grid_index = _to_positive_index(grid, "grid") - 1
    if grid_index is not None:
        validated_grid_index = _to_non_negative_index(grid_index, "grid_index")
        if (
            resolved_grid_index is not None
            and resolved_grid_index != validated_grid_index
        ):
            raise InvalidArgumentError(
                "grid and grid_index must refer to the same grid"
            )
        resolved_grid_index = validated_grid_index
    if resolved_grid_index is None:
        resolved_grid_index = default_grid_index
    return resolved_grid_index


def _to_axis_inc(value: object, label: str) -> int:
    inc = _to_index(value, label)
    if inc < 1:
        raise InvalidArgumentError(f"{label} must be >= 1")
    return inc


def _make_index_range(parent: "Comp", axis: str) -> RangedValue:
    key = f"{axis.lower()}_plane"

    def get_state() -> dict[str, object]:
        plane = parent._state().get(key)
        if isinstance(plane, dict):
            return cast(dict[str, object], plane)
        raise RuntimeError(f"comp surface state is missing {key}")

    def set_state(**kwargs: object) -> None:
        _core_call(_core.comp_surf_set_axis_range, parent._phigs_obj, axis, **kwargs)

    return _bind_ranged_value(
        get_state=get_state,
        set_state=set_state,
        cast=lambda value: _to_index(value, "index"),
    )


[docs] class Comp(SurfaceBase): """Computational 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"), ... ) >>> cs = fv.vis.create_comp(ds) >>> cs.plane = fv.constant.Plane.I >>> cs.i_plane.value = 10 >>> cs.coloring = fv.constant.Coloring.SCALAR >>> cs.colormap.name = fv.constant.ColormapName.SPECTRUM """ _core_prefix = "comp_surf" __slots__ = ("_dataset", "_i_plane", "_j_plane", "_k_plane") def __init__( self, phigs_obj: int, dataset_id: int, dataset: Dataset | None = None ) -> None: """Create a comp-surface wrapper around an existing host object. Args: phigs_obj: Host PHIGS object identifier for the comp 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._i_plane = _make_index_range(self, "I") self._j_plane = _make_index_range(self, "J") self._k_plane = _make_index_range(self, "K") def _require_dataset(self) -> Dataset: dataset = self._dataset if dataset is None: raise InvalidDatasetError("comp 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}") @property def plane(self) -> constant.Plane: """`Plane`: Active plane (i/j/k). Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.plane = fv.constant.Plane.I """ return constant.Plane(str(self._state().get("axis", "i")).lower()) @plane.setter def plane(self, value: constant.Plane | str) -> None: axis = value.value if isinstance(value, constant.Plane) else str(value) _core_call(_core.comp_surf_set_axis, self._phigs_obj, axis) @property def i_plane(self) -> RangedValue: """`RangedValue`: I plane range controller. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds, plane=fv.constant.Plane.I) >>> cs.i_plane.value = 10 """ return self._i_plane @property def j_plane(self) -> RangedValue: """`RangedValue`: J plane range controller. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds, plane=fv.constant.Plane.J) >>> cs.j_plane.value = 8 """ return self._j_plane @property def k_plane(self) -> RangedValue: """`RangedValue`: K plane range controller. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds, plane=fv.constant.Plane.K) >>> cs.k_plane.value = 12 """ return self._k_plane @property def grid_index(self) -> int: """`int`: Dataset-relative grid index (0-based). This is the Python-oriented alias for :attr:`grid`. The two properties refer to the same selected dataset grid. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds, grid_index=0) >>> cs.grid_index = 1 """ return _coerce_int(self._state().get("grid_index", 0)) @grid_index.setter def grid_index(self, value: int) -> None: index = _to_non_negative_index(value, "grid_index") _core_call(_core.comp_surf_set_grid_index, self._phigs_obj, index) @property def grid(self) -> int: """`int`: Dataset-relative grid number (1-based). This is the UI-oriented alias for :attr:`grid_index`. The two properties refer to the same selected dataset grid. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds, grid=1) >>> cs.grid = 2 """ return self.grid_index + 1 @grid.setter def grid(self, value: int) -> None: self.grid_index = _to_positive_index(value, "grid") - 1 @property def scalar_func(self) -> str | None: """`str | None`: Selected scalar function name. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.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.comp_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 >>> cs = fv.vis.create_comp(ds) >>> cs.vector_func = ds.vector_functions[0] """ 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.comp_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 >>> cs = fv.vis.create_comp(ds) >>> cs.threshold_func = ds.scalar_functions[0] """ 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.comp_surf_set_threshold_func, self._phigs_obj, func_id) @property def threshold(self) -> bool: """`bool`: Threshold enabled state. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.threshold = True """ return bool(self._state().get("threshold", False)) @threshold.setter def threshold(self, value: bool) -> None: _core_call(_core.comp_surf_set_threshold, self._phigs_obj, bool(value)) @property def threshold_range(self) -> Range: """`Range`: Threshold min/max range. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.threshold_range = fv.Range(min=0.2, max=0.8) """ 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.comp_surf_set_threshold_range, self._phigs_obj, value.min, value.max ) @property def transparency(self) -> float: """`float`: Transparency (0.0-1.0). Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.transparency = 0.35 """ return _coerce_float(self._state().get("transparency", 0.0)) @transparency.setter def transparency(self, value: float) -> None: _core_call(_core.comp_surf_set_transparency, self._phigs_obj, float(value)) @property def contours(self) -> constant.ContourColoring: """`ContourColoring`: Contour line coloring. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.contours = fv.constant.ContourColoring.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.comp_surf_set_contours, self._phigs_obj, contours) @property def show_mesh(self) -> bool: """`bool`: Mesh overlay visibility. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> cs.show_mesh = True """ return bool(self._state().get("show_mesh", False)) @show_mesh.setter def show_mesh(self, value: bool) -> None: _core_call(_core.comp_surf_set_show_mesh, self._phigs_obj, bool(value)) @property def vector_options(self) -> VectorOptions: """`VectorOptions`: Vector display options. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> opts = cs.vector_options >>> opts.scale = 1.2 """ 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 options. Example usage: .. code-block:: python >>> cs = fv.vis.create_comp(ds) >>> smm = cs.scalar_minmax >>> smm.show = True """ 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 sweep( self, cycles: int = 1, output_file: str | os.PathLike[str] | None = None, direction: constant.SweepDirection | None = None, ) -> None: """Sweep the comp surface and optionally write frames to a file. Args: cycles: Number of sweep cycles to run. output_file: Optional movie or frame output path. direction: Optional sweep direction 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"), ... ) >>> cs = fv.vis.create_comp(ds, plane=fv.constant.Plane.I) >>> cs.sweep(2, "/tmp/comp_sweep.mp4", fv.constant.SweepDirection.BOUNCE) Notes: ``output_file`` must use ``.png``, ``.mp4``, or ``.avi``. """ if not isinstance(cycles, int): raise InvalidArgumentError("cycles must be an integer") if cycles <= 0: raise InvalidArgumentError("cycles must be positive") direction_value: str | None = None if direction is not None: if not isinstance(direction, constant.SweepDirection): raise InvalidArgumentError( "direction must be a constant.SweepDirection" ) direction_value = direction.value if output_file is None: if direction_value is None: _core_call(_core.comp_surf_sweep, self._phigs_obj, cycles) else: _core_call( _core.comp_surf_sweep, self._phigs_obj, cycles, None, direction_value, ) return output_file = _coerce_pathlike_str(output_file, "output_file") if not output_file.strip(): raise InvalidArgumentError("output_file cannot be empty") if direction_value is None: _core_call(_core.comp_surf_sweep, self._phigs_obj, cycles, output_file) else: _core_call( _core.comp_surf_sweep, self._phigs_obj, cycles, output_file, direction_value, )
[docs] def export(self, filename: str | os.PathLike[str], format: str = "txt") -> None: """Export this comp surface to a text file. Args: filename: Destination export path. format: Export format string. Only ``txt`` is currently supported. 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"), ... ) >>> cs = fv.vis.create_comp(ds) >>> cs.export("/tmp/comp_surface.txt", format="txt") """ 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 != "txt": raise InvalidArgumentError("format must be txt") _core_call(_core.comp_surf_export, self._phigs_obj, filename, fmt)
def _get_axis_inc(self, axis: str) -> int: key = f"{axis.lower()}_plane" plane = self._state().get(key) if isinstance(plane, dict): return _coerce_int(cast(dict[str, object], plane).get("inc", 1)) raise RuntimeError(f"comp surface state is missing {key}") def _set_axis_inc(self, axis_key: str, value: int) -> None: axis_inc = _to_axis_inc(value, axis_key) if axis_key == "I_inc": self.modify(I_inc=axis_inc) elif axis_key == "J_inc": self.modify(J_inc=axis_inc) else: self.modify(K_inc=axis_inc) @property def I_inc(self) -> int: """`int`: Sweep increment for the I axis.""" return self._get_axis_inc("I") @I_inc.setter def I_inc(self, value: int) -> None: self._set_axis_inc("I_inc", value) @property def J_inc(self) -> int: """`int`: Sweep increment for the J axis.""" return self._get_axis_inc("J") @J_inc.setter def J_inc(self, value: int) -> None: self._set_axis_inc("J_inc", value) @property def K_inc(self) -> int: """`int`: Sweep increment for the K axis.""" return self._get_axis_inc("K") @K_inc.setter def K_inc(self, value: int) -> None: self._set_axis_inc("K_inc", value)
[docs] def modify( self, *, grid: int | None = None, grid_index: int | None = None, plane: constant.Plane | str | None = None, i_plane: RangedValue | None = None, j_plane: RangedValue | None = None, k_plane: RangedValue | None = None, coloring: constant.Coloring | 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, I_inc: int | None = None, J_inc: int | None = None, K_inc: int | None = None, ) -> None: """Modify multiple comp properties atomically. ``grid`` and ``grid_index`` are interchangeable aliases for selecting the target dataset grid. ``grid`` is 1-based to match the UI, while ``grid_index`` is 0-based for Python-style indexing. When both are provided, they must refer to the same grid. Args: grid: Optional 1-based dataset grid number. grid_index: Optional 0-based dataset grid index. plane: Active plane selection. i_plane: Optional I-plane ranged-value controller. j_plane: Optional J-plane ranged-value controller. k_plane: Optional K-plane ranged-value controller. coloring: Coloring mode for the comp surface. 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 comp 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. I_inc: Sweep increment for the I axis. J_inc: Sweep increment for the J axis. K_inc: Sweep increment for the K axis. 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"), ... ) >>> cs = fv.vis.create_comp(ds) >>> cs.modify(visibility=False, transparency=0.2) """ kwargs = { "grid": grid, "grid_index": grid_index, "plane": plane, "i_plane": i_plane, "j_plane": j_plane, "k_plane": k_plane, "coloring": coloring, "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, "I_inc": I_inc, "J_inc": J_inc, "K_inc": K_inc, } 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.comp_surf_modify, self._phigs_obj, payload)
def _build_modify_payload(self, kwargs: Mapping[str, object]) -> dict[str, object]: payload: dict[str, object] = {} resolved_grid_index = _resolve_grid_selection( grid=kwargs.get("grid"), grid_index=kwargs.get("grid_index"), ) if resolved_grid_index is not None: payload["grid_index"] = resolved_grid_index plane = kwargs.get("plane") if plane is not None: payload["axis"] = ( plane.value if isinstance(plane, constant.Plane) else str(plane) ) axis_ranges = [ ("i_plane", "i_"), ("j_plane", "j_"), ("k_plane", "k_"), ] for key, prefix in axis_ranges: if key in kwargs and kwargs[key] is not None: axis_payload = _axis_range_payload(kwargs[key], key) payload.update( _prefix_payload( prefix, {name: value for name, value in axis_payload.items()} ) ) if kwargs.get("I_inc") is not None: payload["i_inc"] = _to_axis_inc(kwargs["I_inc"], "I_inc") if kwargs.get("J_inc") is not None: payload["j_inc"] = _to_axis_inc(kwargs["J_inc"], "J_inc") if kwargs.get("K_inc") is not None: payload["k_inc"] = _to_axis_inc(kwargs["K_inc"], "K_inc") coloring = kwargs.get("coloring") if coloring is not None: mode = _surface_coloring_mode( cast(constant.Coloring | str, coloring), surface_name="comp surfaces", ) payload["coloring"] = mode 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: if not isinstance(transparency, (int, float)): raise InvalidArgumentError("transparency must be a number") payload["transparency"] = 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") return payload def _copy_create_kwargs(self) -> dict[str, object]: state = self._state() i_state = state.get("i_plane") j_state = state.get("j_plane") k_state = state.get("k_plane") if not isinstance(i_state, dict): raise RuntimeError("comp surface state is missing i_plane") if not isinstance(j_state, dict): raise RuntimeError("comp surface state is missing j_plane") if not isinstance(k_state, dict): raise RuntimeError("comp surface state is missing k_plane") coloring = str(state.get("coloring", constant.Coloring.GEOMETRIC.value)).lower() copy_coloring = coloring if coloring != "vector" else None threshold_range = self.threshold_range payload: dict[str, object] = { "grid_index": self.grid_index, "plane": self.plane, "i_plane": _range_from_axis_state(cast(dict[str, object], i_state)), "j_plane": _range_from_axis_state(cast(dict[str, object], j_state)), "k_plane": _range_from_axis_state(cast(dict[str, object], k_state)), "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(), "I_inc": self.I_inc, "J_inc": self.J_inc, "K_inc": self.K_inc, } if copy_coloring is not None: payload["coloring"] = copy_coloring if coloring == constant.Coloring.SCALAR.value: payload["scalar_minmax"] = self.scalar_minmax.copy() payload["colormap"] = self.colormap.copy() legend = self.legend.copy() if bool(legend.show): payload["legend"] = legend return payload
[docs] def copy(self) -> "Comp": """Return a new comp 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"), ... ) >>> cs = fv.vis.create_comp(ds) >>> copy_obj = cs.copy() """ dataset = self._require_dataset() kwargs = self._copy_create_kwargs() return create_comp( dataset, grid_index=cast(int | None, kwargs.get("grid_index")), plane=cast(constant.Plane | str | None, kwargs.get("plane")), i_plane=cast(RangedValue | None, kwargs.get("i_plane")), j_plane=cast(RangedValue | None, kwargs.get("j_plane")), k_plane=cast(RangedValue | None, kwargs.get("k_plane")), coloring=cast(constant.Coloring | str | None, kwargs.get("coloring")), 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")), I_inc=cast(int | None, kwargs.get("I_inc")), J_inc=cast(int | None, kwargs.get("J_inc")), K_inc=cast(int | None, kwargs.get("K_inc")), )
[docs] def delete(self) -> None: """Delete the comp 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"), ... ) >>> cs = fv.vis.create_comp(ds) >>> cs.delete() """ self._delete_surface(_core.comp_surf_delete)
def _axis_range_payload(value: object, label: str) -> dict[str, object]: if isinstance(value, RangedValue): payload = value.to_payload() else: raise InvalidArgumentError(f"{label} must be a RangedValue") out: dict[str, object] = {} for key, val in payload.items(): out[key] = _to_index(val, f"{label}.{key}") return out
[docs] def create_comp( dataset: Dataset | None = None, *, grid: int | None = None, grid_index: int | None = None, plane: constant.Plane | str | None = None, i_plane: RangedValue | None = None, j_plane: RangedValue | None = None, k_plane: RangedValue | None = None, coloring: constant.Coloring | 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, I_inc: int | None = None, J_inc: int | None = None, K_inc: int | None = None, ) -> Comp: """Create a computational surface for the given dataset. Args: dataset: Dataset to attach the new comp surface to. When omitted, the current dataset is used. grid: Optional 1-based dataset grid number. grid_index: Optional 0-based dataset grid index. plane: Active plane selection. i_plane: Optional I-plane ranged-value controller. j_plane: Optional J-plane ranged-value controller. k_plane: Optional K-plane ranged-value controller. coloring: Coloring mode for the comp surface. 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 comp 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. I_inc: Sweep increment for the I axis. J_inc: Sweep increment for the J axis. K_inc: Sweep increment for the K axis. ``grid`` and ``grid_index`` are interchangeable aliases for selecting the target dataset grid. ``grid`` is 1-based to match the UI, while ``grid_index`` is 0-based for Python-style indexing. When both are provided, they must refer to the same grid. If neither is provided, the comp surface is created on grid 1. 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"), ... ) >>> k_plane = fv.RangedValue(range=fv.Range(min=1, max=30), value=10) >>> cs = fv.vis.create_comp(ds, grid_index=0, plane=fv.constant.Plane.K, k_plane=k_plane) """ dataset = _resolve_dataset_or_current(dataset) resolved_grid_index = _resolve_grid_selection( grid=grid, grid_index=grid_index, default_grid_index=0 ) resolved_grid = (resolved_grid_index if resolved_grid_index is not None else 0) + 1 has_modifications = any( value is not None for value in ( plane, i_plane, j_plane, k_plane, coloring, 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, I_inc, J_inc, K_inc, ) ) phigs_obj = _core_call( _core.comp_surf_create, dataset.dataset_id, resolved_grid, has_modifications ) comp = Comp(phigs_obj, dataset.dataset_id, dataset) if has_modifications: try: comp.modify( plane=plane, i_plane=i_plane, j_plane=j_plane, k_plane=k_plane, coloring=coloring, 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, I_inc=I_inc, J_inc=J_inc, K_inc=K_inc, ) except Exception: _core_call(_core.comp_surf_delete, phigs_obj) raise return comp