This commit is contained in:
Sohel
2024-11-20 17:17:14 +01:00
parent aa14c170c8
commit 09b4990d6d
5404 changed files with 1184888 additions and 3 deletions

View File

@@ -0,0 +1,253 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from contourpy._contourpy import (
ContourGenerator, FillType, LineType, Mpl2005ContourGenerator, Mpl2014ContourGenerator,
SerialContourGenerator, ThreadedContourGenerator, ZInterp, max_threads,
)
from contourpy._version import __version__
from contourpy.chunk import calc_chunk_sizes
from contourpy.enum_util import as_fill_type, as_line_type, as_z_interp
if TYPE_CHECKING:
from typing import Any
from numpy.typing import ArrayLike
from ._contourpy import CoordinateArray, MaskArray
__all__ = [
"__version__",
"contour_generator",
"max_threads",
"FillType",
"LineType",
"ContourGenerator",
"Mpl2005ContourGenerator",
"Mpl2014ContourGenerator",
"SerialContourGenerator",
"ThreadedContourGenerator",
"ZInterp",
]
# Simple mapping of algorithm name to class name.
_class_lookup: dict[str, type[ContourGenerator]] = dict(
mpl2005=Mpl2005ContourGenerator,
mpl2014=Mpl2014ContourGenerator,
serial=SerialContourGenerator,
threaded=ThreadedContourGenerator,
)
def _remove_z_mask(
z: ArrayLike | np.ma.MaskedArray[Any, Any] | None,
) -> tuple[CoordinateArray, MaskArray | None]:
# Preserve mask if present.
z_array = np.ma.asarray(z, dtype=np.float64) # type: ignore[no-untyped-call]
z_masked = np.ma.masked_invalid(z_array, copy=False) # type: ignore[no-untyped-call]
if np.ma.is_masked(z_masked): # type: ignore[no-untyped-call]
mask = np.ma.getmask(z_masked) # type: ignore[no-untyped-call]
else:
mask = None
return np.ma.getdata(z_masked), mask # type: ignore[no-untyped-call]
def contour_generator(
x: ArrayLike | None = None,
y: ArrayLike | None = None,
z: ArrayLike | np.ma.MaskedArray[Any, Any] | None = None,
*,
name: str = "serial",
corner_mask: bool | None = None,
line_type: LineType | str | None = None,
fill_type: FillType | str | None = None,
chunk_size: int | tuple[int, int] | None = None,
chunk_count: int | tuple[int, int] | None = None,
total_chunk_count: int | None = None,
quad_as_tri: bool = False,
z_interp: ZInterp | str | None = ZInterp.Linear,
thread_count: int = 0,
) -> ContourGenerator:
"""Create and return a contour generator object.
The class and properties of the contour generator are determined by the function arguments,
with sensible defaults.
Args:
x (array-like of shape (ny, nx) or (nx,), optional): The x-coordinates of the ``z`` values.
May be 2D with the same shape as ``z.shape``, or 1D with length ``nx = z.shape[1]``.
If not specified are assumed to be ``np.arange(nx)``. Must be ordered monotonically.
y (array-like of shape (ny, nx) or (ny,), optional): The y-coordinates of the ``z`` values.
May be 2D with the same shape as ``z.shape``, or 1D with length ``ny = z.shape[0]``.
If not specified are assumed to be ``np.arange(ny)``. Must be ordered monotonically.
z (array-like of shape (ny, nx), may be a masked array): The 2D gridded values to calculate
the contours of. May be a masked array, and any invalid values (``np.inf`` or
``np.nan``) will also be masked out.
name (str): Algorithm name, one of ``"serial"``, ``"threaded"``, ``"mpl2005"`` or
``"mpl2014"``, default ``"serial"``.
corner_mask (bool, optional): Enable/disable corner masking, which only has an effect if
``z`` is a masked array. If ``False``, any quad touching a masked point is masked out.
If ``True``, only the triangular corners of quads nearest these points are always masked
out, other triangular corners comprising three unmasked points are contoured as usual.
If not specified, uses the default provided by the algorithm ``name``.
line_type (LineType, optional): The format of contour line data returned from calls to
:meth:`~contourpy.ContourGenerator.lines`. If not specified, uses the default provided
by the algorithm ``name``.
fill_type (FillType, optional): The format of filled contour data returned from calls to
:meth:`~contourpy.ContourGenerator.filled`. If not specified, uses the default provided
by the algorithm ``name``.
chunk_size (int or tuple(int, int), optional): Chunk size in (y, x) directions, or the same
size in both directions if only one value is specified.
chunk_count (int or tuple(int, int), optional): Chunk count in (y, x) directions, or the
same count in both directions if only one value is specified.
total_chunk_count (int, optional): Total number of chunks.
quad_as_tri (bool): Enable/disable treating quads as 4 triangles, default ``False``.
If ``False``, a contour line within a quad is a straight line between points on two of
its edges. If ``True``, each full quad is divided into 4 triangles using a virtual point
at the centre (mean x, y of the corner points) and a contour line is piecewise linear
within those triangles. Corner-masked triangles are not affected by this setting, only
full unmasked quads.
z_interp (ZInterp): How to interpolate ``z`` values when determining where contour lines
intersect the edges of quads and the ``z`` values of the central points of quads,
default ``ZInterp.Linear``.
thread_count (int): Number of threads to use for contour calculation, default 0. Threads can
only be used with an algorithm ``name`` that supports threads (currently only
``name="threaded"``) and there must be at least the same number of chunks as threads.
If ``thread_count=0`` and ``name="threaded"`` then it uses the maximum number of threads
as determined by the C++11 call ``std::thread::hardware_concurrency()``. If ``name`` is
something other than ``"threaded"`` then the ``thread_count`` will be set to ``1``.
Return:
:class:`~contourpy._contourpy.ContourGenerator`.
Note:
A maximum of one of ``chunk_size``, ``chunk_count`` and ``total_chunk_count`` may be
specified.
Warning:
The ``name="mpl2005"`` algorithm does not implement chunking for contour lines.
"""
x = np.asarray(x, dtype=np.float64)
y = np.asarray(y, dtype=np.float64)
z, mask = _remove_z_mask(z)
# Check arguments: z.
if z.ndim != 2:
raise TypeError(f"Input z must be 2D, not {z.ndim}D")
if z.shape[0] < 2 or z.shape[1] < 2:
raise TypeError(f"Input z must be at least a (2, 2) shaped array, but has shape {z.shape}")
ny, nx = z.shape
# Check arguments: x and y.
if x.ndim != y.ndim:
raise TypeError(f"Number of dimensions of x ({x.ndim}) and y ({y.ndim}) do not match")
if x.ndim == 0:
x = np.arange(nx, dtype=np.float64)
y = np.arange(ny, dtype=np.float64)
x, y = np.meshgrid(x, y)
elif x.ndim == 1:
if len(x) != nx:
raise TypeError(f"Length of x ({len(x)}) must match number of columns in z ({nx})")
if len(y) != ny:
raise TypeError(f"Length of y ({len(y)}) must match number of rows in z ({ny})")
x, y = np.meshgrid(x, y)
elif x.ndim == 2:
if x.shape != z.shape:
raise TypeError(f"Shapes of x {x.shape} and z {z.shape} do not match")
if y.shape != z.shape:
raise TypeError(f"Shapes of y {y.shape} and z {z.shape} do not match")
else:
raise TypeError(f"Inputs x and y must be None, 1D or 2D, not {x.ndim}D")
# Check mask shape just in case.
if mask is not None and mask.shape != z.shape:
raise ValueError("If mask is set it must be a 2D array with the same shape as z")
# Check arguments: name.
if name not in _class_lookup:
raise ValueError(f"Unrecognised contour generator name: {name}")
# Check arguments: chunk_size, chunk_count and total_chunk_count.
y_chunk_size, x_chunk_size = calc_chunk_sizes(
chunk_size, chunk_count, total_chunk_count, ny, nx)
cls = _class_lookup[name]
# Check arguments: corner_mask.
if corner_mask is None:
# Set it to default, which is True if the algorithm supports it.
corner_mask = cls.supports_corner_mask()
elif corner_mask and not cls.supports_corner_mask():
raise ValueError(f"{name} contour generator does not support corner_mask=True")
# Check arguments: line_type.
if line_type is None:
line_type = cls.default_line_type
else:
line_type = as_line_type(line_type)
if not cls.supports_line_type(line_type):
raise ValueError(f"{name} contour generator does not support line_type {line_type}")
# Check arguments: fill_type.
if fill_type is None:
fill_type = cls.default_fill_type
else:
fill_type = as_fill_type(fill_type)
if not cls.supports_fill_type(fill_type):
raise ValueError(f"{name} contour generator does not support fill_type {fill_type}")
# Check arguments: quad_as_tri.
if quad_as_tri and not cls.supports_quad_as_tri():
raise ValueError(f"{name} contour generator does not support quad_as_tri=True")
# Check arguments: z_interp.
if z_interp is None:
z_interp = ZInterp.Linear
else:
z_interp = as_z_interp(z_interp)
if z_interp != ZInterp.Linear and not cls.supports_z_interp():
raise ValueError(f"{name} contour generator does not support z_interp {z_interp}")
# Check arguments: thread_count.
if thread_count not in (0, 1) and not cls.supports_threads():
raise ValueError(f"{name} contour generator does not support thread_count {thread_count}")
# Prepare args and kwargs for contour generator constructor.
args = [x, y, z, mask]
kwargs: dict[str, int | bool | LineType | FillType | ZInterp] = {
"x_chunk_size": x_chunk_size,
"y_chunk_size": y_chunk_size,
}
if name not in ("mpl2005", "mpl2014"):
kwargs["line_type"] = line_type
kwargs["fill_type"] = fill_type
if cls.supports_corner_mask():
kwargs["corner_mask"] = corner_mask
if cls.supports_quad_as_tri():
kwargs["quad_as_tri"] = quad_as_tri
if cls.supports_z_interp():
kwargs["z_interp"] = z_interp
if cls.supports_threads():
kwargs["thread_count"] = thread_count
# Create contour generator.
cont_gen = cls(*args, **kwargs)
return cont_gen

View File

@@ -0,0 +1,198 @@
from __future__ import annotations
from typing import ClassVar, NoReturn
import numpy as np
import numpy.typing as npt
from typing_extensions import TypeAlias
import contourpy._contourpy as cpy
# Input numpy array types, the same as in common.h
CoordinateArray: TypeAlias = npt.NDArray[np.float64]
MaskArray: TypeAlias = npt.NDArray[np.bool_]
# Output numpy array types, the same as in common.h
PointArray: TypeAlias = npt.NDArray[np.float64]
CodeArray: TypeAlias = npt.NDArray[np.uint8]
OffsetArray: TypeAlias = npt.NDArray[np.uint32]
# Types returned from filled()
FillReturn_OuterCode: TypeAlias = tuple[list[PointArray], list[CodeArray]]
FillReturn_OuterOffset: TypeAlias = tuple[list[PointArray], list[OffsetArray]]
FillReturn_ChunkCombinedCode: TypeAlias = tuple[list[PointArray | None], list[CodeArray | None]]
FillReturn_ChunkCombinedOffset: TypeAlias = tuple[list[PointArray | None], list[OffsetArray | None]]
FillReturn_ChunkCombinedCodeOffset: TypeAlias = tuple[list[PointArray | None], list[CodeArray | None], list[OffsetArray | None]]
FillReturn_ChunkCombinedOffsetOffset: TypeAlias = tuple[list[PointArray | None], list[OffsetArray | None], list[OffsetArray | None]]
FillReturn: TypeAlias = FillReturn_OuterCode | FillReturn_OuterOffset | FillReturn_ChunkCombinedCode | FillReturn_ChunkCombinedOffset | FillReturn_ChunkCombinedCodeOffset | FillReturn_ChunkCombinedOffsetOffset
# Types returned from lines()
LineReturn_Separate: TypeAlias = list[PointArray]
LineReturn_SeparateCode: TypeAlias = tuple[list[PointArray], list[CodeArray]]
LineReturn_ChunkCombinedCode: TypeAlias = tuple[list[PointArray | None], list[CodeArray | None]]
LineReturn_ChunkCombinedOffset: TypeAlias = tuple[list[PointArray | None], list[OffsetArray | None]]
LineReturn: TypeAlias = LineReturn_Separate | LineReturn_SeparateCode | LineReturn_ChunkCombinedCode | LineReturn_ChunkCombinedOffset
NDEBUG: int
__version__: str
class FillType:
ChunkCombinedCode: ClassVar[cpy.FillType]
ChunkCombinedCodeOffset: ClassVar[cpy.FillType]
ChunkCombinedOffset: ClassVar[cpy.FillType]
ChunkCombinedOffsetOffset: ClassVar[cpy.FillType]
OuterCode: ClassVar[cpy.FillType]
OuterOffset: ClassVar[cpy.FillType]
__members__: ClassVar[dict[str, cpy.FillType]]
def __eq__(self, other: object) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: int) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: object) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: int) -> NoReturn: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class LineType:
ChunkCombinedCode: ClassVar[cpy.LineType]
ChunkCombinedOffset: ClassVar[cpy.LineType]
Separate: ClassVar[cpy.LineType]
SeparateCode: ClassVar[cpy.LineType]
__members__: ClassVar[dict[str, cpy.LineType]]
def __eq__(self, other: object) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: int) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: object) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: int) -> NoReturn: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
class ZInterp:
Linear: ClassVar[cpy.ZInterp]
Log: ClassVar[cpy.ZInterp]
__members__: ClassVar[dict[str, cpy.ZInterp]]
def __eq__(self, other: object) -> bool: ...
def __getstate__(self) -> int: ...
def __hash__(self) -> int: ...
def __index__(self) -> int: ...
def __init__(self, value: int) -> None: ...
def __int__(self) -> int: ...
def __ne__(self, other: object) -> bool: ...
def __repr__(self) -> str: ...
def __setstate__(self, state: int) -> NoReturn: ...
@property
def name(self) -> str: ...
@property
def value(self) -> int: ...
def max_threads() -> int: ...
class ContourGenerator:
def create_contour(self, level: float) -> LineReturn: ...
def create_filled_contour(self, lower_level: float, upper_level: float) -> FillReturn: ...
def filled(self, lower_level: float, upper_level: float) -> FillReturn: ...
def lines(self, level: float) -> LineReturn: ...
@staticmethod
def supports_corner_mask() -> bool: ...
@staticmethod
def supports_fill_type(fill_type: FillType) -> bool: ...
@staticmethod
def supports_line_type(line_type: LineType) -> bool: ...
@staticmethod
def supports_quad_as_tri() -> bool: ...
@staticmethod
def supports_threads() -> bool: ...
@staticmethod
def supports_z_interp() -> bool: ...
@property
def chunk_count(self) -> tuple[int, int]: ...
@property
def chunk_size(self) -> tuple[int, int]: ...
@property
def corner_mask(self) -> bool: ...
@property
def fill_type(self) -> FillType: ...
@property
def line_type(self) -> LineType: ...
@property
def quad_as_tri(self) -> bool: ...
@property
def thread_count(self) -> int: ...
@property
def z_interp(self) -> ZInterp: ...
default_fill_type: cpy.FillType
default_line_type: cpy.LineType
class Mpl2005ContourGenerator(ContourGenerator):
def __init__(
self,
x: CoordinateArray,
y: CoordinateArray,
z: CoordinateArray,
mask: MaskArray,
*,
x_chunk_size: int = 0,
y_chunk_size: int = 0,
) -> None: ...
class Mpl2014ContourGenerator(ContourGenerator):
def __init__(
self,
x: CoordinateArray,
y: CoordinateArray,
z: CoordinateArray,
mask: MaskArray,
*,
corner_mask: bool,
x_chunk_size: int = 0,
y_chunk_size: int = 0,
) -> None: ...
class SerialContourGenerator(ContourGenerator):
def __init__(
self,
x: CoordinateArray,
y: CoordinateArray,
z: CoordinateArray,
mask: MaskArray,
*,
corner_mask: bool,
line_type: LineType,
fill_type: FillType,
quad_as_tri: bool,
z_interp: ZInterp,
x_chunk_size: int = 0,
y_chunk_size: int = 0,
) -> None: ...
def _write_cache(self) -> NoReturn: ...
class ThreadedContourGenerator(ContourGenerator):
def __init__(
self,
x: CoordinateArray,
y: CoordinateArray,
z: CoordinateArray,
mask: MaskArray,
*,
corner_mask: bool,
line_type: LineType,
fill_type: FillType,
quad_as_tri: bool,
z_interp: ZInterp,
x_chunk_size: int = 0,
y_chunk_size: int = 0,
thread_count: int = 0,
) -> None: ...
def _write_cache(self) -> None: ...

View File

@@ -0,0 +1 @@
__version__ = "1.1.1"

View File

@@ -0,0 +1,95 @@
from __future__ import annotations
import math
def calc_chunk_sizes(
chunk_size: int | tuple[int, int] | None,
chunk_count: int | tuple[int, int] | None,
total_chunk_count: int | None,
ny: int,
nx: int,
) -> tuple[int, int]:
"""Calculate chunk sizes.
Args:
chunk_size (int or tuple(int, int), optional): Chunk size in (y, x) directions, or the same
size in both directions if only one is specified. Cannot be negative.
chunk_count (int or tuple(int, int), optional): Chunk count in (y, x) directions, or the
same count in both directions if only one is specified. If less than 1, set to 1.
total_chunk_count (int, optional): Total number of chunks. If less than 1, set to 1.
ny (int): Number of grid points in y-direction.
nx (int): Number of grid points in x-direction.
Return:
tuple(int, int): Chunk sizes (y_chunk_size, x_chunk_size).
Note:
Zero or one of ``chunk_size``, ``chunk_count`` and ``total_chunk_count`` should be
specified.
"""
if sum([chunk_size is not None, chunk_count is not None, total_chunk_count is not None]) > 1:
raise ValueError("Only one of chunk_size, chunk_count and total_chunk_count should be set")
if nx < 2 or ny < 2:
raise ValueError(f"(ny, nx) must be at least (2, 2), not ({ny}, {nx})")
if total_chunk_count is not None:
max_chunk_count = (nx-1)*(ny-1)
total_chunk_count = min(max(total_chunk_count, 1), max_chunk_count)
if total_chunk_count == 1:
chunk_size = 0
elif total_chunk_count == max_chunk_count:
chunk_size = (1, 1)
else:
factors = two_factors(total_chunk_count)
if ny > nx:
chunk_count = factors
else:
chunk_count = (factors[1], factors[0])
if chunk_count is not None:
if isinstance(chunk_count, tuple):
y_chunk_count, x_chunk_count = chunk_count
else:
y_chunk_count = x_chunk_count = chunk_count
x_chunk_count = min(max(x_chunk_count, 1), nx-1)
y_chunk_count = min(max(y_chunk_count, 1), ny-1)
chunk_size = (math.ceil((ny-1) / y_chunk_count), math.ceil((nx-1) / x_chunk_count))
if chunk_size is None:
y_chunk_size = x_chunk_size = 0
elif isinstance(chunk_size, tuple):
y_chunk_size, x_chunk_size = chunk_size
else:
y_chunk_size = x_chunk_size = chunk_size
if x_chunk_size < 0 or y_chunk_size < 0:
raise ValueError("chunk_size cannot be negative")
return y_chunk_size, x_chunk_size
def two_factors(n: int) -> tuple[int, int]:
"""Split an integer into two integer factors.
The two factors will be as close as possible to the sqrt of n, and are returned in decreasing
order. Worst case returns (n, 1).
Args:
n (int): The integer to factorize, must be positive.
Return:
tuple(int, int): The two factors of n, in decreasing order.
"""
if n < 0:
raise ValueError(f"two_factors expects positive integer not {n}")
i = math.ceil(math.sqrt(n))
while n % i != 0:
i -= 1
j = n // i
if i > j:
return i, j
else:
return j, i

View File

@@ -0,0 +1,48 @@
from __future__ import annotations
from contourpy._contourpy import FillType, LineType, ZInterp
def as_fill_type(fill_type: FillType | str) -> FillType:
"""Coerce a FillType or string value to a FillType.
Args:
fill_type (FillType or str): Value to convert.
Return:
FillType: Converted value.
"""
if isinstance(fill_type, str):
return FillType.__members__[fill_type]
else:
return fill_type
def as_line_type(line_type: LineType | str) -> LineType:
"""Coerce a LineType or string value to a LineType.
Args:
line_type (LineType or str): Value to convert.
Return:
LineType: Converted value.
"""
if isinstance(line_type, str):
return LineType.__members__[line_type]
else:
return line_type
def as_z_interp(z_interp: ZInterp | str) -> ZInterp:
"""Coerce a ZInterp or string value to a ZInterp.
Args:
z_interp (ZInterp or str): Value to convert.
Return:
ZInterp: Converted value.
"""
if isinstance(z_interp, str):
return ZInterp.__members__[z_interp]
else:
return z_interp

View File

@@ -0,0 +1,5 @@
from __future__ import annotations
from contourpy.util._build_config import build_config
__all__ = ["build_config"]

View File

@@ -0,0 +1,58 @@
# _build_config.py.in is converted into _build_config.py during the meson build process.
from __future__ import annotations
def build_config() -> dict[str, str]:
"""
Return a dictionary containing build configuration settings.
All dictionary keys and values are strings, for example ``False`` is
returned as ``"False"``.
"""
return dict(
# Python settings
python_version="3.8",
python_install_dir=r"/usr/local/lib/python3.8/site-packages/",
python_path=r"/tmp/build-env-dhtgjwl5/bin/python",
# Package versions
contourpy_version="1.1.1",
meson_version="1.2.1",
mesonpy_version="0.14.0",
pybind11_version="2.11.1",
# Misc meson settings
meson_backend="ninja",
build_dir=r"/project/.mesonpy-otn0iaj2/lib/contourpy/util",
source_dir=r"/project/lib/contourpy/util",
cross_build="False",
# Build options
build_options=r"-Dbuildtype=release -Db_ndebug=if-release -Db_vscrt=md -Dvsenv=True --native-file=/project/.mesonpy-otn0iaj2/meson-python-native-file.ini",
buildtype="release",
cpp_std="c++17",
debug="False",
optimization="3",
vsenv="True",
b_ndebug="if-release",
b_vscrt="from_buildtype",
# C++ compiler
compiler_name="gcc",
compiler_version="10.2.1",
linker_id="ld.bfd",
compile_command="c++",
# Host machine
host_cpu="x86_64",
host_cpu_family="x86_64",
host_cpu_endian="little",
host_cpu_system="linux",
# Build machine, same as host machine if not a cross_build
build_cpu="x86_64",
build_cpu_family="x86_64",
build_cpu_endian="little",
build_cpu_system="linux",
)

View File

@@ -0,0 +1,329 @@
from __future__ import annotations
import io
from typing import TYPE_CHECKING, Any
from bokeh.io import export_png, export_svg, show
from bokeh.io.export import get_screenshot_as_png
from bokeh.layouts import gridplot
from bokeh.models.annotations.labels import Label
from bokeh.palettes import Category10
from bokeh.plotting import figure
import numpy as np
from contourpy import FillType, LineType
from contourpy.util.bokeh_util import filled_to_bokeh, lines_to_bokeh
from contourpy.util.renderer import Renderer
if TYPE_CHECKING:
from bokeh.models import GridPlot
from bokeh.palettes import Palette
from numpy.typing import ArrayLike
from selenium.webdriver.remote.webdriver import WebDriver
from contourpy._contourpy import FillReturn, LineReturn
class BokehRenderer(Renderer):
_figures: list[figure]
_layout: GridPlot
_palette: Palette
_want_svg: bool
"""Utility renderer using Bokeh to render a grid of plots over the same (x, y) range.
Args:
nrows (int, optional): Number of rows of plots, default ``1``.
ncols (int, optional): Number of columns of plots, default ``1``.
figsize (tuple(float, float), optional): Figure size in inches (assuming 100 dpi), default
``(9, 9)``.
show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
want_svg (bool, optional): Whether output is required in SVG format or not, default
``False``.
Warning:
:class:`~contourpy.util.bokeh_renderer.BokehRenderer`, unlike
:class:`~contourpy.util.mpl_renderer.MplRenderer`, needs to be told in advance if output to
SVG format will be required later, otherwise it will assume PNG output.
"""
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
show_frame: bool = True,
want_svg: bool = False,
) -> None:
self._want_svg = want_svg
self._palette = Category10[10]
total_size = 100*np.asarray(figsize, dtype=int) # Assuming 100 dpi.
nfigures = nrows*ncols
self._figures = []
backend = "svg" if self._want_svg else "canvas"
for _ in range(nfigures):
fig = figure(output_backend=backend)
fig.xgrid.visible = False
fig.ygrid.visible = False
self._figures.append(fig)
if not show_frame:
fig.outline_line_color = None # type: ignore[assignment]
fig.axis.visible = False
self._layout = gridplot(
self._figures, ncols=ncols, toolbar_location=None, # type: ignore[arg-type]
width=total_size[0] // ncols, height=total_size[1] // nrows)
def _convert_color(self, color: str) -> str:
if isinstance(color, str) and color[0] == "C":
index = int(color[1:])
color = self._palette[index]
return color
def _get_figure(self, ax: figure | int) -> figure:
if isinstance(ax, int):
ax = self._figures[ax]
return ax
def filled(
self,
filled: FillReturn,
fill_type: FillType,
ax: figure | int = 0,
color: str = "C0",
alpha: float = 0.7,
) -> None:
"""Plot filled contours on a single plot.
Args:
filled (sequence of arrays): Filled contour data as returned by
:func:`~contourpy.ContourGenerator.filled`.
fill_type (FillType): Type of ``filled`` data, as returned by
:attr:`~contourpy.ContourGenerator.fill_type`.
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
color (str, optional): Color to plot with. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``Category10`` palette. Default ``"C0"``.
alpha (float, optional): Opacity to plot with, default ``0.7``.
"""
fig = self._get_figure(ax)
color = self._convert_color(color)
xs, ys = filled_to_bokeh(filled, fill_type)
if len(xs) > 0:
fig.multi_polygons(xs=[xs], ys=[ys], color=color, fill_alpha=alpha, line_width=0)
def grid(
self,
x: ArrayLike,
y: ArrayLike,
ax: figure | int = 0,
color: str = "black",
alpha: float = 0.1,
point_color: str | None = None,
quad_as_tri_alpha: float = 0,
) -> None:
"""Plot quad grid lines on a single plot.
Args:
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
color (str, optional): Color to plot grid lines, default ``"black"``.
alpha (float, optional): Opacity to plot lines with, default ``0.1``.
point_color (str, optional): Color to plot grid points or ``None`` if grid points
should not be plotted, default ``None``.
quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default
``0``.
Colors may be a string color or the letter ``"C"`` followed by an integer in the range
``"C0"`` to ``"C9"`` to use a color from the ``Category10`` palette.
Warning:
``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
"""
fig = self._get_figure(ax)
x, y = self._grid_as_2d(x, y)
xs = [row for row in x] + [row for row in x.T]
ys = [row for row in y] + [row for row in y.T]
kwargs = dict(line_color=color, alpha=alpha)
fig.multi_line(xs, ys, **kwargs)
if quad_as_tri_alpha > 0:
# Assumes no quad mask.
xmid = (0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])).ravel()
ymid = (0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])).ravel()
fig.multi_line(
[row for row in np.stack((x[:-1, :-1].ravel(), xmid, x[1:, 1:].ravel()), axis=1)],
[row for row in np.stack((y[:-1, :-1].ravel(), ymid, y[1:, 1:].ravel()), axis=1)],
**kwargs)
fig.multi_line(
[row for row in np.stack((x[:-1, 1:].ravel(), xmid, x[1:, :-1].ravel()), axis=1)],
[row for row in np.stack((y[:-1, 1:].ravel(), ymid, y[1:, :-1].ravel()), axis=1)],
**kwargs)
if point_color is not None:
fig.circle(
x=x.ravel(), y=y.ravel(), fill_color=color, line_color=None, alpha=alpha, size=8)
def lines(
self,
lines: LineReturn,
line_type: LineType,
ax: figure | int = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
) -> None:
"""Plot contour lines on a single plot.
Args:
lines (sequence of arrays): Contour line data as returned by
:func:`~contourpy.ContourGenerator.lines`.
line_type (LineType): Type of ``lines`` data, as returned by
:attr:`~contourpy.ContourGenerator.line_type`.
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
color (str, optional): Color to plot lines. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``Category10`` palette. Default ``"C0"``.
alpha (float, optional): Opacity to plot lines with, default ``1.0``.
linewidth (float, optional): Width of lines, default ``1``.
Note:
Assumes all lines are open line strips not closed line loops.
"""
fig = self._get_figure(ax)
color = self._convert_color(color)
xs, ys = lines_to_bokeh(lines, line_type)
if len(xs) > 0:
fig.multi_line(xs, ys, line_color=color, line_alpha=alpha, line_width=linewidth)
def mask(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike | np.ma.MaskedArray[Any, Any],
ax: figure | int = 0,
color: str = "black",
) -> None:
"""Plot masked out grid points as circles on a single plot.
Args:
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
z (masked array of shape (ny, nx): z-values.
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
color (str, optional): Circle color, default ``"black"``.
"""
mask = np.ma.getmask(z) # type: ignore[no-untyped-call]
if mask is np.ma.nomask:
return
fig = self._get_figure(ax)
color = self._convert_color(color)
x, y = self._grid_as_2d(x, y)
fig.circle(x[mask], y[mask], fill_color=color, size=10)
def save(
self,
filename: str,
transparent: bool = False,
*,
webdriver: WebDriver | None = None,
) -> None:
"""Save plots to SVG or PNG file.
Args:
filename (str): Filename to save to.
transparent (bool, optional): Whether background should be transparent, default
``False``.
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
Warning:
To output to SVG file, ``want_svg=True`` must have been passed to the constructor.
"""
if transparent:
for fig in self._figures:
fig.background_fill_color = None # type: ignore[assignment]
fig.border_fill_color = None # type: ignore[assignment]
if self._want_svg:
export_svg(self._layout, filename=filename, webdriver=webdriver)
else:
export_png(self._layout, filename=filename, webdriver=webdriver)
def save_to_buffer(self, *, webdriver: WebDriver | None = None) -> io.BytesIO:
"""Save plots to an ``io.BytesIO`` buffer.
Args:
webdriver (WebDriver, optional): Selenium WebDriver instance to use to create the image.
Return:
BytesIO: PNG image buffer.
"""
image = get_screenshot_as_png(self._layout, driver=webdriver)
buffer = io.BytesIO()
image.save(buffer, "png")
return buffer
def show(self) -> None:
"""Show plots in web browser, in usual Bokeh manner.
"""
show(self._layout)
def title(self, title: str, ax: figure | int = 0, color: str | None = None) -> None:
"""Set the title of a single plot.
Args:
title (str): Title text.
ax (int or Bokeh Figure, optional): Which plot to set the title of, default ``0``.
color (str, optional): Color to set title. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``Category10`` palette. Default ``None`` which is ``black``.
"""
fig = self._get_figure(ax)
fig.title = title # type: ignore[assignment]
fig.title.align = "center" # type: ignore[attr-defined]
if color is not None:
fig.title.text_color = self._convert_color(color) # type: ignore[attr-defined]
def z_values(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: figure | int = 0,
color: str = "green",
fmt: str = ".1f",
quad_as_tri: bool = False,
) -> None:
"""Show ``z`` values on a single plot.
Args:
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
z (array-like of shape (ny, nx): z-values.
ax (int or Bokeh Figure, optional): Which plot to use, default ``0``.
color (str, optional): Color of added text. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``Category10`` palette. Default ``"green"``.
fmt (str, optional): Format to display z-values, default ``".1f"``.
quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centres
of quads.
Warning:
``quad_as_tri=True`` shows z-values for all quads, even if masked.
"""
fig = self._get_figure(ax)
color = self._convert_color(color)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
kwargs = dict(text_color=color, text_align="center", text_baseline="middle")
for j in range(ny):
for i in range(nx):
fig.add_layout(Label(x=x[j, i], y=y[j, i], text=f"{z[j, i]:{fmt}}", **kwargs))
if quad_as_tri:
for j in range(ny-1):
for i in range(nx-1):
xx = np.mean(x[j:j+2, i:i+2])
yy = np.mean(y[j:j+2, i:i+2])
zz = np.mean(z[j:j+2, i:i+2])
fig.add_layout(Label(x=xx, y=yy, text=f"{zz:{fmt}}", **kwargs))

View File

@@ -0,0 +1,90 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from contourpy import FillType, LineType
from contourpy.util.mpl_util import mpl_codes_to_offsets
if TYPE_CHECKING:
from contourpy._contourpy import (
CoordinateArray, FillReturn, LineReturn, LineReturn_Separate, LineReturn_SeparateCode,
)
def filled_to_bokeh(
filled: FillReturn,
fill_type: FillType,
) -> tuple[list[list[CoordinateArray]], list[list[CoordinateArray]]]:
xs: list[list[CoordinateArray]] = []
ys: list[list[CoordinateArray]] = []
if fill_type in (FillType.OuterOffset, FillType.ChunkCombinedOffset,
FillType.OuterCode, FillType.ChunkCombinedCode):
have_codes = fill_type in (FillType.OuterCode, FillType.ChunkCombinedCode)
for points, offsets in zip(*filled):
if points is None:
continue
if have_codes:
offsets = mpl_codes_to_offsets(offsets)
xs.append([]) # New outer with zero or more holes.
ys.append([])
for i in range(len(offsets)-1):
xys = points[offsets[i]:offsets[i+1]]
xs[-1].append(xys[:, 0])
ys[-1].append(xys[:, 1])
elif fill_type in (FillType.ChunkCombinedCodeOffset, FillType.ChunkCombinedOffsetOffset):
for points, codes_or_offsets, outer_offsets in zip(*filled):
if points is None:
continue
for j in range(len(outer_offsets)-1):
if fill_type == FillType.ChunkCombinedCodeOffset:
codes = codes_or_offsets[outer_offsets[j]:outer_offsets[j+1]]
offsets = mpl_codes_to_offsets(codes) + outer_offsets[j]
else:
offsets = codes_or_offsets[outer_offsets[j]:outer_offsets[j+1]+1]
xs.append([]) # New outer with zero or more holes.
ys.append([])
for k in range(len(offsets)-1):
xys = points[offsets[k]:offsets[k+1]]
xs[-1].append(xys[:, 0])
ys[-1].append(xys[:, 1])
else:
raise RuntimeError(f"Conversion of FillType {fill_type} to Bokeh is not implemented")
return xs, ys
def lines_to_bokeh(
lines: LineReturn,
line_type: LineType,
) -> tuple[list[CoordinateArray], list[CoordinateArray]]:
xs: list[CoordinateArray] = []
ys: list[CoordinateArray] = []
if line_type == LineType.Separate:
if TYPE_CHECKING:
lines = cast(LineReturn_Separate, lines)
for line in lines:
xs.append(line[:, 0])
ys.append(line[:, 1])
elif line_type == LineType.SeparateCode:
if TYPE_CHECKING:
lines = cast(LineReturn_SeparateCode, lines)
for line in lines[0]:
xs.append(line[:, 0])
ys.append(line[:, 1])
elif line_type in (LineType.ChunkCombinedCode, LineType.ChunkCombinedOffset):
for points, offsets in zip(*lines):
if points is None:
continue
if line_type == LineType.ChunkCombinedCode:
offsets = mpl_codes_to_offsets(offsets)
for i in range(len(offsets)-1):
line = points[offsets[i]:offsets[i+1]]
xs.append(line[:, 0])
ys.append(line[:, 1])
else:
raise RuntimeError(f"Conversion of LineType {line_type} to Bokeh is not implemented")
return xs, ys

View File

@@ -0,0 +1,78 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
from contourpy._contourpy import CoordinateArray
def simple(
shape: tuple[int, int], want_mask: bool = False,
) -> tuple[CoordinateArray, CoordinateArray, CoordinateArray | np.ma.MaskedArray[Any, Any]]:
"""Return simple test data consisting of the sum of two gaussians.
Args:
shape (tuple(int, int)): 2D shape of data to return.
want_mask (bool, optional): Whether test data should be masked or not, default ``False``.
Return:
Tuple of 3 arrays: ``x``, ``y``, ``z`` test data, ``z`` will be masked if
``want_mask=True``.
"""
ny, nx = shape
x = np.arange(nx, dtype=np.float64)
y = np.arange(ny, dtype=np.float64)
x, y = np.meshgrid(x, y)
xscale = nx - 1.0
yscale = ny - 1.0
# z is sum of 2D gaussians.
amp = np.asarray([1.0, -1.0, 0.8, -0.9, 0.7])
mid = np.asarray([[0.4, 0.2], [0.3, 0.8], [0.9, 0.75], [0.7, 0.3], [0.05, 0.7]])
width = np.asarray([0.4, 0.2, 0.2, 0.2, 0.1])
z = np.zeros_like(x)
for i in range(len(amp)):
z += amp[i]*np.exp(-((x/xscale - mid[i, 0])**2 + (y/yscale - mid[i, 1])**2) / width[i]**2)
if want_mask:
mask = np.logical_or(
((x/xscale - 1.0)**2 / 0.2 + (y/yscale - 0.0)**2 / 0.1) < 1.0,
((x/xscale - 0.2)**2 / 0.02 + (y/yscale - 0.45)**2 / 0.08) < 1.0,
)
z = np.ma.array(z, mask=mask) # type: ignore[no-untyped-call]
return x, y, z
def random(
shape: tuple[int, int], seed: int = 2187, mask_fraction: float = 0.0,
) -> tuple[CoordinateArray, CoordinateArray, CoordinateArray | np.ma.MaskedArray[Any, Any]]:
"""Return random test data..
Args:
shape (tuple(int, int)): 2D shape of data to return.
seed (int, optional): Seed for random number generator, default 2187.
mask_fraction (float, optional): Fraction of elements to mask, default 0.
Return:
Tuple of 3 arrays: ``x``, ``y``, ``z`` test data, ``z`` will be masked if
``mask_fraction`` is greater than zero.
"""
ny, nx = shape
x = np.arange(nx, dtype=np.float64)
y = np.arange(ny, dtype=np.float64)
x, y = np.meshgrid(x, y)
rng = np.random.default_rng(seed)
z = rng.uniform(size=shape)
if mask_fraction > 0.0:
mask_fraction = min(mask_fraction, 0.99)
mask = rng.uniform(size=shape) < mask_fraction
z = np.ma.array(z, mask=mask) # type: ignore[no-untyped-call]
return x, y, z

View File

@@ -0,0 +1,613 @@
from __future__ import annotations
import io
from typing import TYPE_CHECKING, Any, cast
import matplotlib.collections as mcollections
import matplotlib.pyplot as plt
import numpy as np
from contourpy import FillType, LineType
from contourpy.util.mpl_util import filled_to_mpl_paths, lines_to_mpl_paths, mpl_codes_to_offsets
from contourpy.util.renderer import Renderer
if TYPE_CHECKING:
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from numpy.typing import ArrayLike
import contourpy._contourpy as cpy
class MplRenderer(Renderer):
_axes: Axes
_fig: Figure
_want_tight: bool
"""Utility renderer using Matplotlib to render a grid of plots over the same (x, y) range.
Args:
nrows (int, optional): Number of rows of plots, default ``1``.
ncols (int, optional): Number of columns of plots, default ``1``.
figsize (tuple(float, float), optional): Figure size in inches, default ``(9, 9)``.
show_frame (bool, optional): Whether to show frame and axes ticks, default ``True``.
backend (str, optional): Matplotlib backend to use or ``None`` for default backend.
Default ``None``.
gridspec_kw (dict, optional): Gridspec keyword arguments to pass to ``plt.subplots``,
default None.
"""
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
show_frame: bool = True,
backend: str | None = None,
gridspec_kw: dict[str, Any] | None = None,
) -> None:
if backend is not None:
import matplotlib
matplotlib.use(backend)
kwargs = dict(figsize=figsize, squeeze=False, sharex=True, sharey=True)
if gridspec_kw is not None:
kwargs["gridspec_kw"] = gridspec_kw
else:
kwargs["subplot_kw"] = dict(aspect="equal")
self._fig, axes = plt.subplots(nrows, ncols, **kwargs)
self._axes = axes.flatten()
if not show_frame:
for ax in self._axes:
ax.axis("off")
self._want_tight = True
def __del__(self) -> None:
if hasattr(self, "_fig"):
plt.close(self._fig)
def _autoscale(self) -> None:
# Using axes._need_autoscale attribute if need to autoscale before rendering after adding
# lines/filled. Only want to autoscale once per axes regardless of how many lines/filled
# added.
for ax in self._axes:
if getattr(ax, "_need_autoscale", False):
ax.autoscale_view(tight=True)
ax._need_autoscale = False
if self._want_tight and len(self._axes) > 1:
self._fig.tight_layout()
def _get_ax(self, ax: Axes | int) -> Axes:
if isinstance(ax, int):
ax = self._axes[ax]
return ax
def filled(
self,
filled: cpy.FillReturn,
fill_type: FillType,
ax: Axes | int = 0,
color: str = "C0",
alpha: float = 0.7,
) -> None:
"""Plot filled contours on a single Axes.
Args:
filled (sequence of arrays): Filled contour data as returned by
:func:`~contourpy.ContourGenerator.filled`.
fill_type (FillType): Type of ``filled`` data, as returned by
:attr:`~contourpy.ContourGenerator.fill_type`.
ax (int or Maplotlib Axes, optional): Which axes to plot on, default ``0``.
color (str, optional): Color to plot with. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``tab10`` colormap. Default ``"C0"``.
alpha (float, optional): Opacity to plot with, default ``0.7``.
"""
ax = self._get_ax(ax)
paths = filled_to_mpl_paths(filled, fill_type)
collection = mcollections.PathCollection(
paths, facecolors=color, edgecolors="none", lw=0, alpha=alpha)
ax.add_collection(collection)
ax._need_autoscale = True
def grid(
self,
x: ArrayLike,
y: ArrayLike,
ax: Axes | int = 0,
color: str = "black",
alpha: float = 0.1,
point_color: str | None = None,
quad_as_tri_alpha: float = 0,
) -> None:
"""Plot quad grid lines on a single Axes.
Args:
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
color (str, optional): Color to plot grid lines, default ``"black"``.
alpha (float, optional): Opacity to plot lines with, default ``0.1``.
point_color (str, optional): Color to plot grid points or ``None`` if grid points
should not be plotted, default ``None``.
quad_as_tri_alpha (float, optional): Opacity to plot ``quad_as_tri`` grid, default 0.
Colors may be a string color or the letter ``"C"`` followed by an integer in the range
``"C0"`` to ``"C9"`` to use a color from the ``tab10`` colormap.
Warning:
``quad_as_tri_alpha > 0`` plots all quads as though they are unmasked.
"""
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
kwargs = dict(color=color, alpha=alpha)
ax.plot(x, y, x.T, y.T, **kwargs)
if quad_as_tri_alpha > 0:
# Assumes no quad mask.
xmid = 0.25*(x[:-1, :-1] + x[1:, :-1] + x[:-1, 1:] + x[1:, 1:])
ymid = 0.25*(y[:-1, :-1] + y[1:, :-1] + y[:-1, 1:] + y[1:, 1:])
kwargs["alpha"] = quad_as_tri_alpha
ax.plot(
np.stack((x[:-1, :-1], xmid, x[1:, 1:])).reshape((3, -1)),
np.stack((y[:-1, :-1], ymid, y[1:, 1:])).reshape((3, -1)),
np.stack((x[1:, :-1], xmid, x[:-1, 1:])).reshape((3, -1)),
np.stack((y[1:, :-1], ymid, y[:-1, 1:])).reshape((3, -1)),
**kwargs)
if point_color is not None:
ax.plot(x, y, color=point_color, alpha=alpha, marker="o", lw=0)
ax._need_autoscale = True
def lines(
self,
lines: cpy.LineReturn,
line_type: LineType,
ax: Axes | int = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
) -> None:
"""Plot contour lines on a single Axes.
Args:
lines (sequence of arrays): Contour line data as returned by
:func:`~contourpy.ContourGenerator.lines`.
line_type (LineType): Type of ``lines`` data, as returned by
:attr:`~contourpy.ContourGenerator.line_type`.
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
color (str, optional): Color to plot lines. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``tab10`` colormap. Default ``"C0"``.
alpha (float, optional): Opacity to plot lines with, default ``1.0``.
linewidth (float, optional): Width of lines, default ``1``.
"""
ax = self._get_ax(ax)
paths = lines_to_mpl_paths(lines, line_type)
collection = mcollections.PathCollection(
paths, facecolors="none", edgecolors=color, lw=linewidth, alpha=alpha)
ax.add_collection(collection)
ax._need_autoscale = True
def mask(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike | np.ma.MaskedArray[Any, Any],
ax: Axes | int = 0,
color: str = "black",
) -> None:
"""Plot masked out grid points as circles on a single Axes.
Args:
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
z (masked array of shape (ny, nx): z-values.
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
color (str, optional): Circle color, default ``"black"``.
"""
mask = np.ma.getmask(z) # type: ignore[no-untyped-call]
if mask is np.ma.nomask:
return
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
ax.plot(x[mask], y[mask], "o", c=color)
def save(self, filename: str, transparent: bool = False) -> None:
"""Save plots to SVG or PNG file.
Args:
filename (str): Filename to save to.
transparent (bool, optional): Whether background should be transparent, default
``False``.
"""
self._autoscale()
self._fig.savefig(filename, transparent=transparent)
def save_to_buffer(self) -> io.BytesIO:
"""Save plots to an ``io.BytesIO`` buffer.
Return:
BytesIO: PNG image buffer.
"""
self._autoscale()
buf = io.BytesIO()
self._fig.savefig(buf, format="png")
buf.seek(0)
return buf
def show(self) -> None:
"""Show plots in an interactive window, in the usual Matplotlib manner.
"""
self._autoscale()
plt.show()
def title(self, title: str, ax: Axes | int = 0, color: str | None = None) -> None:
"""Set the title of a single Axes.
Args:
title (str): Title text.
ax (int or Matplotlib Axes, optional): Which Axes to set the title of, default ``0``.
color (str, optional): Color to set title. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``tab10`` colormap. Default is ``None`` which uses Matplotlib's default title color
that depends on the stylesheet in use.
"""
if color:
self._get_ax(ax).set_title(title, color=color)
else:
self._get_ax(ax).set_title(title)
def z_values(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Axes | int = 0,
color: str = "green",
fmt: str = ".1f",
quad_as_tri: bool = False,
) -> None:
"""Show ``z`` values on a single Axes.
Args:
x (array-like of shape (ny, nx) or (nx,)): The x-coordinates of the grid points.
y (array-like of shape (ny, nx) or (ny,)): The y-coordinates of the grid points.
z (array-like of shape (ny, nx): z-values.
ax (int or Matplotlib Axes, optional): Which Axes to plot on, default ``0``.
color (str, optional): Color of added text. May be a string color or the letter ``"C"``
followed by an integer in the range ``"C0"`` to ``"C9"`` to use a color from the
``tab10`` colormap. Default ``"green"``.
fmt (str, optional): Format to display z-values, default ``".1f"``.
quad_as_tri (bool, optional): Whether to show z-values at the ``quad_as_tri`` centers
of quads.
Warning:
``quad_as_tri=True`` shows z-values for all quads, even if masked.
"""
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(ny):
for i in range(nx):
ax.text(x[j, i], y[j, i], f"{z[j, i]:{fmt}}", ha="center", va="center",
color=color, clip_on=True)
if quad_as_tri:
for j in range(ny-1):
for i in range(nx-1):
xx = np.mean(x[j:j+2, i:i+2])
yy = np.mean(y[j:j+2, i:i+2])
zz = np.mean(z[j:j+2, i:i+2])
ax.text(xx, yy, f"{zz:{fmt}}", ha="center", va="center", color=color,
clip_on=True)
class MplTestRenderer(MplRenderer):
"""Test renderer implemented using Matplotlib.
No whitespace around plots and no spines/ticks displayed.
Uses Agg backend, so can only save to file/buffer, cannot call ``show()``.
"""
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
) -> None:
gridspec = {
"left": 0.01,
"right": 0.99,
"top": 0.99,
"bottom": 0.01,
"wspace": 0.01,
"hspace": 0.01,
}
super().__init__(
nrows, ncols, figsize, show_frame=True, backend="Agg", gridspec_kw=gridspec,
)
for ax in self._axes:
ax.set_xmargin(0.0)
ax.set_ymargin(0.0)
ax.set_xticks([])
ax.set_yticks([])
self._want_tight = False
class MplDebugRenderer(MplRenderer):
"""Debug renderer implemented using Matplotlib.
Extends ``MplRenderer`` to add extra information to help in debugging such as markers, arrows,
text, etc.
"""
def __init__(
self,
nrows: int = 1,
ncols: int = 1,
figsize: tuple[float, float] = (9, 9),
show_frame: bool = True,
) -> None:
super().__init__(nrows, ncols, figsize, show_frame)
def _arrow(
self,
ax: Axes,
line_start: cpy.CoordinateArray,
line_end: cpy.CoordinateArray,
color: str,
alpha: float,
arrow_size: float,
) -> None:
mid = 0.5*(line_start + line_end)
along = line_end - line_start
along /= np.sqrt(np.dot(along, along)) # Unit vector.
right = np.asarray((along[1], -along[0]))
arrow = np.stack((
mid - (along*0.5 - right)*arrow_size,
mid + along*0.5*arrow_size,
mid - (along*0.5 + right)*arrow_size,
))
ax.plot(arrow[:, 0], arrow[:, 1], "-", c=color, alpha=alpha)
def _filled_to_lists_of_points_and_offsets(
self,
filled: cpy.FillReturn,
fill_type: FillType,
) -> tuple[list[cpy.PointArray], list[cpy.OffsetArray]]:
if fill_type == FillType.OuterCode:
if TYPE_CHECKING:
filled = cast(cpy.FillReturn_OuterCode, filled)
all_points = filled[0]
all_offsets = [mpl_codes_to_offsets(codes) for codes in filled[1]]
elif fill_type == FillType.ChunkCombinedCode:
if TYPE_CHECKING:
filled = cast(cpy.FillReturn_ChunkCombinedCode, filled)
all_points = [points for points in filled[0] if points is not None]
all_offsets = [mpl_codes_to_offsets(codes) for codes in filled[1] if codes is not None]
elif fill_type == FillType.OuterOffset:
if TYPE_CHECKING:
filled = cast(cpy.FillReturn_OuterOffset, filled)
all_points = filled[0]
all_offsets = filled[1]
elif fill_type == FillType.ChunkCombinedOffset:
if TYPE_CHECKING:
filled = cast(cpy.FillReturn_ChunkCombinedOffset, filled)
all_points = [points for points in filled[0] if points is not None]
all_offsets = [offsets for offsets in filled[1] if offsets is not None]
elif fill_type == FillType.ChunkCombinedCodeOffset:
if TYPE_CHECKING:
filled = cast(cpy.FillReturn_ChunkCombinedCodeOffset, filled)
all_points = []
all_offsets = []
for points, codes, outer_offsets in zip(*filled):
if points is None:
continue
if TYPE_CHECKING:
assert codes is not None and outer_offsets is not None
all_points += np.split(points, outer_offsets[1:-1])
all_codes = np.split(codes, outer_offsets[1:-1])
all_offsets += [mpl_codes_to_offsets(codes) for codes in all_codes]
elif fill_type == FillType.ChunkCombinedOffsetOffset:
if TYPE_CHECKING:
filled = cast(cpy.FillReturn_ChunkCombinedOffsetOffset, filled)
all_points = []
all_offsets = []
for points, offsets, outer_offsets in zip(*filled):
if points is None:
continue
if TYPE_CHECKING:
assert offsets is not None and outer_offsets is not None
for i in range(len(outer_offsets)-1):
offs = offsets[outer_offsets[i]:outer_offsets[i+1]+1]
all_points.append(points[offs[0]:offs[-1]])
all_offsets.append(offs - offs[0])
else:
raise RuntimeError(f"Rendering FillType {fill_type} not implemented")
return all_points, all_offsets
def _lines_to_list_of_points(
self, lines: cpy.LineReturn, line_type: LineType,
) -> list[cpy.PointArray]:
if line_type == LineType.Separate:
if TYPE_CHECKING:
lines = cast(cpy.LineReturn_Separate, lines)
all_lines = lines
elif line_type == LineType.SeparateCode:
if TYPE_CHECKING:
lines = cast(cpy.LineReturn_SeparateCode, lines)
all_lines = lines[0]
elif line_type == LineType.ChunkCombinedCode:
if TYPE_CHECKING:
lines = cast(cpy.LineReturn_ChunkCombinedCode, lines)
all_lines = []
for points, codes in zip(*lines):
if points is not None:
if TYPE_CHECKING:
assert codes is not None
offsets = mpl_codes_to_offsets(codes)
for i in range(len(offsets)-1):
all_lines.append(points[offsets[i]:offsets[i+1]])
elif line_type == LineType.ChunkCombinedOffset:
if TYPE_CHECKING:
lines = cast(cpy.LineReturn_ChunkCombinedOffset, lines)
all_lines = []
for points, all_offsets in zip(*lines):
if points is not None:
if TYPE_CHECKING:
assert all_offsets is not None
for i in range(len(all_offsets)-1):
all_lines.append(points[all_offsets[i]:all_offsets[i+1]])
else:
raise RuntimeError(f"Rendering LineType {line_type} not implemented")
return all_lines
def filled(
self,
filled: cpy.FillReturn,
fill_type: FillType,
ax: Axes | int = 0,
color: str = "C1",
alpha: float = 0.7,
line_color: str = "C0",
line_alpha: float = 0.7,
point_color: str = "C0",
start_point_color: str = "red",
arrow_size: float = 0.1,
) -> None:
super().filled(filled, fill_type, ax, color, alpha)
if line_color is None and point_color is None:
return
ax = self._get_ax(ax)
all_points, all_offsets = self._filled_to_lists_of_points_and_offsets(filled, fill_type)
# Lines.
if line_color is not None:
for points, offsets in zip(all_points, all_offsets):
for start, end in zip(offsets[:-1], offsets[1:]):
xys = points[start:end]
ax.plot(xys[:, 0], xys[:, 1], c=line_color, alpha=line_alpha)
if arrow_size > 0.0:
n = len(xys)
for i in range(n-1):
self._arrow(ax, xys[i], xys[i+1], line_color, line_alpha, arrow_size)
# Points.
if point_color is not None:
for points, offsets in zip(all_points, all_offsets):
mask = np.ones(offsets[-1], dtype=bool)
mask[offsets[1:]-1] = False # Exclude end points.
if start_point_color is not None:
start_indices = offsets[:-1]
mask[start_indices] = False # Exclude start points.
ax.plot(
points[:, 0][mask], points[:, 1][mask], "o", c=point_color, alpha=line_alpha)
if start_point_color is not None:
ax.plot(points[:, 0][start_indices], points[:, 1][start_indices], "o",
c=start_point_color, alpha=line_alpha)
def lines(
self,
lines: cpy.LineReturn,
line_type: LineType,
ax: Axes | int = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
point_color: str = "C0",
start_point_color: str = "red",
arrow_size: float = 0.1,
) -> None:
super().lines(lines, line_type, ax, color, alpha, linewidth)
if arrow_size == 0.0 and point_color is None:
return
ax = self._get_ax(ax)
all_lines = self._lines_to_list_of_points(lines, line_type)
if arrow_size > 0.0:
for line in all_lines:
for i in range(len(line)-1):
self._arrow(ax, line[i], line[i+1], color, alpha, arrow_size)
if point_color is not None:
for line in all_lines:
start_index = 0
end_index = len(line)
if start_point_color is not None:
ax.plot(line[0, 0], line[0, 1], "o", c=start_point_color, alpha=alpha)
start_index = 1
if line[0][0] == line[-1][0] and line[0][1] == line[-1][1]:
end_index -= 1
ax.plot(line[start_index:end_index, 0], line[start_index:end_index, 1], "o",
c=color, alpha=alpha)
def point_numbers(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Axes | int = 0,
color: str = "red",
) -> None:
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(ny):
for i in range(nx):
quad = i + j*nx
ax.text(x[j, i], y[j, i], str(quad), ha="right", va="top", color=color,
clip_on=True)
def quad_numbers(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Axes | int = 0,
color: str = "blue",
) -> None:
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(1, ny):
for i in range(1, nx):
quad = i + j*nx
xmid = x[j-1:j+1, i-1:i+1].mean()
ymid = y[j-1:j+1, i-1:i+1].mean()
ax.text(xmid, ymid, str(quad), ha="center", va="center", color=color, clip_on=True)
def z_levels(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
lower_level: float,
upper_level: float | None = None,
ax: Axes | int = 0,
color: str = "green",
) -> None:
ax = self._get_ax(ax)
x, y = self._grid_as_2d(x, y)
z = np.asarray(z)
ny, nx = z.shape
for j in range(ny):
for i in range(nx):
zz = z[j, i]
if upper_level is not None and zz > upper_level:
z_level = 2
elif zz > lower_level:
z_level = 1
else:
z_level = 0
ax.text(x[j, i], y[j, i], z_level, ha="left", va="bottom", color=color,
clip_on=True)

View File

@@ -0,0 +1,79 @@
from __future__ import annotations
from typing import TYPE_CHECKING, cast
import matplotlib.path as mpath
import numpy as np
from contourpy import FillType, LineType
if TYPE_CHECKING:
from contourpy._contourpy import (
CodeArray, FillReturn, LineReturn, LineReturn_Separate, OffsetArray,
)
def filled_to_mpl_paths(filled: FillReturn, fill_type: FillType) -> list[mpath.Path]:
if fill_type in (FillType.OuterCode, FillType.ChunkCombinedCode):
paths = [mpath.Path(points, codes) for points, codes in zip(*filled) if points is not None]
elif fill_type in (FillType.OuterOffset, FillType.ChunkCombinedOffset):
paths = [mpath.Path(points, offsets_to_mpl_codes(offsets))
for points, offsets in zip(*filled) if points is not None]
elif fill_type == FillType.ChunkCombinedCodeOffset:
paths = []
for points, codes, outer_offsets in zip(*filled):
if points is None:
continue
points = np.split(points, outer_offsets[1:-1])
codes = np.split(codes, outer_offsets[1:-1])
paths += [mpath.Path(p, c) for p, c in zip(points, codes)]
elif fill_type == FillType.ChunkCombinedOffsetOffset:
paths = []
for points, offsets, outer_offsets in zip(*filled):
if points is None:
continue
for i in range(len(outer_offsets)-1):
offs = offsets[outer_offsets[i]:outer_offsets[i+1]+1]
pts = points[offs[0]:offs[-1]]
paths += [mpath.Path(pts, offsets_to_mpl_codes(offs - offs[0]))]
else:
raise RuntimeError(f"Conversion of FillType {fill_type} to MPL Paths is not implemented")
return paths
def lines_to_mpl_paths(lines: LineReturn, line_type: LineType) -> list[mpath.Path]:
if line_type == LineType.Separate:
if TYPE_CHECKING:
lines = cast(LineReturn_Separate, lines)
paths = []
for line in lines:
# Drawing as Paths so that they can be closed correctly.
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
paths.append(mpath.Path(line, closed=closed))
elif line_type in (LineType.SeparateCode, LineType.ChunkCombinedCode):
paths = [mpath.Path(points, codes) for points, codes in zip(*lines) if points is not None]
elif line_type == LineType.ChunkCombinedOffset:
paths = []
for points, offsets in zip(*lines):
if points is None:
continue
for i in range(len(offsets)-1):
line = points[offsets[i]:offsets[i+1]]
closed = line[0, 0] == line[-1, 0] and line[0, 1] == line[-1, 1]
paths.append(mpath.Path(line, closed=closed))
else:
raise RuntimeError(f"Conversion of LineType {line_type} to MPL Paths is not implemented")
return paths
def mpl_codes_to_offsets(codes: CodeArray) -> OffsetArray:
offsets = np.nonzero(codes == 1)[0].astype(np.uint32)
offsets = np.append(offsets, len(codes))
return offsets
def offsets_to_mpl_codes(offsets: OffsetArray) -> CodeArray:
codes = np.full(offsets[-1]-offsets[0], 2, dtype=np.uint8) # LINETO = 2
codes[offsets[:-1]] = 1 # MOVETO = 1
codes[offsets[1:]-1] = 79 # CLOSEPOLY 79
return codes

View File

@@ -0,0 +1,106 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
import io
from numpy.typing import ArrayLike
from contourpy._contourpy import CoordinateArray, FillReturn, FillType, LineReturn, LineType
class Renderer(ABC):
"""Abstract base class for renderers, defining the interface that they must implement."""
def _grid_as_2d(self, x: ArrayLike, y: ArrayLike) -> tuple[CoordinateArray, CoordinateArray]:
x = np.asarray(x)
y = np.asarray(y)
if x.ndim == 1:
x, y = np.meshgrid(x, y)
return x, y
x = np.asarray(x)
y = np.asarray(y)
if x.ndim == 1:
x, y = np.meshgrid(x, y)
return x, y
@abstractmethod
def filled(
self,
filled: FillReturn,
fill_type: FillType,
ax: Any = 0,
color: str = "C0",
alpha: float = 0.7,
) -> None:
pass
@abstractmethod
def grid(
self,
x: ArrayLike,
y: ArrayLike,
ax: Any = 0,
color: str = "black",
alpha: float = 0.1,
point_color: str | None = None,
quad_as_tri_alpha: float = 0,
) -> None:
pass
@abstractmethod
def lines(
self,
lines: LineReturn,
line_type: LineType,
ax: Any = 0,
color: str = "C0",
alpha: float = 1.0,
linewidth: float = 1,
) -> None:
pass
@abstractmethod
def mask(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike | np.ma.MaskedArray[Any, Any],
ax: Any = 0,
color: str = "black",
) -> None:
pass
@abstractmethod
def save(self, filename: str, transparent: bool = False) -> None:
pass
@abstractmethod
def save_to_buffer(self) -> io.BytesIO:
pass
@abstractmethod
def show(self) -> None:
pass
@abstractmethod
def title(self, title: str, ax: Any = 0, color: str | None = None) -> None:
pass
@abstractmethod
def z_values(
self,
x: ArrayLike,
y: ArrayLike,
z: ArrayLike,
ax: Any = 0,
color: str = "green",
fmt: str = ".1f",
quad_as_tri: bool = False,
) -> None:
pass