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