Source code for fieldview.vis.coord

"""Coordinate 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_coord(ds)
"""

from __future__ import annotations

import os
from collections.abc import Mapping
from enum import Enum
from typing import cast
from collections.abc import Callable

from .. import _core
from .. import constant
from .._core_utils import _core_call
from .._base import (
    SurfaceBase,
    _prefix_payload,
    _surface_coloring_mode,
)
from ..exceptions import (
    InvalidArgumentError,
    InvalidDatasetError,
)
from ..data import (
    Dataset,
    IntegrationResult,
    _integrate_partial_surface,
    _resolve_dataset_or_current,
)
from ..utils import (
    Legend,
    Range,
    RangedValue,
    RuledGridAxisOptions,
    RuledGridOptions,
    ScalarAnnotation,
    Colormap,
    ScalarMinMax,
    VectorOptions,
    _coerce_float,
    _coerce_int,
    _coerce_pathlike_str,
    _bind_ranged_value,
    _normalize_ranged_value_from_values,
)

__all__: list[str] = ["Coord", "create_coord"]


def _range_from_axis_state(state: dict[str, object]) -> RangedValue:
    return _normalize_ranged_value_from_values(
        min_value=_coerce_float(state["min"]),
        max_value=_coerce_float(state["max"]),
        abs_min=None
        if state.get("abs_min") is None
        else _coerce_float(state["abs_min"]),
        abs_max=None
        if state.get("abs_max") is None
        else _coerce_float(state["abs_max"]),
        current=_coerce_float(state["current"]),
    )


def _make_plane_range(parent: "Coord", 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 InvalidArgumentError(
            f"{axis} plane is not valid for this coordinate surface"
        )

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

    return _bind_ranged_value(
        get_state=get_state,
        set_state=set_state,
        cast=_coerce_float,
    )


[docs] class Coord(SurfaceBase): """Coordinate 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_coord(ds) >>> cs.plane = fv.constant.Plane.X >>> cs.x_plane.value = 2.0 >>> cs.coloring = fv.constant.Coloring.SCALAR >>> cs.transparency = 0.5 >>> cs.colormap.name = fv.constant.ColormapName.SPECTRUM """ _core_prefix = "coord_surf" __slots__ = ("_dataset", "_x_plane", "_y_plane", "_z_plane", "_r_plane", "_t_plane") def __init__( self, phigs_obj: int, dataset_id: int, dataset: Dataset | None = None ) -> None: """Create a coordinate-surface wrapper around an existing host object. Args: phigs_obj: Host PHIGS object identifier for the coord 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_plane = _make_plane_range(self, "X") self._y_plane = _make_plane_range(self, "Y") self._z_plane = _make_plane_range(self, "Z") self._r_plane = _make_plane_range(self, "R") self._t_plane = _make_plane_range(self, "T") def _require_dataset(self) -> Dataset: dataset = self._dataset if dataset is None: raise InvalidDatasetError("coord 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}")
[docs] def integrate_partial_surface( self, point: object, tolerance: float | None = None, ) -> IntegrationResult | None: """Integrate the connected partial region containing *point*. Returns ``None`` when *point* does not isolate a partial region on the current coordinate surface. Args: point: XYZ point used to identify the connected partial region. tolerance: Optional coordinate tolerance used while locating the partial region. 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"), ... ) >>> probe = fv.data.probe_ijk((2, 3, 4), grid=1, dataset=ds) >>> cs = fv.vis.create_coord(ds) >>> cs.scalar_func = probe.scalar.func >>> cs.plane = fv.constant.Plane.X >>> cs.x_plane.value = probe.point.x >>> result = cs.integrate_partial_surface(tuple(probe.point)) """ return _integrate_partial_surface(self, point, tolerance)
@property def plane(self) -> constant.Plane: """`Plane`: Active plane (x/y/z/r/t). 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_coord(ds) >>> cs.plane = fv.constant.Plane.Y """ return constant.Plane(str(self._state().get("axis", "x")).lower()) @plane.setter def plane(self, value: constant.Plane | str) -> None: """Set the active plane (x, y, z, r, t).""" axis = value.value if isinstance(value, constant.Plane) else str(value) _core_call(_core.coord_surf_set_axis, self._phigs_obj, axis) @property def x_plane(self) -> RangedValue: """`RangedValue`: X plane 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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.x_plane.value = 2.5 >>> cs.x_plane.range.min = -1.0 >>> cs.x_plane.range.max = 3.0 """ return self._x_plane @property def y_plane(self) -> RangedValue: """`RangedValue`: Y plane 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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.y_plane.value = 0.0 """ return self._y_plane @property def z_plane(self) -> RangedValue: """`RangedValue`: Z plane 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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.z_plane.value = 1.0 """ return self._z_plane @property def r_plane(self) -> RangedValue: """`RangedValue`: R plane range controller (cylindrical datasets). Example usage: .. code-block:: python >>> import os >>> import fieldview as fv >>> dataset_path = os.path.join( ... fv.home, "examples", "heat_exchanger", "cocurrent.uns" ... ) >>> ds = fv.data.load_fvuns(dataset_path) >>> cs = fv.vis.create_coord(ds, plane=fv.constant.Plane.R) >>> cs.r_plane.value = 1.0 """ return self._r_plane @property def t_plane(self) -> RangedValue: """`RangedValue`: T plane range controller (cylindrical datasets). Example usage: .. code-block:: python >>> import os >>> import fieldview as fv >>> dataset_path = os.path.join( ... fv.home, "examples", "heat_exchanger", "cocurrent.uns" ... ) >>> ds = fv.data.load_fvuns(dataset_path) >>> cs = fv.vis.create_coord(ds, plane=fv.constant.Plane.T) >>> cs.t_plane.value = 15.0 """ return self._t_plane @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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.scalar_func = "Density (Q1)" """ name = self._state().get("scalar_func", "") return str(name) if name else None @scalar_func.setter def scalar_func(self, value: str) -> None: """Set the scalar function by name (case-insensitive).""" dataset = self._require_dataset() func_id = self._lookup_function_id( value, dataset._scalar_function_ids, "scalar" ) _core_call(_core.coord_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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.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: """Set the vector function by name (case-insensitive).""" dataset = self._require_dataset() func_id = self._lookup_function_id( value, dataset._vector_function_ids, "vector" ) _core_call(_core.coord_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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.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: """Set the threshold function by name (case-insensitive).""" dataset = self._require_dataset() func_id = self._lookup_function_id( value, dataset._scalar_function_ids, "threshold" ) _core_call(_core.coord_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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.threshold = True """ return bool(self._state().get("threshold", False)) @threshold.setter def threshold(self, value: bool) -> None: """Enable or disable thresholding (requires a threshold function).""" _core_call(_core.coord_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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.threshold_range = fv.Range(0.5, 1.0) """ state = self._state() return Range( _coerce_float(state.get("threshold_min", 0.0)), _coerce_float(state.get("threshold_max", 0.0)), ) @threshold_range.setter def threshold_range(self, value: Range) -> None: """Set the threshold range (min <= max).""" 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.coord_surf_set_threshold_range, self._phigs_obj, float(value.min), float(value.max), ) @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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.transparency = 0.5 """ return _coerce_float(self._state().get("transparency", 0.0)) @transparency.setter def transparency(self, value: float) -> None: """Set the surface transparency (0.0-1.0).""" _core_call(_core.coord_surf_set_transparency, self._phigs_obj, float(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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.contours = fv.constant.ContourColoring.BLACK """ return constant.ContourColoring( str( self._state().get("contours", constant.ContourColoring.NONE.value) ).lower() ) @contours.setter def contours(self, value: constant.ContourColoring | str) -> None: """Set the contour coloring mode.""" contours = ( value.value if isinstance(value, constant.ContourColoring) else str(value) ) _core_call(_core.coord_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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.show_mesh = True """ return bool(self._state().get("show_mesh", False)) @show_mesh.setter def show_mesh(self, value: bool) -> None: """Enable or disable mesh overlay without changing display type.""" _core_call(_core.coord_surf_set_show_mesh, self._phigs_obj, bool(value)) @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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.vector_options.head_type = fv.constant.VectorHeadType.HEAD3D >>> cs.vector_options.shaft_type = fv.constant.VectorShaftType.CURVED """ return self._get_vector_options() @vector_options.setter def vector_options(self, value: VectorOptions | dict[str, object]) -> None: """Set vector display options in bulk.""" self._set_vector_options(value)
[docs] def sweep( self, cycles: int = 1, output_file: str | os.PathLike[str] | None = None, direction: constant.SweepDirection | None = None, ) -> None: """Sweep the coordinate 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_coord(ds) >>> cs.sweep(2, "/tmp/out.mp4", fv.constant.SweepDirection.BOUNCE) """ 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.coord_surf_sweep, self._phigs_obj, cycles) else: _core_call( _core.coord_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.coord_surf_sweep, self._phigs_obj, cycles, output_file) else: _core_call( _core.coord_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 coordinate surface to a text/MAT/CSV file. Args: filename: Destination export path. format: Export format string. Supported values are ``txt``, ``mat``, and ``csv``. 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_coord(ds) >>> cs.export("/tmp/coord_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") _core_call(_core.coord_surf_export, self._phigs_obj, filename, fmt)
@property def sweep_steps(self) -> int: """`int`: Sweep step count. 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_coord(ds) >>> cs.sweep_steps = 50 """ steps = self._state().get("sweep_steps") if steps is None: return 25 if isinstance(steps, int) and not isinstance(steps, bool): return steps raise RuntimeError("coord surface state has invalid sweep_steps") @sweep_steps.setter def sweep_steps(self, value: int) -> None: """Set the sweep step count (1-999).""" steps = int(value) if steps < 1 or steps > 999: raise InvalidArgumentError("sweep_steps must be in range 1-999") _core_call(_core.coord_surf_set_sweep_steps, self._phigs_obj, steps)
[docs] def modify( self, *, plane: constant.Plane | str | None = None, x_plane: RangedValue | None = None, y_plane: RangedValue | None = None, z_plane: RangedValue | None = None, r_plane: RangedValue | None = None, t_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, ruled_grid: RuledGridOptions | None = None, scalar_minmax: ScalarMinMax | None = None, colormap: Colormap | None = None, legend: Legend | None = None, sweep_steps: int | None = None, ) -> None: """Modify multiple coord properties atomically. Args: plane: Active plane selection. x_plane: Optional X-plane ranged-value controller. y_plane: Optional Y-plane ranged-value controller. z_plane: Optional Z-plane ranged-value controller. r_plane: Optional R-plane ranged-value controller. t_plane: Optional T-plane ranged-value controller. coloring: Coloring mode for the coord 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 coord 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. ruled_grid: Ruled-grid display options. scalar_minmax: Scalar min/max annotation options. colormap: Scalar colormap options. legend: Legend options. sweep_steps: Sweep step count. 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_coord(ds) >>> cs.modify(coloring=fv.constant.Coloring.SCALAR, transparency=0.5) """ kwargs = { "plane": plane, "x_plane": x_plane, "y_plane": y_plane, "z_plane": z_plane, "r_plane": r_plane, "t_plane": t_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, "ruled_grid": ruled_grid, "scalar_minmax": scalar_minmax, "colormap": colormap, "legend": legend, "sweep_steps": sweep_steps, } 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.coord_surf_modify, self._phigs_obj, payload)
def _build_modify_payload(self, kwargs: Mapping[str, object]) -> dict[str, object]: payload: dict[str, object] = {} plane = kwargs.get("plane") if plane is not None: payload["axis"] = ( plane.value if isinstance(plane, constant.Plane) else str(plane) ) axis_ranges = [ ("x_plane", "x_"), ("y_plane", "y_"), ("z_plane", "z_"), ("r_plane", "r_"), ("t_plane", "t_"), ] 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()} ) ) coloring = kwargs.get("coloring") if coloring is not None: mode = _surface_coloring_mode( cast(constant.Coloring | str, coloring), surface_name="coord 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: 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 not isinstance(vector_options, VectorOptions): raise InvalidArgumentError("vector_options must be a VectorOptions") payload.update( _prefix_payload( "vector_", _flatten_vector_options(vector_options.to_payload()), ) ) ruled_grid = kwargs.get("ruled_grid") if ruled_grid is not None: if not isinstance(ruled_grid, RuledGridOptions): raise InvalidArgumentError("ruled_grid must be a RuledGridOptions") payload["ruled_grid"] = bool(ruled_grid.show) payload.update(_prefix_payload("ruled_grid_", ruled_grid.to_payload())) scalar_minmax = kwargs.get("scalar_minmax") if scalar_minmax is not None: if not isinstance(scalar_minmax, ScalarMinMax): raise InvalidArgumentError("scalar_minmax must be a ScalarMinMax") payload["scalar_minmax"] = bool(scalar_minmax.show) payload.update( _prefix_payload("scalar_minmax_", scalar_minmax.to_payload()) ) colormap = kwargs.get("colormap") if colormap is not None: if not isinstance(colormap, Colormap): raise InvalidArgumentError("colormap must be a Colormap") payload.update(_prefix_payload("scalar_colormap_", colormap.to_payload())) legend = kwargs.get("legend") if legend is not None: if not isinstance(legend, Legend): raise InvalidArgumentError("legend must be a Legend") payload.update(_prefix_payload("legend_", legend.to_payload())) sweep_steps = kwargs.get("sweep_steps") if sweep_steps is not None: if not isinstance(sweep_steps, int): raise InvalidArgumentError("sweep_steps must be an int") steps = int(sweep_steps) if steps < 1 or steps > 999: raise InvalidArgumentError("sweep_steps must be in range 1-999") payload["sweep_steps"] = steps if not payload: raise InvalidArgumentError("modify requires at least one property") return payload @property def ruled_grid(self) -> RuledGridOptions: """`RuledGridOptions`: Ruled grid 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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.ruled_grid.show = True >>> cs.ruled_grid.color = fv.constant.GeometricColor.RED """ state = self._state() show = bool(state.get("ruled_grid", False)) payload = state.get("ruled_grid_options", {}) if not isinstance(payload, dict): raise RuntimeError("coord surface state has invalid ruled_grid_options") options = RuledGridOptions.from_payload( cast(dict[str, object], payload), show=show ) return _bind_ruled_grid_options(options, self._phigs_obj) @ruled_grid.setter def ruled_grid(self, value: RuledGridOptions | dict[str, object]) -> None: """Set ruled grid options.""" payload: dict[str, object] show: bool | None if isinstance(value, RuledGridOptions): payload = value.to_payload() show = value.show elif isinstance(value, dict): payload, show = _flatten_ruled_grid_options(value) else: raise InvalidArgumentError("ruled_grid must be a RuledGridOptions or dict") if show is not None: _core_call(_core.coord_surf_set_ruled_grid, self._phigs_obj, bool(show)) if payload: _core_call( _core.coord_surf_set_ruled_grid_options, self._phigs_obj, payload ) @property def scalar_minmax(self) -> ScalarMinMax: """`ScalarMinMax`: Scalar min/max 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"), ... ) >>> cs = fv.vis.create_coord(ds) >>> cs.scalar_minmax.show = True >>> cs.scalar_minmax.max.text = "Max: %%SCALAR_MAX" """ return self._get_scalar_minmax() @scalar_minmax.setter def scalar_minmax(self, value: ScalarMinMax | dict[str, object]) -> None: """Set scalar min/max options.""" self._set_scalar_minmax(value) def _get_scalar_minmax(self) -> ScalarMinMax: state = self._state() payload = state.get("scalar_minmax_options", {}) if not isinstance(payload, dict): raise RuntimeError("coord surface state has invalid scalar_minmax_options") show_state = state.get("scalar_minmax", False) if isinstance(show_state, Mapping): show_map = cast(Mapping[object, object], show_state) show = bool(show_map.get("enabled", show_map.get("show", False))) else: show = bool(show_state) options = ScalarMinMax.from_payload(cast(dict[str, object], payload), show=show) phigs_obj = self._phigs_obj def get_data() -> dict[str, object]: data = self._state().get("scalar_minmax_data") if isinstance(data, dict): return cast(dict[str, object], data) raise InvalidArgumentError( "ScalarMinMax data is not available for this coordinate surface" ) return _bind_scalar_minmax(options, phigs_obj, get_data) def _copy_create_kwargs(self) -> dict[str, object]: def axis_payload_or_none(axis: str, active_axis: str) -> RangedValue | None: plane_state = state.get(f"{axis.lower()}_plane") if not isinstance(plane_state, dict): return None plane_payload = cast(dict[str, object], plane_state) normalized = _normalize_ranged_value_from_values( min_value=_coerce_float(plane_payload["min"]), max_value=_coerce_float(plane_payload["max"]), abs_min=None if plane_payload.get("abs_min") is None else _coerce_float(plane_payload["abs_min"]), abs_max=None if plane_payload.get("abs_max") is None else _coerce_float(plane_payload["abs_max"]), current=_coerce_float(plane_payload["current"]), ) abs_min = _coerce_float(normalized.range.abs_min) abs_max = _coerce_float(normalized.range.abs_max) min_value = _coerce_float(normalized.range.min) max_value = _coerce_float(normalized.range.max) current_value = _coerce_float(normalized.value) tol = 1e-6 min_is_default = abs(min_value - abs_min) <= tol max_is_default = abs(max_value - abs_max) <= tol mid_value = 0.5 * (min_value + max_value) current_is_default = abs(current_value - mid_value) <= tol kwargs: dict[str, float] = {} if not min_is_default: kwargs["min"] = min_value if not max_is_default: kwargs["max"] = max_value selected_value: float | None = None if axis == active_axis and not current_is_default: selected_value = current_value if not kwargs and selected_value is None: return None return RangedValue(range=Range(**kwargs), value=selected_value) plane = self.plane active_axis = plane.value.upper() state = self._state() sweep_steps_state = state.get("sweep_steps") sweep_steps = ( _coerce_int(sweep_steps_state) if isinstance(sweep_steps_state, int) and not isinstance(sweep_steps_state, bool) else None ) coloring = str(state.get("coloring", constant.Coloring.GEOMETRIC.value)).lower() display_type = self.display_type copy_coloring = coloring if coloring != "vector" else None threshold_range = self.threshold_range payload: dict[str, object] = { "plane": plane, "geometric_color": self.geometric_color, "display_type": 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(), "sweep_steps": sweep_steps, } if copy_coloring is not None: payload["coloring"] = copy_coloring ruled_grid = self.ruled_grid.copy() if bool(ruled_grid.show): payload["ruled_grid"] = ruled_grid if copy_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 axis_map = { "x_plane": "X", "y_plane": "Y", "z_plane": "Z", "r_plane": "R", "t_plane": "T", } for key, axis in axis_map.items(): axis_payload = axis_payload_or_none(axis, active_axis) if axis_payload is not None: payload[key] = axis_payload return payload
[docs] def copy(self) -> "Coord": """Return a new coord 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_coord(ds) >>> copy_obj = cs.copy() """ dataset = self._require_dataset() kwargs = self._copy_create_kwargs() return create_coord( dataset, plane=cast(constant.Plane | str | None, kwargs.get("plane")), x_plane=cast(RangedValue | None, kwargs.get("x_plane")), y_plane=cast(RangedValue | None, kwargs.get("y_plane")), z_plane=cast(RangedValue | None, kwargs.get("z_plane")), r_plane=cast(RangedValue | None, kwargs.get("r_plane")), t_plane=cast(RangedValue | None, kwargs.get("t_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")), ruled_grid=cast(RuledGridOptions | None, kwargs.get("ruled_grid")), scalar_minmax=cast(ScalarMinMax | None, kwargs.get("scalar_minmax")), colormap=cast(Colormap | None, kwargs.get("colormap")), legend=cast(Legend | None, kwargs.get("legend")), sweep_steps=cast(int | None, kwargs.get("sweep_steps")), )
[docs] def delete(self) -> None: """Delete this coord 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_coord(ds) >>> cs.delete() """ self._delete_surface(_core.coord_surf_delete)
def _bind_scalar_annotation( annotation: ScalarAnnotation | None, on_change: Callable[[str, object], None], prefix: str, ) -> ScalarAnnotation: if annotation is None: annotation = ScalarAnnotation() object.__setattr__(annotation, "_on_change", on_change) object.__setattr__(annotation, "_prefix", prefix) return annotation def _bind_ruled_grid_axis_options( axis: RuledGridAxisOptions | None, on_change: Callable[[str, object], None], prefix: str, ) -> RuledGridAxisOptions: if axis is None: axis = RuledGridAxisOptions() object.__setattr__(axis, "_on_change", on_change) object.__setattr__(axis, "_prefix", prefix) return axis def _bind_ruled_grid_options( options: RuledGridOptions, phigs_obj: int ) -> RuledGridOptions: def on_change(key: str, value: object) -> None: if value is None: return if key == "show": _core_call(_core.coord_surf_set_ruled_grid, phigs_obj, bool(value)) else: _core_call(_core.coord_surf_set_ruled_grid_options, phigs_obj, {key: value}) object.__setattr__(options, "_on_change", on_change) horiz = _bind_ruled_grid_axis_options(options.horizontal_axis, on_change, "horiz") vert = _bind_ruled_grid_axis_options(options.vertical_axis, on_change, "vert") object.__setattr__(options, "horizontal_axis", horiz) object.__setattr__(options, "vertical_axis", vert) return options def _bind_scalar_minmax( options: ScalarMinMax, phigs_obj: int, get_data: Callable[[], dict[str, object]], ) -> ScalarMinMax: def on_change(key: str, value: object) -> None: if value is None: return if key == "show": _core_call(_core.coord_surf_set_scalar_minmax, phigs_obj, bool(value)) else: _core_call( _core.coord_surf_set_scalar_minmax_options, phigs_obj, {key: value} ) object.__setattr__(options, "_on_change", on_change) object.__setattr__(options, "_get_data", get_data) min_annotation = _bind_scalar_annotation(options.min, on_change, "min") max_annotation = _bind_scalar_annotation(options.max, on_change, "max") object.__setattr__(options, "min", min_annotation) object.__setattr__(options, "max", max_annotation) return options def _flatten_ruled_grid_options( options: dict[str, object], ) -> tuple[dict[str, object], bool | None]: payload: dict[str, object] = dict(options) show = cast(bool | None, payload.pop("show", None)) horiz = payload.pop("horizontal_axis", None) vert = payload.pop("vertical_axis", None) if horiz is not None: horiz_dict = ( dict(cast(dict[str, object], horiz)) if isinstance(horiz, dict) else None ) labels_params = ( horiz_dict.pop("labels_parameters", None) if horiz_dict is not None else None ) if horiz_dict is not None and isinstance(labels_params, dict): labels_map = cast(dict[str, object], labels_params) horiz_dict.setdefault( "numerical_format", labels_map.get("numerical_format") ) horiz_dict.setdefault("decimal_places", labels_map.get("decimal_places")) if horiz_dict is not None: for key, value in horiz_dict.items(): if key == "label": continue payload[f"horiz_{key}"] = value if vert is not None: vert_dict = ( dict(cast(dict[str, object], vert)) if isinstance(vert, dict) else None ) labels_params = ( vert_dict.pop("labels_parameters", None) if vert_dict is not None else None ) if vert_dict is not None and isinstance(labels_params, dict): labels_map = cast(dict[str, object], labels_params) vert_dict.setdefault("numerical_format", labels_map.get("numerical_format")) vert_dict.setdefault("decimal_places", labels_map.get("decimal_places")) if vert_dict is not None: for key, value in vert_dict.items(): if key == "label": continue payload[f"vert_{key}"] = value return payload, show def _flatten_scalar_minmax( options: dict[str, object], ) -> tuple[dict[str, object], bool | None]: payload: dict[str, object] = dict(options) show = cast(bool | None, payload.pop("show", None)) min_opts = payload.pop("min", None) max_opts = payload.pop("max", None) if isinstance(min_opts, ScalarAnnotation): payload.update(min_opts.to_payload("min")) elif isinstance(min_opts, dict): for key, value in cast(dict[str, object], min_opts).items(): payload[f"min_{key}"] = value if isinstance(max_opts, ScalarAnnotation): payload.update(max_opts.to_payload("max")) elif isinstance(max_opts, dict): for key, value in cast(dict[str, object], max_opts).items(): payload[f"max_{key}"] = value return payload, show def _flatten_vector_options(options: dict[str, object]) -> dict[str, object]: payload: dict[str, object] = {} for key, value in options.items(): if value is None: continue if isinstance(value, Enum): payload[key] = cast(object, value.value) else: payload[key] = value if "r_samples" in payload: if "x_samples" in payload: raise InvalidArgumentError("r_samples conflicts with x_samples") payload["x_samples"] = payload.pop("r_samples") if "t_samples" in payload: if "y_samples" in payload: raise InvalidArgumentError("t_samples conflicts with y_samples") payload["y_samples"] = payload.pop("t_samples") return payload def _axis_range_payload(value: object, label: str) -> dict[str, object]: if isinstance(value, RangedValue): return {key: item for key, item in value.to_payload().items()} raise InvalidArgumentError(f"{label} must be a RangedValue")
[docs] def create_coord( dataset: Dataset | None = None, *, plane: constant.Plane | str | None = None, x_plane: RangedValue | None = None, y_plane: RangedValue | None = None, z_plane: RangedValue | None = None, r_plane: RangedValue | None = None, t_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, ruled_grid: RuledGridOptions | None = None, scalar_minmax: ScalarMinMax | None = None, colormap: Colormap | None = None, legend: Legend | None = None, sweep_steps: int | None = None, ) -> Coord: """Create a coordinate surface for the given dataset. Args: dataset: Dataset to attach the new coord surface to. When omitted, the current dataset is used. plane: Active plane selection. x_plane: Optional X-plane ranged-value controller. y_plane: Optional Y-plane ranged-value controller. z_plane: Optional Z-plane ranged-value controller. r_plane: Optional R-plane ranged-value controller. t_plane: Optional T-plane ranged-value controller. coloring: Coloring mode for the coord 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 coord 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. ruled_grid: Ruled-grid display options. scalar_minmax: Scalar min/max annotation options. colormap: Scalar colormap options. legend: Legend options. sweep_steps: Sweep step count. 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"), ... ) >>> x_plane = fv.RangedValue(range=fv.Range(min=1.0, max=50.0), value=10.0) >>> cs = fv.vis.create_coord(ds, plane=fv.constant.Plane.X, x_plane=x_plane) """ dataset = _resolve_dataset_or_current(dataset) has_modifications = any( value is not None for value in ( plane, x_plane, y_plane, z_plane, r_plane, t_plane, coloring, geometric_color, display_type, line_type, contours, show_mesh, visibility, transparency, scalar_func, vector_func, threshold, threshold_func, threshold_range, vector_options, ruled_grid, scalar_minmax, colormap, legend, sweep_steps, ) ) phigs_obj = _core_call( _core.coord_surf_create, dataset.dataset_id, has_modifications ) coord = Coord(phigs_obj, dataset.dataset_id, dataset) if has_modifications: try: coord.modify( plane=plane, x_plane=x_plane, y_plane=y_plane, z_plane=z_plane, r_plane=r_plane, t_plane=t_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, ruled_grid=ruled_grid, scalar_minmax=scalar_minmax, colormap=colormap, legend=legend, sweep_steps=sweep_steps, ) except Exception: _core_call(_core.coord_surf_delete, phigs_obj) raise return coord