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