"""Surface flow feature-extraction path helpers.
Example:
.. 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"),
... )
>>> sf = fv.vis.create_surface_flows(
... ds,
... mode=fv.constant.SurfaceFlowMode.NO_SLIP,
... vector_func="Velocity Vectors [PLOT3D]",
... direction=fv.constant.CalculationDirection.BOTH,
... )
>>> sf.calculate()
"""
from __future__ import annotations
import os
from typing import TypedDict, cast
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, _coerce_float, _coerce_int, _coerce_pathlike_str
__all__: list[str] = ["SurfaceFlows", "create_surface_flows"]
_UNSET = object()
_SURFACE_FLOW_MODES: dict[str, str] = {
"no slip": "no_slip",
"noslip": "no_slip",
"euler": "euler",
"offset": "offset",
}
_SURFACE_FLOW_SEEDING: dict[str, str] = {
"low density": "low_density",
"lowdensity": "low_density",
"medium density": "medium_density",
"mediumdensity": "medium_density",
"high density": "high_density",
"highdensity": "high_density",
"from file": "from_file",
}
_SURFACE_FLOW_PATH_VARIABLES: dict[str, str] = {
"duration": "Duration",
"wall shear magnitude": "Wall Shear Magnitude",
"wallshearmagnitude": "Wall Shear Magnitude",
}
_FEATURE_COLORING = {
constant.Coloring.GEOMETRIC.value,
constant.Coloring.SCALAR.value,
}
_FEATURE_LINE_TYPES = {
constant.LineType.THIN.value,
constant.LineType.MEDIUM.value,
constant.LineType.THICK.value,
}
class _FeatureStateKwargs(TypedDict):
visibility: bool | None
line_type: constant.LineType | str | None
geometric_color: constant.GeometricColor | int | None
coloring: constant.Coloring | str | None
colormap: Colormap | dict[str, object] | None
legend: Legend | dict[str, object] | None
class _SurfaceFlowStateKwargs(_FeatureStateKwargs):
mode: constant.SurfaceFlowMode | str
scalar_func: str | None
scalar_path_variable: str | None
vector_func: str | None
direction: constant.CalculationDirection | str | None
time_limit_enabled: bool | None
time_limit: object
seeding: constant.SurfaceFlowSeeding | str | None
seed_file: object
offset_distance: float | None
step: int | None
def _normalize_mode(value: constant.SurfaceFlowMode | str) -> str:
if isinstance(value, constant.SurfaceFlowMode):
return value.value
canonical = _SURFACE_FLOW_MODES.get(str(value).strip().lower().replace("_", " "))
if canonical is None:
allowed = ", ".join(sorted({v for v in _SURFACE_FLOW_MODES.values()}))
raise InvalidArgumentError(f"mode must be one of: {allowed}")
return canonical
def _normalize_feature_coloring(value: constant.Coloring | str) -> str:
if isinstance(value, constant.Coloring):
mode = value.value
else:
mode = str(value).strip().lower()
if mode == constant.Coloring.MATERIAL.value:
raise InvalidArgumentError(
"material coloring is not supported for surface-flow path objects"
)
if mode not in _FEATURE_COLORING:
raise InvalidArgumentError("coloring must be geometric or scalar")
return mode
def _normalize_feature_line_type(value: constant.LineType | str) -> str:
if isinstance(value, constant.LineType):
return value.value
line_type = str(value).strip().lower()
if line_type not in _FEATURE_LINE_TYPES:
raise InvalidArgumentError("line_type must be thin, medium, or thick")
return line_type
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_seeding(value: constant.SurfaceFlowSeeding | str) -> str:
if isinstance(value, constant.SurfaceFlowSeeding):
return value.value
canonical = _SURFACE_FLOW_SEEDING.get(str(value).strip().lower().replace("_", " "))
if canonical is None:
allowed = ", ".join(sorted({v for v in _SURFACE_FLOW_SEEDING.values()}))
raise InvalidArgumentError(f"seeding must be one of: {allowed}")
return canonical
def _feature_state_kwargs(state: dict[str, object]) -> _FeatureStateKwargs:
return {
"visibility": cast(bool | None, state.get("visibility")),
"line_type": cast(constant.LineType | str | None, state.get("line_type")),
"geometric_color": cast(
constant.GeometricColor | int | None, state.get("geometric_color")
),
"coloring": cast(constant.Coloring | str | None, state.get("coloring")),
"colormap": cast(
Colormap | dict[str, object] | None, state.get("scalar_colormap")
),
"legend": cast(Legend | dict[str, object] | None, state.get("legend")),
}
def _require_dataset(dataset: Dataset | None, label: str) -> Dataset:
if dataset is None:
raise InvalidDatasetError(f"{label} is missing a dataset reference")
dataset._ensure_valid()
return dataset
def _lookup_function_id(
dataset: Dataset | None, name: str, attr: str, label: str
) -> int | None:
ds = _require_dataset(dataset, label)
mapping = cast(dict[str, int], getattr(ds, attr, {}))
needle = name.strip().lower()
for func_name, func_id in mapping.items():
if func_name.lower() == needle:
return func_id
return None
def _lookup_function_name(
dataset: Dataset | None, name: str, attr: str, label: str
) -> str | None:
ds = _require_dataset(dataset, label)
mapping = cast(dict[str, int], getattr(ds, attr, {}))
needle = name.strip().lower()
for func_name in mapping:
if func_name.lower() == needle:
return str(func_name)
return None
def _normalize_surface_flow_scalar_path_variable(
dataset: Dataset | None, value: str
) -> str:
if not isinstance(value, str):
raise InvalidArgumentError("scalar_path_variable must be a string")
name = value.strip()
if not name:
raise InvalidArgumentError("scalar_path_variable cannot be empty")
dataset_name = _lookup_function_name(
dataset, name, "_scalar_function_ids", "surface flows"
)
if dataset_name is not None:
return dataset_name
canonical = _SURFACE_FLOW_PATH_VARIABLES.get(name.lower().replace("_", " "))
if canonical is not None:
return canonical
allowed = ["Duration", "Wall Shear Magnitude"]
raise InvalidArgumentError(
"scalar_path_variable must be a dataset scalar or one of: " + ", ".join(allowed)
)
def _build_path_visual_payload(
*,
visibility: bool | None = None,
line_type: constant.LineType | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
coloring: constant.Coloring | str | None = None,
colormap: Colormap | dict[str, object] | None = None,
legend: Legend | dict[str, object] | None = None,
) -> dict[str, object]:
payload: dict[str, object] = {}
if visibility is not None:
payload["visibility"] = bool(visibility)
if line_type is not None:
payload["line_type"] = _normalize_feature_line_type(line_type)
if geometric_color is not None:
payload["geometric_color"] = int(geometric_color)
if coloring is not None:
payload["coloring"] = _normalize_feature_coloring(coloring)
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))
return payload
def _build_surface_flows_payload(
*,
dataset: Dataset | None,
mode: object = _UNSET,
visibility: bool | None = None,
line_type: constant.LineType | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
coloring: constant.Coloring | str | None = None,
scalar_func: str | None = None,
scalar_path_variable: str | None = None,
vector_func: str | None = None,
direction: constant.CalculationDirection | str | None = None,
time_limit_enabled: bool | None = None,
time_limit: float | None | object = _UNSET,
seeding: constant.SurfaceFlowSeeding | str | None = None,
seed_file: str | os.PathLike[str] | None | object = _UNSET,
offset_distance: float | None = None,
step: int | None = None,
colormap: Colormap | dict[str, object] | None = None,
legend: Legend | dict[str, object] | None = None,
) -> dict[str, object]:
payload = _build_path_visual_payload(
visibility=visibility,
line_type=line_type,
geometric_color=geometric_color,
coloring=coloring,
colormap=colormap,
legend=legend,
)
if mode is not _UNSET:
if not isinstance(mode, (constant.SurfaceFlowMode, str)):
raise InvalidArgumentError("mode must be a SurfaceFlowMode or string")
payload["mode"] = _normalize_mode(mode)
if scalar_path_variable is not None:
payload["scalar_path_variable"] = _normalize_surface_flow_scalar_path_variable(
dataset, scalar_path_variable
)
if scalar_func is not None:
stripped = scalar_func.strip()
if not stripped:
raise InvalidArgumentError("scalar_func cannot be empty")
if stripped.lower() == "none":
payload["scalar_func"] = "none"
else:
func_id = _lookup_function_id(
dataset, stripped, "_scalar_function_ids", "surface flows"
)
if func_id is None:
raise InvalidArgumentError(
"scalar_func must be a dataset scalar function; use scalar_path_variable for path variables"
)
payload["scalar_func_id"] = func_id
if vector_func is not None:
stripped = vector_func.strip()
if not stripped:
raise InvalidArgumentError("vector_func cannot be empty")
if stripped.lower() == "none":
payload["vector_func"] = "none"
else:
func_id = _lookup_function_id(
dataset, stripped, "_vector_function_ids", "surface flows"
)
if func_id is not None:
payload["vector_func_id"] = func_id
else:
payload["vector_func_name"] = stripped
if direction is not None:
payload["direction"] = _normalize_direction(direction)
if time_limit_enabled is not None:
payload["time_limit_enabled"] = bool(time_limit_enabled)
if time_limit is not _UNSET:
if time_limit is None:
payload["time_limit"] = None
else:
try:
payload["time_limit"] = _coerce_float(time_limit)
except (TypeError, ValueError) as exc:
raise InvalidArgumentError(
"time_limit must be a float or None"
) from exc
if seeding is not None:
payload["seeding"] = _normalize_seeding(seeding)
if seed_file is not _UNSET:
if seed_file is None:
payload["seed_file"] = None
else:
normalized_seed_file = _coerce_pathlike_str(seed_file, "seed_file")
if not normalized_seed_file.strip():
raise InvalidArgumentError("seed_file cannot be empty")
payload["seed_file"] = normalized_seed_file
if offset_distance is not None:
try:
payload["offset_distance"] = _coerce_float(offset_distance)
except (TypeError, ValueError) as exc:
raise InvalidArgumentError("offset_distance must be a float") from exc
if step is not None:
payload["step"] = _coerce_int(step)
return payload
def _surface_flow_kwargs_from_state(
state: dict[str, object],
) -> _SurfaceFlowStateKwargs:
mode = state.get("mode", constant.SurfaceFlowMode.NO_SLIP.value)
include_offset_distance = str(mode).lower() == constant.SurfaceFlowMode.OFFSET.value
feature_kwargs = _feature_state_kwargs(state)
offset_distance = state.get("offset_distance")
step = state.get("step")
seed_file = state.get("seed_file")
seed_file_value = None
if isinstance(seed_file, str):
seed_file_value = seed_file.strip() or None
return {
"visibility": feature_kwargs["visibility"],
"line_type": feature_kwargs["line_type"],
"geometric_color": feature_kwargs["geometric_color"],
"coloring": feature_kwargs["coloring"],
"colormap": feature_kwargs["colormap"],
"legend": feature_kwargs["legend"],
"mode": str(mode),
"scalar_func": cast(str | None, state.get("scalar_func_name") or None),
"scalar_path_variable": cast(
str | None, state.get("scalar_path_variable") or None
),
"vector_func": cast(str | None, state.get("vector_func_name") or None),
"direction": cast(
constant.CalculationDirection | str | None, state.get("direction")
),
"time_limit_enabled": cast(bool | None, state.get("time_limit_enabled")),
"time_limit": state.get("time_limit"),
"seeding": cast(constant.SurfaceFlowSeeding | str | None, state.get("seeding")),
"seed_file": seed_file_value,
"offset_distance": None
if not include_offset_distance or offset_distance is None
else _coerce_float(offset_distance),
"step": None if step is None else _coerce_int(step),
}
[docs]
class SurfaceFlows(PathBase):
"""Surface restricted flow path 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"),
... )
>>> sf = fv.vis.create_surface_flows(
... ds,
... mode=fv.constant.SurfaceFlowMode.NO_SLIP,
... vector_func="Velocity Vectors [PLOT3D]",
... )
>>> sf.direction = fv.constant.CalculationDirection.BOTH
>>> sf.calculate()
"""
_core_prefix = "surface_flows"
__slots__ = ("_dataset", "_mode")
def __init__(
self,
phigs_obj: int,
dataset_id: int,
dataset: Dataset | None = None,
mode: constant.SurfaceFlowMode | str | None = None,
) -> None:
"""Create a surface-flows wrapper around an existing host object.
Args:
phigs_obj: Host PHIGS object identifier for the feature object.
dataset_id: Host dataset identifier backing the object.
dataset: Optional live :class:`Dataset` wrapper for validation.
mode: Optional surface-flow extraction mode to cache locally.
"""
super().__init__(phigs_obj, dataset_id)
self._dataset = dataset
self._mode = _normalize_mode(mode) if mode is not None else None
def _state(self) -> dict[str, object]:
return _core_call(_core.surface_flows_get_state, self._phigs_obj)
@property
def display_type(self) -> constant.PathsDisplayType:
"""`PathsDisplayType`: Display representation for surface flows.
Only ``COMPLETE`` is supported.
"""
return constant.PathsDisplayType.COMPLETE
@display_type.setter
def display_type(self, value: constant.PathsDisplayType | str) -> None:
normalized = (
value.value
if isinstance(value, constant.PathsDisplayType)
else str(value).strip().lower()
)
if normalized != constant.PathsDisplayType.COMPLETE.value:
raise InvalidArgumentError(
"surface flows only support display_type='complete'"
)
@property
def mode(self) -> constant.SurfaceFlowMode:
"""`SurfaceFlowMode`: Surface-flow extraction mode."""
value = self._state().get(
"mode", self._mode or constant.SurfaceFlowMode.NO_SLIP.value
)
return constant.SurfaceFlowMode(str(value).lower())
@mode.setter
def mode(self, value: constant.SurfaceFlowMode | str) -> None:
new_mode = _normalize_mode(value)
if new_mode == self.mode.value:
return
self._rebuild(mode=new_mode)
@property
def scalar_func(self) -> str | None:
"""`Optional[str]`: Dataset scalar function cached for registry loading."""
name = str(self._state().get("scalar_func_name", "")).strip()
return name or None
@scalar_func.setter
def scalar_func(self, value: str) -> None:
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")
if name.lower() == "none":
self._modify({"scalar_func": "none"})
return
func_id = _lookup_function_id(
self._dataset, name, "_scalar_function_ids", "surface flows"
)
if func_id is None:
raise InvalidArgumentError(
"scalar_func must be a dataset scalar function; use scalar_path_variable for path variables"
)
self._modify({"scalar_func_id": func_id})
@property
def scalar_path_variable(self) -> str | None:
"""`Optional[str]`: Active scalar coloring selection for dataset or feature variables."""
name = str(self._state().get("scalar_path_variable", "")).strip()
return name or None
@scalar_path_variable.setter
def scalar_path_variable(self, value: str) -> None:
self._modify(
{
"scalar_path_variable": _normalize_surface_flow_scalar_path_variable(
self._dataset, value
)
}
)
@property
def vector_func(self) -> str | None:
"""`Optional[str]`: Vector function used for the path calculation."""
name = str(self._state().get("vector_func_name", "")).strip()
return name or None
@vector_func.setter
def vector_func(self, value: str) -> None:
if not isinstance(value, str):
raise InvalidArgumentError("vector_func must be a string")
name = value.strip()
if not name:
raise InvalidArgumentError("vector_func cannot be empty")
if name.lower() == "none":
self._modify({"vector_func": "none"})
return
func_id = _lookup_function_id(
self._dataset, name, "_vector_function_ids", "surface flows"
)
if func_id is not None:
self._modify({"vector_func_id": func_id})
else:
self._modify({"vector_func_name": name})
@property
def direction(self) -> constant.CalculationDirection:
"""`CalculationDirection`: Integration direction."""
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:
self._modify({"direction": _normalize_direction(value)})
@property
def time_limit_enabled(self) -> bool:
"""`bool`: Enable the duration-limit field."""
return bool(self._state().get("time_limit_enabled", False))
@time_limit_enabled.setter
def time_limit_enabled(self, value: bool) -> None:
self._modify({"time_limit_enabled": bool(value)})
@property
def time_limit(self) -> float | None:
"""`Optional[float]`: Duration-limit value, or ``None`` when disabled."""
value = self._state().get("time_limit")
return None if value is None else _coerce_float(value)
@time_limit.setter
def time_limit(self, value: float | None) -> None:
if value is None:
self._modify({"time_limit": None})
else:
self._modify({"time_limit": float(value)})
@property
def seeding(self) -> constant.SurfaceFlowSeeding:
"""`SurfaceFlowSeeding`: Seeding density/source mode."""
value = str(
self._state().get("seeding", constant.SurfaceFlowSeeding.LOW_DENSITY.value)
).lower()
return constant.SurfaceFlowSeeding(value)
@seeding.setter
def seeding(self, value: constant.SurfaceFlowSeeding | str) -> None:
self._modify({"seeding": _normalize_seeding(value)})
@property
def seed_file(self) -> str | None:
"""`Optional[str]`: Seed-file path when ``seeding`` is ``FROM_FILE``."""
value = self._state().get("seed_file")
if isinstance(value, str):
stripped = value.strip()
return stripped or None
return None
@seed_file.setter
def seed_file(self, value: str | os.PathLike[str] | None) -> None:
if value is None:
self._modify({"seed_file": None})
else:
normalized = _coerce_pathlike_str(value, "seed_file")
if not normalized.strip():
raise InvalidArgumentError("seed_file cannot be empty")
self._modify({"seed_file": normalized})
@property
def offset_distance(self) -> float:
"""`float`: Surface offset distance used by OFFSET mode."""
return _coerce_float(self._state().get("offset_distance", 0.0))
@offset_distance.setter
def offset_distance(self, value: float) -> None:
self._modify({"offset_distance": float(value)})
@property
def step(self) -> int:
"""`int`: Integration step count."""
return _coerce_int(self._state().get("step", 1))
@step.setter
def step(self, value: int) -> None:
self._modify({"step": int(value)})
[docs]
def calculate(self) -> None:
"""Calculate or recalculate the current surface-flow paths."""
_core_call(_core.surface_flows_calculate, self._phigs_obj)
def _rebuild(
self,
*,
mode: constant.SurfaceFlowMode | str,
visibility: bool | None = None,
line_type: constant.LineType | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
coloring: constant.Coloring | str | None = None,
scalar_func: str | None = None,
scalar_path_variable: str | None = None,
vector_func: str | None = None,
direction: constant.CalculationDirection | str | None = None,
time_limit_enabled: bool | None = None,
time_limit: object = _UNSET,
seeding: constant.SurfaceFlowSeeding | str | None = None,
seed_file: object = _UNSET,
offset_distance: float | None = None,
step: int | None = None,
colormap: Colormap | dict[str, object] | None = None,
legend: Legend | dict[str, object] | None = None,
calculate: bool = False,
) -> None:
dataset = _require_dataset(self._dataset, "surface flows")
state = _surface_flow_kwargs_from_state(self._state())
new_surf = create_surface_flows(
dataset,
mode=mode,
visibility=state["visibility"] if visibility is None else visibility,
line_type=state["line_type"] if line_type is None else line_type,
geometric_color=state["geometric_color"]
if geometric_color is None
else geometric_color,
coloring=state["coloring"] if coloring is None else coloring,
scalar_func=state["scalar_func"] if scalar_func is None else scalar_func,
scalar_path_variable=(
state["scalar_path_variable"]
if scalar_path_variable is None
else scalar_path_variable
),
vector_func=state["vector_func"] if vector_func is None else vector_func,
direction=state["direction"] if direction is None else direction,
time_limit_enabled=state["time_limit_enabled"]
if time_limit_enabled is None
else time_limit_enabled,
time_limit=state["time_limit"] if time_limit is _UNSET else time_limit,
seeding=state["seeding"] if seeding is None else seeding,
seed_file=state["seed_file"] if seed_file is _UNSET else seed_file,
offset_distance=state["offset_distance"]
if offset_distance is None
else offset_distance,
step=state["step"] if step is None else step,
colormap=state["colormap"] if colormap is None else colormap,
legend=state["legend"] if legend is None else legend,
calculate=calculate,
)
old_phigs_obj = self._phigs_obj
self._phigs_obj = new_surf._phigs_obj
self._mode = new_surf._mode
_core_call(_core.surface_flows_delete, old_phigs_obj)
[docs]
def modify(
self,
*,
mode: object = _UNSET,
visibility: bool | None = None,
line_type: constant.LineType | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
coloring: constant.Coloring | str | None = None,
scalar_func: str | None = None,
scalar_path_variable: str | None = None,
vector_func: str | None = None,
direction: constant.CalculationDirection | str | None = None,
time_limit_enabled: bool | None = None,
time_limit: float | None | object = _UNSET,
seeding: constant.SurfaceFlowSeeding | str | None = None,
seed_file: str | os.PathLike[str] | None | object = _UNSET,
offset_distance: float | None = None,
step: int | None = None,
colormap: Colormap | dict[str, object] | None = None,
legend: Legend | dict[str, object] | None = None,
calculate: bool = False,
) -> None:
"""Modify multiple surface-flow settings in one call.
Set ``calculate=True`` to run the calculation after the settings update.
Args:
mode: Optional surface-flow extraction mode.
visibility: Whether the object is visible.
line_type: Line style used for the extracted paths.
geometric_color: Geometric color id used when geometric coloring is
active.
coloring: Coloring mode for the extracted paths.
scalar_func: Scalar function name used for scalar coloring.
scalar_path_variable: Path-variable name used for scalar coloring.
vector_func: Vector function name used for path calculation.
direction: Integration direction.
time_limit_enabled: Whether the time-limit field is enabled.
time_limit: Optional tracing time limit.
seeding: Seeding mode.
seed_file: Optional seed-file path.
offset_distance: Surface offset distance.
step: Integration step count.
colormap: Scalar colormap options.
legend: Legend options.
calculate: When ``True``, run the calculation after updating the
settings.
"""
payload = _build_surface_flows_payload(
dataset=self._dataset,
mode=mode,
visibility=visibility,
line_type=line_type,
geometric_color=geometric_color,
coloring=coloring,
scalar_func=scalar_func,
scalar_path_variable=scalar_path_variable,
vector_func=vector_func,
direction=direction,
time_limit_enabled=time_limit_enabled,
time_limit=time_limit,
seeding=seeding,
seed_file=seed_file,
offset_distance=offset_distance,
step=step,
colormap=colormap,
legend=legend,
)
if not payload and not calculate:
raise InvalidArgumentError("modify requires at least one property")
payload_mode = payload.pop("mode", None)
if payload_mode is not None and payload_mode != self.mode.value:
self._rebuild(
mode=cast(constant.SurfaceFlowMode | str, payload_mode),
visibility=visibility,
line_type=line_type,
geometric_color=geometric_color,
coloring=coloring,
scalar_func=scalar_func,
scalar_path_variable=scalar_path_variable,
vector_func=vector_func,
direction=direction,
time_limit_enabled=time_limit_enabled,
time_limit=time_limit,
seeding=seeding,
seed_file=seed_file,
offset_distance=offset_distance,
step=step,
colormap=colormap,
legend=legend,
calculate=calculate,
)
return
if payload:
_core_call(_core.surface_flows_modify, self._phigs_obj, payload)
if calculate:
self.calculate()
[docs]
def copy(self) -> "SurfaceFlows":
"""Return a new surface-flow object with the same public settings."""
dataset = _require_dataset(self._dataset, "surface flows")
return create_surface_flows(
dataset, **_surface_flow_kwargs_from_state(self._state())
)
[docs]
def delete(self) -> None:
"""Delete this surface-flow object."""
self._delete_surface(_core.surface_flows_delete)
[docs]
def create_surface_flows(
dataset: Dataset | None = None,
*,
mode: constant.SurfaceFlowMode | str,
visibility: bool | None = None,
line_type: constant.LineType | str | None = None,
geometric_color: constant.GeometricColor | int | None = None,
coloring: constant.Coloring | str | None = None,
scalar_func: str | None = None,
scalar_path_variable: str | None = None,
vector_func: str | None = None,
direction: constant.CalculationDirection | str | None = None,
time_limit_enabled: bool | None = None,
time_limit: float | None | object = _UNSET,
seeding: constant.SurfaceFlowSeeding | str | None = None,
seed_file: str | os.PathLike[str] | None | object = _UNSET,
offset_distance: float | None = None,
step: int | None = None,
colormap: Colormap | dict[str, object] | None = None,
legend: Legend | dict[str, object] | None = None,
calculate: bool = False,
) -> SurfaceFlows:
"""Create a surface restricted flow path object.
Args:
dataset: Dataset to attach the new feature object to. When omitted,
the current dataset is used.
mode: Surface-flow extraction mode.
visibility: Whether the object is initially visible.
line_type: Line style used for the extracted paths.
geometric_color: Geometric color id used when geometric coloring is
active.
coloring: Coloring mode for the extracted paths.
scalar_func: Scalar function name used for scalar coloring.
scalar_path_variable: Path-variable name used for scalar coloring.
vector_func: Vector function name used for path calculation.
direction: Integration direction.
time_limit_enabled: Whether the time-limit field is enabled.
time_limit: Optional tracing time limit.
seeding: Seeding mode.
seed_file: Optional seed-file path.
offset_distance: Surface offset distance.
step: Integration step count.
colormap: Scalar colormap options.
legend: Legend options.
calculate: When ``True``, run the 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"),
... )
>>> sf = fv.vis.create_surface_flows(
... ds,
... mode=fv.constant.SurfaceFlowMode.NO_SLIP,
... vector_func="Velocity Vectors [PLOT3D]",
... direction=fv.constant.CalculationDirection.BOTH,
... )
>>> sf.calculate()
"""
ds = _resolve_dataset_or_current(dataset)
canonical_mode = _normalize_mode(mode)
payload = _build_surface_flows_payload(
dataset=ds,
visibility=visibility,
line_type=line_type,
geometric_color=geometric_color,
coloring=coloring,
scalar_func=scalar_func,
scalar_path_variable=scalar_path_variable,
vector_func=vector_func,
direction=direction,
time_limit_enabled=time_limit_enabled,
time_limit=time_limit,
seeding=seeding,
seed_file=seed_file,
offset_distance=offset_distance,
step=step,
colormap=colormap,
legend=legend,
)
phigs_obj = _core_call(_core.surface_flows_create, ds.dataset_id, canonical_mode)
surf = SurfaceFlows(
phigs_obj=phigs_obj, dataset_id=ds.dataset_id, dataset=ds, mode=canonical_mode
)
if payload or calculate:
try:
if payload:
_core_call(_core.surface_flows_modify, phigs_obj, payload)
if calculate:
surf.calculate()
except Exception:
_core_call(_core.surface_flows_delete, phigs_obj)
raise
return surf