Source code for fieldview.vis.streamlines

"""Streamlines 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"),
    ... )
    >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0])
    >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL
    >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1)
    >>> sl.calculate()
"""

from __future__ import annotations

import os
from typing import cast
from collections.abc import Sequence

from .. import _core
from .. import constant
from .._core_utils import _core_call
from .._base import (
    PathBase,
    SurfaceBase,
    _flatten_colormap,
    _flatten_legend_options,
    _prefix_payload,
)
from ..data import Dataset, _resolve_dataset_or_current
from ..exceptions import InvalidArgumentError, InvalidDatasetError
from ..utils import Colormap, Legend, _coerce_float, _coerce_int, _coerce_pathlike_str

__all__: list[str] = ["StreamlinesStreak", "Streamlines", "create_streamlines"]

_UNSET = object()
_STREAMLINES_DISPLAY_TYPES: set[str] = {
    constant.PathsDisplayType.COMPLETE.value,
    constant.PathsDisplayType.FILAMENT.value,
    constant.PathsDisplayType.FILAMENT_ARROWS.value,
    constant.PathsDisplayType.FILAMENT_SPHERES.value,
    constant.PathsDisplayType.GROWING.value,
    constant.PathsDisplayType.SPHERES_AND_LINES.value,
    constant.PathsDisplayType.SPHERES.value,
    constant.PathsDisplayType.DOTS.value,
    constant.PathsDisplayType.LINES_OF_SPHERES.value,
    constant.PathsDisplayType.LINES_OF_DOTS.value,
    constant.PathsDisplayType.RIBBONS.value,
}


def _normalize_seed_coord(value: constant.StreamlinesSeedCoord | str) -> str:
    if isinstance(value, constant.StreamlinesSeedCoord):
        return value.value
    normalized = str(value).strip().lower()
    if normalized in {"ijk", "ijk_int"}:
        return constant.StreamlinesSeedCoord.IJK.value
    if normalized == constant.StreamlinesSeedCoord.IJK_REAL.value:
        return normalized
    if normalized == constant.StreamlinesSeedCoord.XYZ.value:
        return normalized
    raise InvalidArgumentError(
        "seed_coord must be StreamlinesSeedCoord.IJK, StreamlinesSeedCoord.IJK_REAL, or StreamlinesSeedCoord.XYZ"
    )


def _normalize_direction(value: constant.CalculationDirection | str) -> str:
    if isinstance(value, constant.CalculationDirection):
        return value.value
    direction = str(value).strip().lower()
    if direction in {
        constant.CalculationDirection.FORWARD.value,
        constant.CalculationDirection.BACKWARD.value,
        constant.CalculationDirection.BOTH.value,
    }:
        return direction
    raise InvalidArgumentError("direction must be forward, backward, or both")


def _normalize_animate_direction(value: constant.AnimationDirection | str) -> str:
    if isinstance(value, constant.AnimationDirection):
        return value.value
    direction = str(value).strip().lower()
    if direction in {
        constant.AnimationDirection.FORWARD.value,
        constant.AnimationDirection.BACKWARD.value,
    }:
        return direction
    raise InvalidArgumentError("animate_direction must be forward or backward")


def _normalize_streamlines_coloring(value: constant.Coloring | str) -> str:
    mode = (
        value.value
        if isinstance(value, constant.Coloring)
        else str(value).strip().lower()
    )
    if mode == constant.Coloring.MATERIAL.value:
        raise InvalidArgumentError("material coloring is not supported for streamlines")
    if mode not in {constant.Coloring.GEOMETRIC.value, constant.Coloring.SCALAR.value}:
        raise InvalidArgumentError("coloring must be geometric or scalar")
    return mode


def _normalize_line_type(value: constant.LineType | str) -> str:
    line_type = (
        value.value
        if isinstance(value, constant.LineType)
        else str(value).strip().lower()
    )
    if line_type not in {
        constant.LineType.THIN.value,
        constant.LineType.MEDIUM.value,
        constant.LineType.THICK.value,
    }:
        raise InvalidArgumentError("line_type must be thin, medium, or thick")
    return line_type


def _normalize_streamlines_display_type(
    value: constant.PathsDisplayType | str,
) -> str:
    display_type = (
        value.value if isinstance(value, constant.PathsDisplayType) else str(value)
    )
    normalized = display_type.strip().lower()
    if normalized in _STREAMLINES_DISPLAY_TYPES:
        return normalized
    allowed = ", ".join(sorted(_STREAMLINES_DISPLAY_TYPES))
    raise InvalidArgumentError(f"display_type must be one of: {allowed}")


def _to_seed_triplet(
    seed: Sequence[int | float], *, integral: bool
) -> list[int | float]:
    if not isinstance(seed, Sequence) or len(seed) != 3:
        raise InvalidArgumentError(
            "each seed must be a sequence of exactly 3 numeric values"
        )
    values: list[int | float] = []
    for item in seed:
        if isinstance(item, bool) or not isinstance(item, (int, float)):
            raise InvalidArgumentError("seed values must be numeric")
        values.append(int(item) if integral else float(item))
    return values


def _normalize_streak_int(value: object, label: str) -> int:
    if isinstance(value, bool):
        raise InvalidArgumentError(f"{label} must be an integer")
    try:
        return _coerce_int(value)
    except (TypeError, ValueError) as exc:
        raise InvalidArgumentError(f"{label} must be an integer") from exc


def _validate_streak_pair(release_interval: int, duration: int) -> tuple[int, int]:
    if release_interval <= 0:
        raise InvalidArgumentError("release_interval must be > 0")
    if duration <= 0 or duration >= release_interval:
        raise InvalidArgumentError("duration must be > 0 and < release_interval")
    return release_interval, duration


def _resolve_streak_pair(
    *,
    current_release_interval: int,
    current_duration: int,
    release_interval: object = _UNSET,
    duration: object = _UNSET,
) -> tuple[int | None, int | None]:
    if release_interval is _UNSET and duration is _UNSET:
        return None, None

    next_release_interval = (
        current_release_interval
        if release_interval is _UNSET
        else _normalize_streak_int(release_interval, "release_interval")
    )
    next_duration = (
        current_duration
        if duration is _UNSET
        else _normalize_streak_int(duration, "duration")
    )
    _validate_streak_pair(next_release_interval, next_duration)

    return (
        None if release_interval is _UNSET else next_release_interval,
        None if duration is _UNSET else next_duration,
    )


class StreamlinesStreak:
    """Streakline controls bound to a :class:`Streamlines` 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"),
        ... )
        >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0])
        >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL
        >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1)
        >>> sl.calculate()
        >>> streak = sl.streak
        >>> streak.duration = 2
    """

    __slots__ = ("_parent",)

    def __init__(self, parent: "Streamlines") -> None:
        """Create a convenience wrapper for streak controls.

        Args:
            parent: Owning :class:`Streamlines` object.
        """
        self._parent = parent

    @property
    def duration(self) -> int:
        """`int`: Streak duration in frames.

        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"),
            ... )
            >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0])
            >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL
            >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1)
            >>> sl.calculate()
            >>> streak = sl.streak
            >>> streak.duration = 2
        """
        return self._parent.duration

    @duration.setter
    def duration(self, value: int) -> None:
        self._parent.duration = value


[docs] class Streamlines(PathBase): """Streamlines surface wrapper. Notes: ``seed_coord`` cannot be changed while the streamline object still has seeds. Delete them first with :meth:`delete_all_seeds`. :meth:`seed_surface` requires a live ``Boundary``, ``Comp``, ``Coord``, or ``Iso`` surface from the same dataset as the streamline object. Streak controls use a single validation rule: ``release_interval > 0`` and ``0 < duration < release_interval``. Invalid values are rejected before any core update, and :meth:`copy` raises if the source object violates that contract. 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"), ... ) >>> sl = fv.vis.create_streamlines( ... ds, ... vector_func=ds.vector_functions[0], ... coloring=fv.constant.Coloring.SCALAR, ... scalar_func=ds.scalar_functions[0], ... display_type=fv.constant.PathsDisplayType.FILAMENT, ... seed_coord=fv.constant.StreamlinesSeedCoord.IJK_REAL, ... ) >>> sl.add_seeds([(1.0, 1.0, 1.0), (40.0, 20.0, 10.0)], grid=1) >>> sl.calculate() """ _core_prefix = "streamlines_surf" __slots__ = ("_dataset", "_streak") def __init__( self, phigs_obj: int, dataset_id: int, dataset: Dataset | None = None ) -> None: """Create a streamlines wrapper around an existing host object. Args: phigs_obj: Host PHIGS object identifier for the streamlines 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._streak = StreamlinesStreak(self) def _state(self, *, include_seeds: bool = False) -> dict[str, object]: return _core_call( _core.streamlines_surf_get_state, self._phigs_obj, bool(include_seeds) ) def _current_streak_pair(self) -> tuple[int, int]: state = self._state() release_interval = _normalize_streak_int( state.get("release_interval", 3), "release_interval" ) duration = _normalize_streak_int(state.get("duration", 1), "duration") return release_interval, duration def _require_dataset(self) -> Dataset: dataset = self._dataset if dataset is None: raise InvalidDatasetError( "streamlines 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 display_type(self) -> constant.PathsDisplayType: """`PathsDisplayType`: Path display style. Example usage: .. code-block:: python >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.display_type = fv.constant.PathsDisplayType.FILAMENT """ return constant.PathsDisplayType( str( self._state().get( "display_type", constant.PathsDisplayType.COMPLETE.value ) ).lower() ) @display_type.setter def display_type(self, value: constant.PathsDisplayType | str) -> None: self._modify({"display_type": _normalize_streamlines_display_type(value)}) def _seed_entries_from_state( self, state: dict[str, object] | None = None ) -> list[dict[str, object]]: src = self._state(include_seeds=True) if state is None else state entries = src.get("seeds", []) if not isinstance(entries, list): return [] return [ dict(cast(dict[str, object], entry)) for entry in cast(list[object], entries) if isinstance(entry, dict) ] def _seed_payload_from_entries( self, entries: Sequence[dict[str, object]], seed_coord: str, ) -> tuple[list[list[int | float]], list[int]]: coords: list[list[int | float]] = [] grids: list[int] = [] for entry in entries: if seed_coord == constant.StreamlinesSeedCoord.XYZ.value: xyz = cast(Sequence[int | float], entry.get("xyz", [])) coords.append(_to_seed_triplet(xyz, integral=False)) elif seed_coord == constant.StreamlinesSeedCoord.IJK.value: ijk = cast(Sequence[int | float], entry.get("ijk", [])) coords.append(_to_seed_triplet(ijk, integral=True)) grids.append(_coerce_int(entry.get("grid", 1))) else: ijk = cast(Sequence[int | float], entry.get("ijk", [])) coords.append(_to_seed_triplet(ijk, integral=False)) grids.append(_coerce_int(entry.get("grid", 1))) return coords, grids def _rebuild_with_seed_entries( self, state: dict[str, object], seed_entries: Sequence[dict[str, object]], *, seed_coord: str, ) -> None: dataset = self._require_dataset() vector_func_name = str(state.get("vector_func_name", "")).strip() if not vector_func_name: raise InvalidArgumentError( "streamlines surface has no vector_func; cannot rebuild" ) new_surf = create_streamlines( dataset, coloring=cast(constant.Coloring | str | None, state.get("coloring")), geometric_color=cast( constant.GeometricColor | int | None, state.get("geometric_color"), ), line_type=cast(constant.LineType | str | None, state.get("line_type")), display_type=cast( constant.PathsDisplayType | str | None, state.get("display_type"), ), sphere_scale=None if state.get("sphere_scale") is None else _coerce_float(state.get("sphere_scale")), ribbon_width=None if state.get("ribbon_width") is None else _coerce_int(state.get("ribbon_width")), transparency=None if state.get("transparency") is None else _coerce_float(state.get("transparency")), visibility=cast(bool | None, state.get("visibility")), scalar_func=cast(str | None, state.get("scalar_func_name") or None), vector_func=vector_func_name, animate=cast(bool | None, state.get("animate")), animate_direction=cast( constant.AnimationDirection | str | None, state.get("animate_direction"), ), animate_divs=None if state.get("animate_divs") is None else _coerce_int(state.get("animate_divs")), colormap=cast( Colormap | dict[str, object] | None, state.get("scalar_colormap"), ), legend=cast(Legend | dict[str, object] | None, state.get("legend")), defer_apply=True, calculate=False, ) try: release_interval = _normalize_streak_int( state.get("release_interval", 3), "release_interval" ) duration = _normalize_streak_int(state.get("duration", 1), "duration") release_interval, duration = _validate_streak_pair( release_interval, duration ) calc_payload: dict[str, object] = { "seed_coord": seed_coord, "show_seeds": bool(state.get("show_seeds", True)), "direction": str( state.get("direction", constant.CalculationDirection.FORWARD.value) ), "step": _coerce_int(state.get("step", 3)), "time_limit": state.get("time_limit"), "release_interval": release_interval, "duration": duration, } _core_call( _core.streamlines_surf_modify, new_surf._phigs_obj, calc_payload, ) coords, grids = self._seed_payload_from_entries(seed_entries, seed_coord) if coords: if seed_coord == constant.StreamlinesSeedCoord.XYZ.value: new_surf.add_seeds(coords) else: new_surf.add_seeds(coords, grids=grids) except Exception: new_surf.delete() raise old_phigs_obj = self._phigs_obj self._phigs_obj = new_surf._phigs_obj _core_call(_core.streamlines_surf_delete, old_phigs_obj) @property def sphere_scale(self) -> float: """`float`: Sphere/arrow size scale. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.sphere_scale = 1.25 """ return _coerce_float(self._state().get("sphere_scale", 1.0)) @sphere_scale.setter def sphere_scale(self, value: float) -> None: """Set sphere/arrow size scale.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"sphere_scale": float(value)}, ) @property def ribbon_width(self) -> int: """`int`: Ribbon width for ribbon display 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.ribbon_width = 24 """ return _coerce_int(self._state().get("ribbon_width", 32)) @ribbon_width.setter def ribbon_width(self, value: int) -> None: """Set ribbon width.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"ribbon_width": int(value)} ) @property def transparency(self) -> float: """`float`: Transparency percentage. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.transparency = 0.2 """ return _coerce_float(self._state().get("transparency", 0.0)) @transparency.setter def transparency(self, value: float) -> None: """Set transparency percentage.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"transparency": float(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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.scalar_func = "Density (Q1)" """ name = str(self._state().get("scalar_func_name", "")) return name or None @scalar_func.setter def scalar_func(self, value: str) -> None: """Set 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.streamlines_surf_modify, self._phigs_obj, {"scalar_func_id": 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.vector_func = "Momentum Vectors [PLOT3D]" """ name = str(self._state().get("vector_func_name", "")) return name or None @vector_func.setter def vector_func(self, value: str) -> None: """Set 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.streamlines_surf_modify, self._phigs_obj, {"vector_func_id": func_id} ) @property def animate(self) -> bool: """`bool`: Animate path traversal in the viewer. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.animate = True """ return bool(self._state().get("animate", False)) @animate.setter def animate(self, value: bool) -> None: """Enable or disable streamline animation.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"animate": bool(value)} ) @property def animate_direction(self) -> constant.AnimationDirection: """`AnimationDirection`: Direction of streamline animation. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.animate_direction = "forward" """ value = str( self._state().get( "animate_direction", constant.AnimationDirection.FORWARD.value ) ).lower() return constant.AnimationDirection(value) @animate_direction.setter def animate_direction(self, value: constant.AnimationDirection | str) -> None: """Set animation direction.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"animate_direction": _normalize_animate_direction(value)}, ) @property def animate_divs(self) -> int: """`int`: Number of animation divisions. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.animate_divs = 30 """ return _coerce_int(self._state().get("animate_divs", 25)) @animate_divs.setter def animate_divs(self, value: int) -> None: """Set animation divisions.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"animate_divs": int(value)} ) @property def show_seeds(self) -> bool: """`bool`: Show or hide seed points in the viewer. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.show_seeds = True """ return bool(self._state().get("show_seeds", True)) @show_seeds.setter def show_seeds(self, value: bool) -> None: """Set seed visibility.""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"show_seeds": bool(value)} ) @property def seed_coord(self) -> constant.StreamlinesSeedCoord: """`StreamlinesSeedCoord`: Coordinate system used by seed operations. Changing `seed_coord` while seeds exist is rejected by the core. Delete existing seeds first with :meth:`delete_all_seeds`. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.seed_coord """ value = str( self._state().get( "seed_coord", constant.StreamlinesSeedCoord.IJK_REAL.value ) ).lower() return constant.StreamlinesSeedCoord(value) @seed_coord.setter def seed_coord(self, value: constant.StreamlinesSeedCoord | str) -> None: """Set seed coordinate type (`ijk`, `ijk_real`, or `xyz`). Changing `seed_coord` while seeds exist is rejected by the core. Delete existing seeds first with :meth:`delete_all_seeds`. """ target = _normalize_seed_coord(value) state = self._state() current = str( state.get("seed_coord", constant.StreamlinesSeedCoord.IJK_REAL.value) ).lower() if target == current: return _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"seed_coord": target} ) @property def num_seeds(self) -> int: """`int`: Number of seeds currently assigned (read-only). 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.num_seeds """ return _coerce_int(self._state().get("num_seeds", 0)) @property def direction(self) -> constant.CalculationDirection: """`CalculationDirection`: Trace direction for calculation. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.direction = "both" """ value = str( self._state().get("direction", constant.CalculationDirection.FORWARD.value) ).lower() return constant.CalculationDirection(value) @direction.setter def direction(self, value: constant.CalculationDirection | str) -> None: """Set trace direction (`forward`, `backward`, or `both`).""" _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"direction": _normalize_direction(value)}, ) @property def step(self) -> int: """`int`: Integration step size. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.step = 3 """ return _coerce_int(self._state().get("step", 3)) @step.setter def step(self, value: int) -> None: """Set integration step size.""" _core_call(_core.streamlines_surf_modify, self._phigs_obj, {"step": int(value)}) @property def time_limit(self) -> float | None: """`float | None`: Optional time limit for tracing. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.time_limit = 0.25 """ value = self._state().get("time_limit") if value is None: return None return _coerce_float(value) @time_limit.setter def time_limit(self, value: float | None) -> None: """Set or clear (`None`) the tracing time limit.""" payload_value: float | None if value is None: payload_value = None else: payload_value = float(value) _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"time_limit": payload_value}, ) @property def release_interval(self) -> int: """`int`: Streakline release interval. Must be > 0, and the paired `duration` must remain smaller than this value. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.release_interval = 3 """ return _coerce_int(self._state().get("release_interval", 3)) @release_interval.setter def release_interval(self, value: int) -> None: """Set streakline release interval. `release_interval` must be > 0 and the current `duration` must remain smaller than the new value. """ current_release_interval, current_duration = self._current_streak_pair() validated_release_interval, _ = _resolve_streak_pair( current_release_interval=current_release_interval, current_duration=current_duration, release_interval=value, ) _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"release_interval": validated_release_interval}, ) @property def duration(self) -> int: """`int`: Streakline duration. Must be > 0 and < `release_interval`. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.duration = 1 """ return _coerce_int(self._state().get("duration", 1)) @duration.setter def duration(self, value: int) -> None: """Set streakline duration. `duration` must be > 0 and < `release_interval`. """ current_release_interval, current_duration = self._current_streak_pair() _, validated_duration = _resolve_streak_pair( current_release_interval=current_release_interval, current_duration=current_duration, duration=value, ) _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"duration": validated_duration}, ) @property def streak(self) -> StreamlinesStreak: """`StreamlinesStreak`: Convenience access to streak controls. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> streak = sl.streak """ return self._streak
[docs] def add_seeds( self, seeds: Sequence[Sequence[int | float]], *, grid: int = 1, grids: Sequence[int] | None = None, ) -> None: """Add one or more seed points. Args: seeds: Sequence of seed coordinates. Use XYZ values when ``seed_coord`` is ``xyz``; otherwise use IJK or IJK-real triplets. grid: Default grid number to apply to all seeds when the active seed coordinate mode requires grid ids. grids: Optional per-seed grid numbers. When provided, the length must match ``seeds``. Example usage: .. code-block:: python >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0), (10.0, 8.0, 6.0)], grid=1) """ if not isinstance(seeds, Sequence): raise InvalidArgumentError("seeds must be a sequence of 3-value points") if len(seeds) == 0: return seed_coord = self.seed_coord integral = seed_coord == constant.StreamlinesSeedCoord.IJK normalized_seeds = [_to_seed_triplet(seed, integral=integral) for seed in seeds] payload: dict[str, object] = {"seeds": normalized_seeds} if seed_coord != constant.StreamlinesSeedCoord.XYZ: if grids is not None: grid_values = [int(item) for item in grids] if len(grid_values) != len(normalized_seeds): raise InvalidArgumentError( "grids must have the same length as seeds" ) payload["grids"] = grid_values else: payload["grid"] = int(grid) _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"add_seeds": payload} )
[docs] def seed_surface( self, surface: SurfaceBase, *, inc: int = 3, num_seeds: int = 10, ) -> None: """Seed from another live surface (`Boundary`, `Comp`, `Coord`, or `Iso`). The surface must not be deleted and must belong to the same dataset as this streamline object. Args: surface: Source surface used to generate streamline seeds. inc: Grid-space increment used when ``seed_coord`` is ``ijk``. num_seeds: Number of seeds to distribute when ``seed_coord`` is ``ijk_real`` or ``xyz``. 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) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.seed_surface(surface=bnd) """ if not isinstance(surface, SurfaceBase): raise InvalidArgumentError( "surface must be a Boundary, Comp, Coord, or Iso surface" ) if getattr(surface, "_is_deleted", False): raise InvalidArgumentError("surface was deleted") surface_dataset_id = getattr(surface, "_dataset_id", None) if surface_dataset_id != self._dataset_id: raise InvalidArgumentError( "surface must belong to the same dataset as the streamlines object" ) phigs_obj = getattr(surface, "_phigs_obj", None) if not isinstance(phigs_obj, int): raise InvalidArgumentError("surface must be a FieldView surface object") seed_coord = self.seed_coord payload: dict[str, object] = {"surface_phigs_obj": int(phigs_obj)} if seed_coord == constant.StreamlinesSeedCoord.IJK: payload["inc"] = int(inc) else: payload["num_seeds"] = int(num_seeds) _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"seed_surface": payload} )
[docs] def delete_all_seeds(self) -> None: """Delete all seeds. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.delete_all_seeds() """ _core_call( _core.streamlines_surf_modify, self._phigs_obj, {"delete_all_seeds": True} )
[docs] def calculate(self) -> None: """Run streamline calculation using current seeds and parameters. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() """ _core_call(_core.streamlines_surf_calculate, self._phigs_obj)
[docs] def modify( self, *, coloring: constant.Coloring | str | None = None, geometric_color: constant.GeometricColor | int | None = None, line_type: constant.LineType | str | None = None, display_type: constant.PathsDisplayType | str | None = None, sphere_scale: float | None = None, ribbon_width: int | None = None, transparency: float | None = None, visibility: bool | None = None, scalar_func: str | None = None, vector_func: str | None = None, animate: bool | None = None, animate_direction: constant.AnimationDirection | str | None = None, animate_divs: int | None = None, show_seeds: bool | None = None, seed_coord: constant.StreamlinesSeedCoord | str | None = None, direction: constant.CalculationDirection | str | None = None, step: int | None = None, time_limit: float | None | object = _UNSET, release_interval: int | None = None, duration: int | None = None, colormap: Colormap | dict[str, object] | None = None, legend: Legend | dict[str, object] | None = None, calculate: bool = False, ) -> None: """Update multiple properties in one call. Calculation runs implicitly when any calculation parameter (``direction``, ``step``, ``time_limit``, ``release_interval``, ``duration``) is changed, or when ``calculate=True``. Args: coloring: Coloring mode for the streamline geometry. geometric_color: Geometric color id used when geometric coloring is active. line_type: Streamline line style. display_type: Path display type such as complete, filament, or ribbon. sphere_scale: Sphere or arrow size scale. ribbon_width: Ribbon width used by ribbon display modes. transparency: Transparency amount for the streamline geometry. visibility: Whether the streamline object is visible. scalar_func: Scalar function name used for scalar coloring. vector_func: Vector function name used for streamline integration. animate: Whether streamline animation is enabled. animate_direction: Direction used for streamline animation. animate_divs: Number of animation divisions. show_seeds: Whether seed markers are shown. seed_coord: Seed coordinate system to use for subsequent seed operations. direction: Integration direction used for calculation. step: Integration step size. time_limit: Optional tracing time limit. Pass ``None`` to clear the current limit. release_interval: Optional streakline release interval. duration: Optional streakline duration. colormap: Optional colormap settings as a :class:`Colormap` or payload dict. legend: Optional legend settings as a :class:`Legend` or payload dict. calculate: When ``True``, run streamline calculation after applying the updates. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.modify(visibility=False, show_seeds=False) `release_interval` and `duration` are validated together before any core call. Invalid combinations are rejected without partial updates. """ payload: dict[str, object] = {} if coloring is not None: payload["coloring"] = _normalize_streamlines_coloring(coloring) if geometric_color is not None: payload["geometric_color"] = int(geometric_color) if line_type is not None: payload["line_type"] = _normalize_line_type(line_type) if display_type is not None: payload["display_type"] = _normalize_streamlines_display_type(display_type) if sphere_scale is not None: payload["sphere_scale"] = float(sphere_scale) if ribbon_width is not None: payload["ribbon_width"] = int(ribbon_width) if transparency is not None: payload["transparency"] = float(transparency) if visibility is not None: payload["visibility"] = bool(visibility) if scalar_func is not None: dataset = self._require_dataset() payload["scalar_func_id"] = self._lookup_function_id( scalar_func, dataset._scalar_function_ids, "scalar" ) if vector_func is not None: dataset = self._require_dataset() payload["vector_func_id"] = self._lookup_function_id( vector_func, dataset._vector_function_ids, "vector" ) if animate is not None: payload["animate"] = bool(animate) if animate_direction is not None: payload["animate_direction"] = _normalize_animate_direction( animate_direction ) if animate_divs is not None: payload["animate_divs"] = int(animate_divs) if show_seeds is not None: payload["show_seeds"] = bool(show_seeds) if seed_coord is not None: payload["seed_coord"] = _normalize_seed_coord(seed_coord) if direction is not None: payload["direction"] = _normalize_direction(direction) if step is not None: payload["step"] = int(step) if time_limit is not _UNSET: payload["time_limit"] = ( None if time_limit is None else _coerce_float(time_limit) ) validated_release_interval: int | None = None validated_duration: int | None = None if release_interval is not None or duration is not None: current_release_interval, current_duration = self._current_streak_pair() # modify() uses None for "argument omitted", while the shared # validator uses _UNSET for that role so it can distinguish # omission from an explicit numeric value. requested_release_interval = ( release_interval if release_interval is not None else _UNSET ) requested_duration = duration if duration is not None else _UNSET validated_release_interval, validated_duration = _resolve_streak_pair( current_release_interval=current_release_interval, current_duration=current_duration, release_interval=requested_release_interval, duration=requested_duration, ) if validated_release_interval is not None: payload["release_interval"] = validated_release_interval if validated_duration is not None: payload["duration"] = validated_duration if colormap is not None: if isinstance(colormap, Colormap): cmap_payload = _flatten_colormap(colormap.to_payload()) elif isinstance(colormap, dict): cmap_payload = _flatten_colormap(colormap) else: raise InvalidArgumentError("colormap must be a Colormap or dict") payload.update(_prefix_payload("scalar_colormap_", cmap_payload)) if legend is not None: if isinstance(legend, Legend): legend_payload = legend.to_payload() elif isinstance(legend, dict): legend_payload = _flatten_legend_options(legend) else: raise InvalidArgumentError("legend must be a Legend or dict") payload.update(_prefix_payload("legend_", legend_payload)) implicit_calculate = any( value is not None for value in (direction, step, release_interval, duration) ) or (time_limit is not _UNSET) if not payload and not calculate and not implicit_calculate: raise InvalidArgumentError("modify requires at least one property") if payload: _core_call(_core.streamlines_surf_modify, self._phigs_obj, payload) if calculate or implicit_calculate: self.calculate()
[docs] def export(self, filename: str | os.PathLike[str], format: str = "fvp") -> None: """Export this streamlines surface to an FVP file. Args: filename: Destination export path. format: Export format string. Only ``fvp`` 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.calculate() >>> sl.export("/tmp/streamlines.fvp", format="fvp") """ 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 not in {"fvp", "fv particle path"}: raise InvalidArgumentError("format must be fvp") _core_call(_core.streamlines_surf_export, self._phigs_obj, filename, "fvp")
[docs] def copy(self) -> "Streamlines": """Return a new streamlines surface with the same settings and seeds. Raises :class:`InvalidArgumentError` if the current streak controls are not valid for the Python API contract. 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> copy_obj = sl.copy() """ dataset = self._require_dataset() state = self._state(include_seeds=True) vector_func_name = state.get("vector_func_name") or None if vector_func_name is None: raise InvalidArgumentError("cannot copy streamlines without vector_func") release_interval = _normalize_streak_int( state.get("release_interval", 3), "release_interval" ) duration = _normalize_streak_int(state.get("duration", 1), "duration") release_interval, duration = _validate_streak_pair(release_interval, duration) surf = create_streamlines( dataset, coloring=cast(constant.Coloring | str | None, state.get("coloring")), geometric_color=cast( constant.GeometricColor | int | None, state.get("geometric_color"), ), line_type=cast(constant.LineType | str | None, state.get("line_type")), display_type=cast( constant.PathsDisplayType | str | None, state.get("display_type"), ), sphere_scale=None if state.get("sphere_scale") is None else _coerce_float(state.get("sphere_scale")), ribbon_width=None if state.get("ribbon_width") is None else _coerce_int(state.get("ribbon_width")), transparency=None if state.get("transparency") is None else _coerce_float(state.get("transparency")), visibility=cast(bool | None, state.get("visibility")), scalar_func=cast(str | None, state.get("scalar_func_name") or None), vector_func=cast(str, vector_func_name), animate=cast(bool | None, state.get("animate")), animate_direction=cast( constant.AnimationDirection | str | None, state.get("animate_direction"), ), animate_divs=None if state.get("animate_divs") is None else _coerce_int(state.get("animate_divs")), show_seeds=cast(bool | None, state.get("show_seeds")), seed_coord=cast( constant.StreamlinesSeedCoord | str | None, state.get("seed_coord"), ), direction=cast( constant.CalculationDirection | str | None, state.get("direction"), ), step=None if state.get("step") is None else _coerce_int(state.get("step")), time_limit=state.get("time_limit"), release_interval=release_interval, duration=duration, colormap=cast( Colormap | dict[str, object] | None, state.get("scalar_colormap"), ), legend=cast(Legend | dict[str, object] | None, state.get("legend")), calculate=False, ) seed_entries = self._seed_entries_from_state(state) if seed_entries: target_seed_coord = str( state.get("seed_coord", constant.StreamlinesSeedCoord.IJK_REAL.value) ).lower() coords, grids = self._seed_payload_from_entries( seed_entries, target_seed_coord ) if target_seed_coord == constant.StreamlinesSeedCoord.XYZ.value: surf.add_seeds(coords) else: surf.add_seeds(coords, grids=grids) return surf
[docs] def delete(self) -> None: """Delete this streamlines 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"), ... ) >>> sl = fv.vis.create_streamlines(ds, vector_func=ds.vector_functions[0]) >>> sl.seed_coord = fv.constant.StreamlinesSeedCoord.IJK_REAL >>> sl.add_seeds([(5.0, 5.0, 5.0)], grid=1) >>> sl.calculate() >>> sl.delete() """ self._delete_surface(_core.streamlines_surf_delete)
[docs] def create_streamlines( dataset: Dataset | None = None, *, coloring: constant.Coloring | str | None = None, geometric_color: constant.GeometricColor | int | None = None, line_type: constant.LineType | str | None = None, display_type: constant.PathsDisplayType | str | None = None, sphere_scale: float | None = None, ribbon_width: int | None = None, transparency: float | None = None, visibility: bool | None = None, scalar_func: str | None = None, vector_func: str | None = None, animate: bool | None = None, animate_direction: constant.AnimationDirection | str | None = None, animate_divs: int | None = None, show_seeds: bool | None = None, seed_coord: constant.StreamlinesSeedCoord | str | None = None, direction: constant.CalculationDirection | str | None = None, step: int | None = None, time_limit: float | None | object = _UNSET, release_interval: int | None = None, duration: int | None = None, colormap: Colormap | dict[str, object] | None = None, legend: Legend | dict[str, object] | None = None, defer_apply: bool = False, calculate: bool = False, ) -> Streamlines: """Create a streamlines surface for the given dataset. `vector_func` is required and can be any dataset vector function name. When provided, `release_interval` and `duration` must satisfy `release_interval > 0` and `0 < duration < release_interval`. Args: dataset: Dataset to attach the new streamlines surface to. When omitted, the current dataset is used. coloring: Coloring mode for the streamline geometry. geometric_color: Geometric color id used when geometric coloring is active. line_type: Streamline line style. display_type: Path display type such as complete, filament, or ribbon. sphere_scale: Sphere or arrow size scale. ribbon_width: Ribbon width used by ribbon display modes. transparency: Transparency amount for the streamline geometry. visibility: Whether the new streamline object is initially visible. scalar_func: Optional scalar function name used for scalar coloring. vector_func: Required vector function name used for streamline integration. animate: Whether streamline animation is enabled. animate_direction: Direction used for streamline animation. animate_divs: Number of animation divisions. show_seeds: Whether seed markers are shown. seed_coord: Seed coordinate system to use for subsequent seed operations. direction: Integration direction used for calculation. step: Integration step size. time_limit: Optional tracing time limit. Pass ``None`` to clear the current limit. release_interval: Optional streakline release interval. duration: Optional streakline duration. colormap: Optional colormap settings as a :class:`Colormap` or payload dict. legend: Optional legend settings as a :class:`Legend` or payload dict. defer_apply: When ``True``, create the object without forcing an immediate apply/calculation step. calculate: When ``True``, run streamline calculation after creation. 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"), ... ) >>> sl = fv.vis.create_streamlines( ... ds, ... vector_func=ds.vector_functions[0], ... coloring=fv.constant.Coloring.SCALAR, ... scalar_func=ds.scalar_functions[0], ... seed_coord=fv.constant.StreamlinesSeedCoord.IJK_REAL, ... ) >>> sl.add_seeds([(1.0, 1.0, 1.0), (40.0, 20.0, 10.0)], grid=1) >>> sl.calculate() """ ds = _resolve_dataset_or_current(dataset) if vector_func is None: raise InvalidArgumentError("vector_func is required when creating streamlines") if release_interval is not None or duration is not None: _resolve_streak_pair( current_release_interval=3, current_duration=1, release_interval=release_interval if release_interval is not None else _UNSET, duration=duration if duration is not None else _UNSET, ) if coloring is not None: _normalize_streamlines_coloring(coloring) if line_type is not None: _normalize_line_type(line_type) if animate_direction is not None: _normalize_animate_direction(animate_direction) has_modifications = any( (value is not None and value is not _UNSET) for value in ( coloring, geometric_color, line_type, display_type, sphere_scale, ribbon_width, transparency, visibility, scalar_func, vector_func, animate, animate_direction, animate_divs, show_seeds, seed_coord, direction, step, time_limit, release_interval, duration, colormap, legend, ) ) phigs_obj = _core_call( _core.streamlines_surf_create, ds.dataset_id, defer_apply=bool(defer_apply or has_modifications), ) surf = Streamlines(phigs_obj=phigs_obj, dataset_id=ds.dataset_id, dataset=ds) if has_modifications or calculate: try: surf.modify( coloring=coloring, geometric_color=geometric_color, line_type=line_type, display_type=display_type, sphere_scale=sphere_scale, ribbon_width=ribbon_width, transparency=transparency, visibility=visibility, scalar_func=scalar_func, vector_func=vector_func, animate=animate, animate_direction=animate_direction, animate_divs=animate_divs, show_seeds=show_seeds, seed_coord=seed_coord, direction=direction, step=step, time_limit=time_limit, release_interval=release_interval, duration=duration, colormap=colormap, legend=legend, calculate=calculate, ) except Exception: _core_call(_core.streamlines_surf_delete, phigs_obj) raise return surf