Source code for fieldview.vis.particle_paths

"""Particle paths surface helpers."""

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,
    _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,
    Range,
    _coerce_float,
    _coerce_int,
    _coerce_pathlike_str,
    _normalize_range_from_values,
)

__all__: list[str] = ["ParticlePaths", "create_particle_paths"]


def _normalize_display_type(value: constant.PathsDisplayType | str) -> str:
    if isinstance(value, constant.PathsDisplayType):
        return value.value
    return str(value).strip().lower()


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_select_tags(value: str | Sequence[str]) -> str | list[str]:
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered not in {"all", "none"}:
            raise InvalidArgumentError(
                "tags must be 'all', 'none', or a sequence of tag names"
            )
        return lowered
    if not isinstance(value, Sequence):
        raise InvalidArgumentError(
            "tags must be 'all', 'none', or a sequence of tag names"
        )
    tags: list[str] = []
    for item in value:
        if not isinstance(item, str) or not item.strip():
            raise InvalidArgumentError("each tag name must be a non-empty string")
        tags.append(item)
    return tags


def _normalize_scalar_func_name(value: object) -> str:
    if not isinstance(value, str):
        raise InvalidArgumentError("scalar_func must be a string")
    name = value.strip()
    if not name:
        raise InvalidArgumentError("scalar_func cannot be empty")
    return name


def _dataset_has_particle_paths(dataset_id: int) -> bool:
    get_all_objects = getattr(_core, "get_all_objects_metadata", None)
    if not callable(get_all_objects):
        return False

    metadata = _core_call(get_all_objects)
    if not isinstance(metadata, dict):
        return False
    metadata = cast(dict[str, object], metadata)

    particle_paths = metadata.get("particle_paths_list", [])
    if not isinstance(particle_paths, list):
        return False

    for item in cast(list[object], particle_paths):
        if not isinstance(item, dict):
            continue
        try:
            item_dataset_id = _coerce_int(
                cast(dict[str, object], item).get("dataset_id", -1)
            )
        except (TypeError, ValueError):
            continue
        if item_dataset_id == dataset_id:
            return True

    return False


[docs] class ParticlePaths(PathBase): """Particle paths surface wrapper. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.display_type = fv.constant.PathsDisplayType.SPHERES_AND_LINES """ _core_prefix = "particle_paths_surf" _default_geometric_color = 3 __slots__ = ("_dataset",) def __init__( self, phigs_obj: int, dataset_id: int, dataset: Dataset | None = None ) -> None: """Create a particle-paths wrapper around an existing host object. Args: phigs_obj: Host PHIGS object identifier for the particle paths. dataset_id: Host dataset identifier backing the object. dataset: Optional live :class:`Dataset` wrapper for function lookup and validation. """ super().__init__(phigs_obj, dataset_id) self._dataset = dataset def _state(self) -> dict[str, object]: return _core_call(_core.particle_paths_surf_get_state, self._phigs_obj) def _require_dataset(self) -> Dataset: dataset = self._dataset if dataset is None: raise InvalidDatasetError( "particle paths surface is missing a dataset reference" ) dataset._ensure_valid() return dataset def _lookup_scalar_function_id(self, name: str) -> int | None: dataset = self._require_dataset() needle = name.strip().lower() for func_name, func_id in dataset._scalar_function_ids.items(): if func_name.lower() == needle: return int(func_id) return None @property def display_type(self) -> constant.PathsDisplayType: """`PathsDisplayType`: Display representation for particle paths. Example usage: .. code-block:: python >>> ppath = fv.vis.create_particle_paths(ds, filename="paths.fvp") >>> ppath.display_type = fv.constant.PathsDisplayType.SPHERES_AND_LINES """ return constant.PathsDisplayType( str( self._state().get( "display_type", constant.PathsDisplayType.SPHERES_AND_LINES.value ) ).lower() ) @display_type.setter def display_type(self, value: constant.PathsDisplayType | str) -> None: self._modify({"display_type": _normalize_display_type(value)}) @property def sphere_scale(self) -> float: """`float`: Scale factor for spheres/arrows in path displays. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.sphere_scale = 1.25 """ return _coerce_float(self._state().get("sphere_scale", 1.0)) @sphere_scale.setter def sphere_scale(self, value: float) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"sphere_scale": float(value)}, ) @property def transparency(self) -> float: """`float`: Surface transparency in range `0.0` to `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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.transparency = 0.25 """ return _coerce_float(self._state().get("transparency", 0.0)) @transparency.setter def transparency(self, value: float) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"transparency": float(value)}, ) @property def scalar_func(self) -> str | None: """`Optional[str]`: Scalar variable name used for scalar coloring. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.scalar_func = "Duration" """ name = str(self._state().get("scalar_func_name", "")).strip() return name or None @scalar_func.setter def scalar_func(self, value: str) -> None: name = _normalize_scalar_func_name(value) if name.lower() == "none": _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"scalar_func": "none"}, ) return func_id = self._lookup_scalar_function_id(name) if func_id is not None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"scalar_func_id": func_id}, ) else: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"scalar_func_name": name}, ) @property def animate(self) -> bool: """`bool`: Animation toggle for particle paths. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.animate = True """ return bool(self._state().get("animate", False)) @animate.setter def animate(self, value: bool) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"animate": bool(value)} ) @property def animate_direction(self) -> constant.AnimationDirection: """`constant.AnimationDirection`: Animation direction (`forward` or `backward`). 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.animate_direction = fv.constant.AnimationDirection.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: _core_call( _core.particle_paths_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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.animate_divs = 25 """ return _coerce_int(self._state().get("animate_divs", 25)) @animate_divs.setter def animate_divs(self, value: int) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"animate_divs": int(value)}, ) @property def subset_inc(self) -> int: """`int`: Subset increment used when displaying path samples. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.subset_inc = 1 """ return _coerce_int(self._state().get("subset_inc", 1)) @subset_inc.setter def subset_inc(self, value: int) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"subset_inc": int(value)}, ) @property def path_variables(self) -> list[str]: """`list[str]`: Imported particle-path variable names (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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.path_variables """ values = self._state().get("path_variables", []) if not isinstance(values, list): return [] return [ str(item) for item in cast(list[object], values) if isinstance(item, str) ] @property def tags(self) -> list[str]: """`list[str]`: Available imported tag names (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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.tags """ values = self._state().get("tags", []) if not isinstance(values, list): return [] return [ str(item) for item in cast(list[object], values) if isinstance(item, str) ] @property def select_by_initial_value(self) -> bool: """`bool`: Enable or disable Select By Initial 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_initial_value = True """ return bool(self._state().get("select_by_initial_value_enabled", False)) @select_by_initial_value.setter def select_by_initial_value(self, value: bool) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"select_by_initial_value_enabled": bool(value)}, ) @property def select_by_initial_value_variable(self) -> str | None: """`Optional[str]`: Variable name used by Select By Initial 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_initial_value_variable = "Duration" """ value = str(self._state().get("select_by_initial_value_variable", "")).strip() return value or None @select_by_initial_value_variable.setter def select_by_initial_value_variable(self, value: str) -> None: if not isinstance(value, str) or not value.strip(): raise InvalidArgumentError( "select_by_initial_value_variable must be a non-empty string" ) _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"select_by_initial_value_variable": value.strip()}, ) @property def select_by_initial_value_range(self) -> Range: """`Range`: Current and absolute range for Select By Initial Value (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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_initial_value_range """ state = self._state() return _normalize_range_from_values( min_value=_coerce_float(state.get("select_by_initial_value_min", 0.0)), max_value=_coerce_float(state.get("select_by_initial_value_max", 0.0)), abs_min=cast(float | None, state.get("select_by_initial_value_abs_min")), abs_max=cast(float | None, state.get("select_by_initial_value_abs_max")), ) @property def select_by_value_on_path(self) -> bool: """`bool`: Enable or disable Select By Value on Path. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_value_on_path = True """ return bool(self._state().get("select_by_value_on_path_enabled", False)) @select_by_value_on_path.setter def select_by_value_on_path(self, value: bool) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"select_by_value_on_path_enabled": bool(value)}, ) @property def select_by_value_on_path_variable(self) -> str | None: """`Optional[str]`: Variable name used by Select By Value on Path. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_value_on_path_variable = "Duration" """ value = str(self._state().get("select_by_value_on_path_variable", "")).strip() return value or None @select_by_value_on_path_variable.setter def select_by_value_on_path_variable(self, value: str) -> None: if not isinstance(value, str) or not value.strip(): raise InvalidArgumentError( "select_by_value_on_path_variable must be a non-empty string" ) _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"select_by_value_on_path_variable": value.strip()}, ) @property def select_by_value_on_path_range(self) -> Range: """`Range`: Current and absolute range for Select By Value on Path (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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_value_on_path_range """ state = self._state() return _normalize_range_from_values( min_value=_coerce_float(state.get("select_by_value_on_path_min", 0.0)), max_value=_coerce_float(state.get("select_by_value_on_path_max", 0.0)), abs_min=cast(float | None, state.get("select_by_value_on_path_abs_min")), abs_max=cast(float | None, state.get("select_by_value_on_path_abs_max")), ) @property def select_by_tags(self) -> bool: """`bool`: Enable or disable Select By Tag filtering. 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.select_by_tags = True """ return bool(self._state().get("select_by_tags_enabled", False)) @select_by_tags.setter def select_by_tags(self, value: bool) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"select_by_tags_enabled": bool(value)}, ) @property def selected_tags(self) -> list[str]: """`list[str]`: Currently selected tag names. Example usage: .. code-block:: python >>> import os >>> import fieldview as fv >>> data_dir = os.path.join(fv.home, "examples", "f18") >>> ds = fv.data.load_plot3d( ... os.path.join(data_dir, "f18i9b_g_bin"), ... os.path.join(data_dir, "f18i9b_q_bin"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.selected_tags = ["stream_1", "stream_2"] """ values = self._state().get("select_by_tags_selected", []) if not isinstance(values, list): return [] return [ str(item) for item in cast(list[object], values) if isinstance(item, str) ] @selected_tags.setter def selected_tags(self, value: str | Sequence[str]) -> None: _core_call( _core.particle_paths_surf_modify, self._phigs_obj, {"select_by_tags": _normalize_select_tags(value)}, )
[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, transparency: float | None = None, visibility: bool | None = None, scalar_func: str | None = None, animate: bool | None = None, animate_direction: constant.AnimationDirection | str | None = None, animate_divs: int | None = None, subset_inc: int | None = None, select_by_initial_value: bool | None = None, select_by_initial_value_variable: str | None = None, select_by_initial_value_min: float | None = None, select_by_initial_value_max: float | None = None, select_by_value_on_path: bool | None = None, select_by_value_on_path_variable: str | None = None, select_by_value_on_path_min: float | None = None, select_by_value_on_path_max: float | None = None, select_by_tags: bool | None = None, tags: str | Sequence[str] | None = None, colormap: Colormap | dict[str, object] | None = None, legend: Legend | dict[str, object] | None = None, ) -> None: """Modify multiple particle-path display attributes in one call. Args: coloring: Coloring mode for the particle paths. geometric_color: Geometric color id used when geometric coloring is active. line_type: Line style used for the particle paths. display_type: Path display type such as complete or filament. sphere_scale: Sphere or marker size scale. transparency: Particle-path transparency amount. visibility: Whether the particle paths are visible. scalar_func: Scalar function name used for scalar coloring. animate: Whether particle-path animation is enabled. animate_direction: Direction used for animation. animate_divs: Number of animation divisions. subset_inc: Subsampling increment for displayed paths. select_by_initial_value: Whether initial-value filtering is enabled. select_by_initial_value_variable: Variable name used for initial-value filtering. select_by_initial_value_min: Initial-value filter minimum. select_by_initial_value_max: Initial-value filter maximum. select_by_value_on_path: Whether value-on-path filtering is enabled. select_by_value_on_path_variable: Variable name used for value-on-path filtering. select_by_value_on_path_min: Value-on-path filter minimum. select_by_value_on_path_max: Value-on-path filter maximum. select_by_tags: Whether tag-based filtering is enabled. tags: Tag names to select when tag-based filtering is used. colormap: Scalar colormap options. legend: Legend 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.modify(visibility=True, animate=True, animate_divs=20) """ payload: dict[str, object] = {} if coloring is not None: mode = ( coloring.value if isinstance(coloring, constant.Coloring) else str(coloring) ) mode = mode.lower() if mode == constant.Coloring.MATERIAL.value: raise InvalidArgumentError( "material coloring is not supported for particle paths" ) payload["coloring"] = mode if geometric_color is not None: payload["geometric_color"] = int(geometric_color) if line_type is not None: payload["line_type"] = ( line_type.value if isinstance(line_type, constant.LineType) else str(line_type) ) if display_type is not None: payload["display_type"] = _normalize_display_type(display_type) if sphere_scale is not None: payload["sphere_scale"] = float(sphere_scale) if transparency is not None: payload["transparency"] = float(transparency) if visibility is not None: payload["visibility"] = bool(visibility) if scalar_func is not None: stripped = _normalize_scalar_func_name(scalar_func) if stripped.lower() == "none": payload["scalar_func"] = "none" else: func_id = self._lookup_scalar_function_id(stripped) if func_id is not None: payload["scalar_func_id"] = func_id else: payload["scalar_func_name"] = stripped 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 subset_inc is not None: payload["subset_inc"] = int(subset_inc) if select_by_initial_value is not None: payload["select_by_initial_value_enabled"] = bool(select_by_initial_value) if select_by_initial_value_variable is not None: payload["select_by_initial_value_variable"] = str( select_by_initial_value_variable ) if select_by_initial_value_min is not None: payload["select_by_initial_value_min"] = float(select_by_initial_value_min) if select_by_initial_value_max is not None: payload["select_by_initial_value_max"] = float(select_by_initial_value_max) if select_by_value_on_path is not None: payload["select_by_value_on_path_enabled"] = bool(select_by_value_on_path) if select_by_value_on_path_variable is not None: payload["select_by_value_on_path_variable"] = str( select_by_value_on_path_variable ) if select_by_value_on_path_min is not None: payload["select_by_value_on_path_min"] = float(select_by_value_on_path_min) if select_by_value_on_path_max is not None: payload["select_by_value_on_path_max"] = float(select_by_value_on_path_max) if select_by_tags is not None: payload["select_by_tags_enabled"] = bool(select_by_tags) if tags is not None: payload["select_by_tags"] = _normalize_select_tags(tags) 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)) if not payload: raise InvalidArgumentError("modify requires at least one property") _core_call(_core.particle_paths_surf_modify, self._phigs_obj, payload)
[docs] def copy(self) -> "ParticlePaths": """Return a new particle-paths surface with the same settings. Example usage: .. code-block:: python >>> copy_obj = ppath.copy() """ dataset = self._require_dataset() state = self._state() filename_raw = state.get("filename") filename = filename_raw.strip() if isinstance(filename_raw, str) else "" if not filename: raise InvalidArgumentError("cannot copy particle paths without filename") format_raw = state.get("format", "fv particle path") format_name = format_raw.strip() if isinstance(format_raw, str) else "" if not format_name: format_name = "fv particle path" scalar_func_raw = state.get("scalar_func_name") scalar_func_name = ( scalar_func_raw.strip() if isinstance(scalar_func_raw, str) else "" ) scalar_func: str | None = scalar_func_name or None initial_var_raw = state.get("select_by_initial_value_variable") initial_var_name = ( initial_var_raw.strip() if isinstance(initial_var_raw, str) else "" ) initial_var: str | None = initial_var_name or None value_on_path_var_raw = state.get("select_by_value_on_path_variable") value_on_path_var_name = ( value_on_path_var_raw.strip() if isinstance(value_on_path_var_raw, str) else "" ) value_on_path_var: str | None = value_on_path_var_name or None selected_tags_raw = state.get("select_by_tags_selected") selected_tags: str | Sequence[str] | None = ( cast(list[str], selected_tags_raw) if isinstance(selected_tags_raw, list) and selected_tags_raw else None ) return create_particle_paths( dataset, filename=filename, format=format_name, 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")), transparency=None if state.get("transparency") is None else _coerce_float(state.get("transparency")), visibility=cast(bool | None, state.get("visibility")), scalar_func=scalar_func, 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")), subset_inc=None if state.get("subset_inc") is None else _coerce_int(state.get("subset_inc")), select_by_initial_value=cast( bool | None, state.get("select_by_initial_value_enabled") ), select_by_initial_value_variable=initial_var, select_by_initial_value_min=( None if state.get("select_by_initial_value_min") is None else _coerce_float(state.get("select_by_initial_value_min")) ), select_by_initial_value_max=( None if state.get("select_by_initial_value_max") is None else _coerce_float(state.get("select_by_initial_value_max")) ), select_by_value_on_path=cast( bool | None, state.get("select_by_value_on_path_enabled") ), select_by_value_on_path_variable=value_on_path_var, select_by_value_on_path_min=( None if state.get("select_by_value_on_path_min") is None else _coerce_float(state.get("select_by_value_on_path_min")) ), select_by_value_on_path_max=( None if state.get("select_by_value_on_path_max") is None else _coerce_float(state.get("select_by_value_on_path_max")) ), select_by_tags=cast(bool | None, state.get("select_by_tags_enabled")), tags=selected_tags, colormap=cast( Colormap | dict[str, object] | None, state.get("scalar_colormap"), ), legend=cast(Legend | dict[str, object] | None, state.get("legend")), )
[docs] def delete(self) -> None: """Delete this particle-path 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"), ... ) >>> ppath = fv.vis.create_particle_paths(ds, filename="/path/to/particle_paths.fvp") >>> ppath.delete() """ self._delete_surface(_core.particle_paths_surf_delete)
[docs] def create_particle_paths( dataset: Dataset | None = None, *, filename: str | os.PathLike[str], format: str = "fv particle path", 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, transparency: float | None = None, visibility: bool | None = None, scalar_func: str | None = None, animate: bool | None = None, animate_direction: constant.AnimationDirection | str | None = None, animate_divs: int | None = None, subset_inc: int | None = None, select_by_initial_value: bool | None = None, select_by_initial_value_variable: str | None = None, select_by_initial_value_min: float | None = None, select_by_initial_value_max: float | None = None, select_by_value_on_path: bool | None = None, select_by_value_on_path_variable: str | None = None, select_by_value_on_path_min: float | None = None, select_by_value_on_path_max: float | None = None, select_by_tags: bool | None = None, tags: str | Sequence[str] | None = None, colormap: Colormap | dict[str, object] | None = None, legend: Legend | dict[str, object] | None = None, defer_apply: bool = False, ) -> ParticlePaths: """Import and create a particle-paths surface. `filename` is required and must point to an existing particle-path file supported by the selected `format`. Only one particle-paths object is supported per dataset. Delete the existing particle paths before creating a new one on the same dataset. A second create on the same dataset raises :class:`InvalidArgumentError`. Args: dataset: Dataset to attach the new particle-paths object to. When omitted, the current dataset is used. filename: Particle-path file to import. format: Particle-path file format name. coloring: Coloring mode for the particle paths. geometric_color: Geometric color id used when geometric coloring is active. line_type: Line style used for the particle paths. display_type: Path display type such as complete or filament. sphere_scale: Sphere or marker size scale. transparency: Particle-path transparency amount. visibility: Whether the imported particle paths are initially visible. scalar_func: Scalar function name used for scalar coloring. animate: Whether particle-path animation is enabled. animate_direction: Direction used for animation. animate_divs: Number of animation divisions. subset_inc: Subsampling increment for displayed paths. select_by_initial_value: Whether initial-value filtering is enabled. select_by_initial_value_variable: Variable name used for initial-value filtering. select_by_initial_value_min: Initial-value filter minimum. select_by_initial_value_max: Initial-value filter maximum. select_by_value_on_path: Whether value-on-path filtering is enabled. select_by_value_on_path_variable: Variable name used for value-on-path filtering. select_by_value_on_path_min: Value-on-path filter minimum. select_by_value_on_path_max: Value-on-path filter maximum. select_by_tags: Whether tag-based filtering is enabled. tags: Tag names to select when tag-based filtering is used. colormap: Scalar colormap options. legend: Legend options. defer_apply: When ``True``, defer the final apply step 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"), ... ) >>> ppath = fv.vis.create_particle_paths( ... ds, ... filename="/path/to/particle_paths.fvp", ... coloring=fv.constant.Coloring.SCALAR, ... scalar_func="Duration", ... ) """ ds = _resolve_dataset_or_current(dataset) filename = _coerce_pathlike_str(filename, "filename") if not filename.strip(): raise InvalidArgumentError("filename is required when creating particle paths") if not isinstance(format, str) or not format.strip(): raise InvalidArgumentError("format must be a non-empty string") if _dataset_has_particle_paths(ds.dataset_id): raise InvalidArgumentError( "only one particle paths object is supported per dataset; " + "delete the existing particle paths before creating a new one" ) has_modifications = any( value is not None for value in ( coloring, geometric_color, line_type, display_type, sphere_scale, transparency, visibility, scalar_func, animate, animate_direction, animate_divs, subset_inc, select_by_initial_value, select_by_initial_value_variable, select_by_initial_value_min, select_by_initial_value_max, select_by_value_on_path, select_by_value_on_path_variable, select_by_value_on_path_min, select_by_value_on_path_max, select_by_tags, tags, colormap, legend, ) ) phigs_obj = _core_call( _core.particle_paths_surf_create, ds.dataset_id, filename.strip(), format.strip(), defer_apply=bool(defer_apply or has_modifications), ) surf = ParticlePaths(phigs_obj=phigs_obj, dataset_id=ds.dataset_id, dataset=ds) if has_modifications: try: surf.modify( coloring=coloring, geometric_color=geometric_color, line_type=line_type, display_type=display_type, sphere_scale=sphere_scale, transparency=transparency, visibility=visibility, scalar_func=scalar_func, animate=animate, animate_direction=animate_direction, animate_divs=animate_divs, subset_inc=subset_inc, select_by_initial_value=select_by_initial_value, select_by_initial_value_variable=select_by_initial_value_variable, select_by_initial_value_min=select_by_initial_value_min, select_by_initial_value_max=select_by_initial_value_max, select_by_value_on_path=select_by_value_on_path, select_by_value_on_path_variable=select_by_value_on_path_variable, select_by_value_on_path_min=select_by_value_on_path_min, select_by_value_on_path_max=select_by_value_on_path_max, select_by_tags=select_by_tags, tags=tags, colormap=colormap, legend=legend, ) except Exception: _core_call(_core.particle_paths_surf_delete, phigs_obj) raise return surf